精华内容
下载资源
问答
  • #include using namespace std; void print(int i, int j) { cout; cout;...在VS2010中,对形参的调用是从右到左调用,并压入栈中,也就是先将i++压入栈底,然后将i压入栈,这样i的值由0变为了1.
    #include <iostream>
    using namespace std;
    
    void print(int i, int j)
    {
    	cout<<i<<endl;
    	cout<<j<<endl;
    }
    
    void main()
    {
    	int i=0;
    	print(i,i++);
    
    }
    

    观察上述代码,常规思维下,print()的输出应该为0和0,但是实际输出为1,0



    在VS2010中,对形参的调用是从右到左调用,并压入栈中,也就是先将i++压入栈底,然后将i压入栈,这样i的值由0变为了1.

    展开全文
  • /* * 用于向链表插入数据,这里可以看到我们使用了一个二级指针的形参用来保存传入的节点指针, * 与一个用来保存插入值的形参。 */ void printList(Node *phead); //用于打印链表的函数 接下来是包含头文件中各...

    a85c3459ec1f8acc915f1fb0a8386554.png

    ——借由实现一个简单的链表效果讲解一下二级指针,希望这篇文章能给阅读的小伙伴一些帮助吧。

    看文章的小伙伴应该对指针有一个大概的印象吧,这里再来讲解一下。说到指针那就不得不提另外一个东西了,那就是内存。指针存在的很大一部分意义就是为了给我们提供便捷的操纵内存的工具。对于我们来说,内存中最重要的东西就是“每个位置独一无二的地址标识符以及内存中每个位置所存的值”。下面看一下内存模型图:

    81ef8ce0ff9e6b09654dbea314a10b92.png

    而对于指针来说就是保存图中的那个内存地址编号,并通过*操作符来操纵内存中的数据。这里再补充一点:在我们的高级语言之中,我们平时使用的变量也是一种访问内存的方式。毕竟如果每次修改数据都要记住那些地址未免有点蠢。那么 我们平时声明变量像

    int a=1,b=2,c=44,d=12345,e=1111111;

    就可以把上图变为

    273c9a8162a4ad7a6bfe87e3bc7571ed.png

    此时如果我们在之前的基础上再声明一个指针的话

    int a=1,b=2,c=44,d=12345,e=1111111;
    int *p;//声明一个指向int的指针
    p = &a;//a的地址赋值给p

    在我们的模型图中就是:

    e137cfdaf3e064628c927c109333cc94.png

    这里要明确的一件事是我们的指针p它本身也是一个变量,占用4个字节。既然我们现在知道a所代表的内存地址编号,那么我们便可以通过*号来操纵里面的数据啦!补充了这些,那么接下来就步入我们今天的正题啦——二级指针。

    首先我们先看下面一段代码

    int a = 3;
    int *p = &a;
    int **q = &p;

    这里的p很容易理解就是一个指向int型的指针,那我们来分析一下这个q,在这里我们一定要明确一件事q它本质上还是一个指针,只不过它保存的是指针的地址,我们来看图。

    60d69dcbdd190930413bf008828402e3.png

    这下是不是一目了然多了呀!嘿嘿,既然它也是一个指针那么我们就按照分析指针的方法来分析它好了。

    int a = 3;
    int b = 5;
    int *p = &a;
    /*
    *  对于一级指针来说,首先*p告诉编译器这是一个指针变量,
    *  其次int则告诉编译器这个指针所指向的内容是int型的。
    */
    int **q = &p;
    /*
    *  二级指针同理,首先*q告诉编译器这是一个指针变量,
    *  其次int*告诉编译器这个指针所指向的内容是一个int型的指针。
    *  其实这个声明我们可以把他拆开来成(int *)与(*p)理解,前面是保存的类型后面是变量的类型。
    *  多级指针同理。
    */
    
    //接下我们来对指针q进行操作来验证我们的想法
    
    printf("%dn",*p);//此时对p进行解引用操作,打印a的值3。
    
    *q = &b;//结合上图,我们对q进行解引操作得到的是p的内存的地址,也就是说此时的赋值操作是对p进行的。
    
    printf("%dn",*p);//此时我们再对p进行解应操作,打印的值变为了b的值5。验证了我们的想法。
    
    **q = 10;//结合上面的分析你肯定知道这步操作的实际操作对象了吧,没错就是b。
    
    printf("%dn",b);//打印输出10

    看过上面介绍的一些内容,我想你已经对二级指针有了个大概的认识吧,其实在实际操作中我们一般都只涉及到一级和二级指针。不过,你学会了二级指针,其他高级指针也是一样分析的,只是多了几个*而已。那么接下来我们就来看一个实际中的例子吧。利用二级指针来实现一个链表,顺带再介绍一下C语言中的接口概念。

    首先我们创建一个Link.h包含我们对链表的一些宏定义与对用户暴露出来的接口。

    #define LINK_TYPE int//这里我们设定节点内保存的值类型为int
    #define FALSE 0
    #define TRUE 1
    /*
    * 这里先定义一个结构体,包含一个值类型,和一个指向下一个节点的指针类型。
    */
    typedef struct LINK_NODE {
    	LINK_TYPE value;
    	struct LINK_NODE *next;
    } Node;
    /*
    * 暴露给用户的函数接口。
    */
    
    int insertList(Node **phead, LINK_TYPE);
    /*
    *  用于向链表插入数据,这里可以看到我们使用了一个二级指针的形参用来保存传入的节点指针,
    *  与一个用来保存插入值的形参。
    */
    void printList(Node *phead);
    //用于打印链表的函数

    接下来是包含头文件中各函数的具体实现的Link.c文件。

    #include "Link.h"
    //因为要使用头文件中声明的Node结构体所以这里把头文件包含进来
    //这里解释一下include后面跟<>与""的区别,<>一般是使用标准库的头文件,而""一般是你自已定义的头文件。
    #include<stdio.h>
    #include<stdlib.h>
    
    /*
    *  这里我定义了一个初始化Node节点的函数也是为指针开辟一块堆内存空间,并且将节点内的next指针赋值为空。
    *  并将这个指针返回出来。
    *  因为这个函数我不打算对用户暴露,所以可以看到我在头文件中并没有声明这个函数,也即是用户无法直接使用。
    */
    Node* init(Node *pnode) {
    	pnode = (Node*)malloc(sizeof(Node));
    	pnode->next = NULL;
    	return pnode;
    }
    
    int insertList(Node **phead, LINK_TYPE value) {
    	if (*phead == NULL) { 
            //现在我们对这个二级指针进行解引操作,按照上面所讲的内容,这里实际操作的phead内保存的那个指针
            //也就是我们在主函数中声明的那个指针。
    		*phead = init(*phead);//如果该指针为空那么我们就对他进行赋值初始化操作。
    	}
    	Node *next = NULL; //声明一个新的指针来保存我们将要插入的那个节点。
    	next = init(next);
    	if (next == NULL)
    		return FALSE; //如果开辟失败那么返回一个FALSE。
    	next->value = value; //将要插入的值赋值给新节点的value属性。
            /*
            *  接下来的操作就很重要了,(*phead)我们在这里就可以理解为传入的指针对象
            *  如果该指针所指的结构体的next指针为空的话,也是说phead现在所指的是该该链表最后一个元素。
            *  那么就结束循环,将我们新创建的节点接到链表最后一个元素后面。
            *  如果为空的话,那么让phead指针指向它后面一个节点,以此类推,知道phead指向最后一个节点。
            */
    	while ((*phead)->next)
    	{
    		*phead = (*phead)->next;
    	}
    	(*phead)->next = next;
    	return TRUE;
    }
    /*
    *  把链表中的元素打印出来。
    */
    void printList(Node *phead) {
    	while (phead != NULL)
    	{
    		printf("%dn", phead->value);
    		phead = phead->next;
    	}
    }

    最后则是我们的main.c文件

    #include"Link.h"
    #include<stdio.h>
    
    int main() {
    	Node *pList = NULL;
    
    	insertList(&pList, 1);
    	insertList(&pList, 2);
    	printList(pList);
    
    	getchar();
    	return 0;
    }

    d27369fd5e4dd502c98746de87a60e52.png

    可以看到结果正常输出。

    这里主要是通过这个例子让小伙伴们对二级指针有个更好的了解,如果文章中有什么错误之处也请各位小伙伴们指出来啦!嘿嘿,感谢你看完了这篇文章哦,希望能对你有帮助。我们下期见。哈哈,See you!

    展开全文
  • 该函数无返回值,唯一的形参p给出的地址只能由malloc()和calloc()申请内存时返回的地址, 该函数执行后,将以前分配的指针p指向的内存返还给系统,以便系统重新分配 为什么要用free释放内存 (在程序运行期间,用动态...

    d8886d0e77feb83d55834d4b1683f451.png

    线性结构:有且只有一个根节点,且每个节点最多有一个直接前驱和一个直接后继的非空数据结构

    非线性结构:不满足线性结构的数据结构

    链表(单向链表的建立、删除、插入、打印)

    1、链表一般分为: 

      单向链表

        双向链表

        环形链表

    2、基本概念

    链表实际上是线性表的链式存储结构,与数组不同的是,它是用一组任意的存储单元来存储线性表中的数据,存储单元不一定是连续的,

    且链表的长度不是固定的,链表数据的这一特点使其可以非常的方便地实现节点的插入和删除操作

    链表的每个元素称为一个节点,每个节点都可以存储在内存中的不同的位置,为了表示每个元素与后继元素的逻辑关系,以便构成“一个节点链着一个节点”的链式存储结构,

    除了存储元素本身的信息外,还要存储其直接后继信息,因此,每个节点都包含两个部分,第一部分称为链表的数据区域,用于存储元素本身的数据信息,这里用data表示,

    它不局限于一个成员数据,也可是多个成员数据,第二部分是一个结构体指针,称为链表的指针域,用于存储其直接后继的节点信息,这里用next表示,

    next的值实际上就是下一个节点的地址,当前节点为末节点时,next的值设为空指针

    1 struct link2 {3     int data;4     struct link *next;5 };

    像上面这种只包含一个指针域、由n个节点链接形成的链表,就称为线型链表或者单向链表,链表只能顺序访问,不能随机访问,链表这种存储方式最大缺点就是容易出现断链

    一旦链表中某个节点的指针域数据丢失,那么意味着将无法找到下一个节点,该节点后面的数据将全部丢失

    3、链表与数组比较

    数组(包括结构体数组)的实质是一种线性表的顺序表示方式,它的优点是使用直观,便于快速、随机地存取线性表中的任一元素,但缺点是对其进行 插入和删除操作时需要移动大量的数组元素,同时由于数组属于静态内存分配,定义数组时必须指定数组的长度,程序一旦运行,其长度就不能再改变,实际使用个数不能超过数组元素最大长度的限制,否则就会发生下标越界的错误,低于最大长度时又会造成系统资源的浪费,因此空间效率差

    4、单向链表的建立

     1 #include  2 #include  3  4 struct link *AppendNode (struct link *head); 5 void DisplyNode (struct link *head); 6 void DeletMemory (struct link *head); 7  8 struct link 9 {10     int data;11     struct link *next;12 };13 14 int main(void)15 {16     int i = 0;17     char c;18     struct link *head = NULL;    //链表头指针19     printf("Do you want to append a new node(Y/N)?");20     scanf_s(" %c", &c);21     while (c == 'Y' || c == 'y')22     {23         head = AppendNode(head);//向head为头指针的链表末尾添加节点24         DisplyNode(head);        //显示当前链表中的各节点的信息25         printf("Do your want to append a new node(Y/N)");26         scanf_s(" %c", &c);27         i++;28     }29     printf("%d new nodes have been apended", i);30     DeletMemory(head);    //释放所有动态分配的内存31 32     return 0;33 }34 /* 函数功能:新建一个节点并添加到链表末尾,返回添加节点后的链表的头指针 */35 struct link *AppendNode(struct link *head)36 {37     struct link *p = NULL, *pr = head;38     int data;39     p = (struct link *)malloc(sizeof(struct link));//让p指向新建的节点40     if (p == NULL)        //若新建节点申请内存失败,则退出程序41     {42         printf("No enough memory to allocate\n");43         exit(0);44     }45     if (head == NULL)    //若原链表为空表46     {47         head = p;        //将新建节点置为头节点48     }49     else                //若原链表为非空,则将新建节点添加到表尾50     {51         while (pr->next != NULL)//若未到表尾,则移动pr直到pr指向表尾52         {53             pr = pr->next;        //让pr指向下一个节点54         }55         pr->next = p;            //让末节点的指针指向新建的节点56     }57     printf("Input node data\n");58     scanf_s("%d", &data); //输入节点数据59     p->data = data;        //将新建节点的数据域赋值为输入的节点数据值60     p->next = NULL;        //将新建的节点置为表尾61     return head;        //返回添加节点后的链表的头指针62 }63 /* 函数的功能:显示链表中所有节点的节点号和该节点中的数据项的内容*/64 void DisplyNode (struct link *head)65 {66     struct link *p = head;67     int j = 1;68     while (p != NULL)  //若不是表尾,则循环打印节点的数值69     {70         printf("%5d%10d\n", j, p->data);//打印第j个节点数据71         p = p->next;  //让p指向下一个节点72         j++;73     }74 }75 //函数的功能:释放head所指向的链表中所有节点占用的内存76 void DeletMemory(struct link *head)77 {78     struct link *p = head, *pr = NULL;79     while (p != NULL)  //若不是表尾,则释放节点占用的内存80     {81         pr = p;        //在pr中保存当前节点的指针82         p = p->next;//让p指向下一个节点83         free(pr);    //释放pr指向的当前节点占用的内存84     }85 }

    上面的代码使用了三个函数AppendNode、DisplyNode、DeletMemory

    struct link *AppendNode (struct link *head);(函数作用:新建一个节点并添加到链表末尾,返回添加节点后的链表的头指针)

    void DisplyNode (struct link *head);(函数功能:显示链表中所有节点的节点号和该节点中的数据项的内容)

    void DeletMemory (struct link *head);(函数功能:释放head所指向的链表中所有节点占用的内存)

    (还使用了malloc函数和free函数)

    5、malloc函数

    作用:用于分配若干字节的内存空间,返回一个指向该内存首地址的指针,若系统不能提供足够的内存单元,函数将返回空指针NULL,函数原型为void *malloc(unsigned int size)

    其中size是表示向系统申请空间的大小,函数调用成功将返回一个指向void的指针(void*指针是ANSIC新标准中增加的一种指针类型,

    具有一般性,通常称为通用指针或者无类型的指针)常用来说明其基类型未知的指针,即声明了一个指针变量,但未指定它可以指向哪一种基类型的数据,

    因此,若要将函数调用的返回值赋予某个指针,则应先根据该指针的基类型,用强转的方法将返回的指针值强转为所需的类型,然后再进行赋值

     1 int *pi; 2 pi = (int *)malloc(4); 

    其中malloc(4)表示申请一个大小为4字节的内存,将malloc(4)返回值的void*类型强转为int*类型后再赋值给int型指针变量pi,即用int型指针变量pi指向这段存储空间的首地址

    若不能确定某种类型所占内存的字节数,则需使用sizeof()计算本系统中该类型所占的内存字节数,然后再用malloc()向系统申请相应字节数的存储空间

    pi = (int *)malloc(sizeof(int));

    6、free函数

    释放向系统动态申请的由指针p指向的内存存储空间,其原型为:Void free(void *p);该函数无返回值,唯一的形参p给出的地址只能由malloc()和calloc()申请内存时返回的地址,

    该函数执行后,将以前分配的指针p指向的内存返还给系统,以便系统重新分配

     为什么要用free释放内存

    (在程序运行期间,用动态内存分配函数来申请的内存都是从堆上分配的,动态内存的生存期有程序员自己来决定,使用非常灵活,但也易出现内存泄漏的问题,

    为了防止内存泄漏的发生,程序员必须及时调用free()释放已不再使用的内存)

    7、单向链表的删除操作

    删除操作就是将一个待删除的节点从链表中断开,不再与链表的其他节点有任何联系

    需考虑四种情况:

      1.若原链表为空表,则无需删除节点,直接退出程序

      2.若找到的待删除节点p是头节点,则将head指向当前节点的下一个节点(p->next),即可删除当前节点

       3.若找到的待删除节点不是头节点,则将前一节点的指针域指向当前节点的下一节点(pr->next = p->next),即可删除当前节点,当待删除节点是末节点时,

    由于p->next值为NULL,因此执行pr->next = p->next后,pr->next的值也变成NULL,从而使pr所指向的节点由倒数第2个节点变成了末节点

      4.若已搜索到表尾(p->next == NULL),仍未找到待删除节点,则显示“未找到”,注意:节点被删除后,只是将它从链表中断开而已,它仍占用着内存,必须释放其所占的内存,否则将出现内存泄漏

    (头结点不是头指针,注意两者区别)

    8、头节点和头指针

    头指针存储的是头节点内存的首地址,头结点的数据域可以存储如链表长度等附加信息,也可以不存储任何信息

        参考链接---头指针和头节点:https://www.cnblogs.com/didi520/p/4165486.html

                      https://blog.csdn.net/qq_37037492/article/details/78453333

                       https://www.cnblogs.com/marsggbo/p/6622962.html

                      https://blog.csdn.net/hunjiancuo5340/article/details/80671298

    2a731101f7fa79824ca27e7d9cfe29ac.png

    (图片出处:https://blog.csdn.net/hunjiancuo5340/article/details/80671298)

    值得注意的是:

      1.无论链表是否为空,头指针均不为空。头指针是链表的必要元素

      2.链表可以没有头节点,但不能没有头指针,头指针是链表的必要元素

       3.记得使用free释放内存

    单向链表的删除操作实现

     1 struct link *DeleteNode (struct link *head, int nodeData) 2 { 3     struct link *p = head, *pr = head; 4  5     if (head == NULL) 6     { 7         printf("Linked table is empty!\n"); 8         return 0; 9     }10     while (nodeData != p->data && p->next != NULL)11     {12         pr = p;            /* pr保存当前节点 */13         p = p->next;    /* p指向当前节点的下一节点 */14     }15     if (nodeData == p->data)16     {17         if (p == head)    /* 如果待删除为头节点 (注意头指针和头结点的区别)*/18         {19             head = p->next;20         }21         else            /* 如果待删除不是头节点 */22         {23             pr->next = p->next;24         }25         free(p);        /* 释放已删除节点的内存 */26     }27     else            /* 未发现节点值为nodeData的节点 */28     {29         printf("This Node has not been found");30     }31 32     return head;33 }

    9、单向链表的插入

    向链表中插入一个新的节点时,首先由新建一个节点,将其指针域赋值为空指针(p->next = NULL),然后在链表中寻找适当的位置执行节点的插入操作,

    此时需要考虑以下四种情况:

        1.若原链表为空,则将新节点p作为头节点,让head指向新节点p(head = p)

          2.若原链表为非空,则按节点值的大小(假设节点值已按升序排序)确定插入新节点的位置,若在头节点前插入新节点,则将新节点的指针域指向原链表的头节点(p->next = head),且让head指向新节点(head =p)

        3.若在链表中间插入新节点,则将新节点的指针域之下一节点(p->next = pr -> next),且让前一节点的指针域指向新节点(pr->next = p)

        4.若在表尾插入新节点,则末节点指针域指向新节点(p->next = p)

    单向链表的插入操作实现

     1 /* 函数功能:向单向链表中插入数据 按升序排列*/ 2 struct link *InsertNode(struct link *head, int nodeData) 3 { 4     struct link *p = head, *pr = head, *temp = NULL; 5  6     p = (struct link *)malloc(sizeof(struct link)); 7     if (p == NULL) 8     { 9         printf("No enough meomory!\n");10         exit(0);11     }12     p->next = NULL;        /* 待插入节点指针域赋值为空指针 */13     p->data = nodeData;14 15     if (head == NULL)    /* 若原链表为空 */16     {17         head = p;        /* 插入节点作头结点 */18     }19     else        /* 原链表不为空 */20     {21         while (pr->data < nodeData && pr->next != NULL)22         {23             temp = pr;        /* 保存当前节点的指针 */24             pr = pr->next;    /* pr指向当前节点的下一节点 */25         }26         if (pr->data >= nodeData)27         {28             if (pr == head)        /* 在头节点前插入新节点 */29             {30                 p->next = head;    /* 新节点指针域指向原链表头结点 */31                 head = p;        /* 头指针指向新节点 */32             }33             else34             {35                 pr = temp;36                 p->next = pr->next;        /* 新节点指针域指向下一节点 */37                 pr->next = p;            /* 让前一节点指针域指向新节点 */38             }39         }40         else        /* 若在表尾插入新节点 */41         {42             pr->next = p;    /* 末节点指针域指向新节点*/43         }44     }45 46     return head;47 }

     (编译器:Microsoft Visual C++ 2010 Express)

    (tips:上面的代码中将头节点中的数据当作第一个元素,大多数情况头节点是不存储数据的(当时没注意···),读者可自行尝试修改代码让头结点不存储数据,头节点的后一个节点作为第一个元素)

    下面附上头结点不存储数据的代码(区别不是很大,就是多用了一个子函数来初始化头结点)

    89c099a2a37887c30ce9b9e4fb3a36d6.png

      1 #include   2 #include   3   4 struct link *AppendNode (struct link *head);  5 void DisplyNode (struct link *head);  6 void DeletMemory (struct link *head);  7 struct link *init (struct link *head);  8   9 struct link 10 { 11     int data; 12     struct link *next; 13 }; 14 int main(void) 15 { 16     int i = 0; 17     char c; 18     struct link *head = NULL;        //链表头指针 19  20     head = init(head);        /* 初始化队列 */ 21     printf("Do you want to append a new node(Y/N)?"); 22     scanf_s(" %c", &c);   //%c前有一个空格 23     while (c == 'Y' || c == 'y') 24     { 25         head = AppendNode(head);//向head为头指针的链表末尾添加节点 26         DisplyNode(head);        //显示当前链表中的各节点的信息 27         printf("Do your want to append a new node(Y/N)"); 28         scanf_s(" %c", &c);        //%c前有一个空格 29         i++; 30     } 31     printf("%d new nodes have been apended", i); 32     DeletMemory(head);    //释放所有动态分配的内存 33  34     return 0; 35 } 36  37 //函数功能:初始化链表,即新建一个头结点(此处头结点不放数据,原则上不放,实际还是可以放数据) 38 struct link *init (struct link *head) 39 { 40     struct link *p = NULL; 41  42     p = (struct link *)malloc(sizeof(struct link)); 43     if (p == NULL) 44     { 45         printf("初始化链表失败\n"); 46         exit(0); 47     } 48     head = p; 49     p->next = NULL; 50  51     return head; 52 } 53  54 //函数功能:新建一个节点并添加到链表末尾,返回添加节点后的链表的头指针 55 struct link *AppendNode(struct link *head) 56 { 57     struct link *p = NULL, *pr = head; 58     int data; 59     p = (struct link *)malloc(sizeof(struct link));//让p指向新建的节点 60     if (p == NULL)        //若新建节点申请内存失败,则退出程序 61     { 62         printf("No enough memory to allocate\n"); 63         exit(0); 64     } 65     if (head->next == NULL)    //若原链表为空表(只有头节点,头节点不存储数据为空表) 66     { 67         printf("Input node data"); 68         scanf_s("%d", &data); 69         head->next = p;        /* 让头结点的指针指向新建节点 */ 70         p->data = data; 71         p->next = NULL;        /* 新建结点置为表尾 */ 72         return head; 73     } 74     else    //若原链表为非空,则将新建节点添加到表尾 75     { 76         while (pr->next != NULL)//若未到表尾,则移动pr直到pr指向表尾 77         { 78             pr = pr->next;    //让pr指向下一个节点 79         } 80         pr->next = p;        //让末节点的指针指向新建的节点 81      82     printf("Input node data"); 83     scanf_s("%d", &data); //输入节点数据 84     p->data = data; //将新建节点的数据域赋值为输入的节点数据值 85     p->next = NULL;//将新建的节点置为表尾 86     return head;  //返回添加节点后的链表的头指针 87     } 88 } 89 //函数的功能:显示链表中所有节点的节点号和该节点中的数据项的内容 90 void DisplyNode (struct link *head) 91 { 92     struct link *p = head; 93     int j = 1; 94  95     p = p->next; 96     while (p != NULL)  //若不是表尾,则循环打印节点的数值 97     { 98         printf("%5d%10d\n", j, p->data);//打印第j个节点数据 99         p = p->next;  //让p指向下一个节点100         j++;101     }102 }103 //函数的功能:释放head所指向的链表中所有节点占用的内存104 void DeletMemory(struct link *head)105 {106     struct link *p = head, *pr = NULL;107     while (p != NULL)  //若不是表尾,则释放节点占用的内存108     {109         pr = p;  //在pr中保存当前节点的指针110         p = p->next;//让p指向下一个节点111         free(pr); //释放pr指向的当前节点占用的内存112     }113 }

    View Code

    展开全文
  • 使用 C/C++ 的二级指针可以让删除结点的代码非常精简,但如果面试官对此不熟悉的话,会看得一头雾水。 那么,如何才能简洁明了地解决单链表问题呢?实际上很多链表题目都是类型化的,都可以归结为链表的遍历,以及...

    98a8ea0bf53d2b4009a9843617c311d8.png

    本期例题:LeetCode 206 - Reverse Linked List(Easy)

    反转一个单链表。示例:
    • 输入: 1->2->3->4->5->NULL
    • 输出: 5->4->3->2->1->NULL

    反转链表这道题是我在阿里的面试中遇到的题目。它本身也是单链表题目中非常典型的一道,不少题目的解法以反转链表为基础。这篇文章将会包含:

    • 链表类题目的注意点
    • 链表遍历的基本框架
    • 本期例题:反转链表的解法
    • 相关题目

    链表类题目的注意点

    在面试中涉及到的链表类题目,一定都是单链表。虽然实际中双向链表使用较多,但单链表更适合作为面试题考察。

    单链表这样一个相对“简陋”的数据结构,实际上就是为了考察面试者指针操作的基本功。很多题目需要修改指针链接,如果操作不当,会造成链表结点的丢失,或者出现错误的回路。

    我们早在 C/C++ 编程课上就学过链表数据结构。你一定对各种链表的变体印象深刻,单链表、双链表、循环链表……但是在面试中,请忘记你记得的各种花哨样式,只使用最简单的单链表操作。面试官很可能不希望看到你的各种“奇技淫巧”:

    • 加入哑结点(即额外的链表头结点)可以简化插入操作,但面试官通常会要求你不要创建额外的链表结点,哑结点显然也属于额外的结点。
    • 使用 C/C++ 的二级指针可以让删除结点的代码非常精简,但如果面试官对此不熟悉的话,会看得一头雾水。

    那么,如何才能简洁明了地解决单链表问题呢?实际上很多链表题目都是类型化的,都可以归结为链表的遍历,以及在遍历中做插入和删除操作。我们可以使用链表遍历的框架来解题。

    链表遍历的基本框架

    单链表操作的本质难度在哪里?相比于双向链表,单链表缺少了指向前一个结点的指针,所以在删除结点时,还需要持有前一个结点的指针。这让遍历过程变得麻烦了许多。

    比较容易想到的方法是将遍历的指针指向“前一个结点”,删除结点时使用 p.next = p.next.next。但这个方法会带来一些心智负担:

    • 每次要查看的结点是 p.next,也就是下一个结点,别扭
    • 循环终止条件不是 p == null 而是 p.next == null,容易出错

    fadbc1af05e3cf649110a02ad3fa6afe.png

    实际上,这就是单链表操作的复杂性所在。我们前面也否定了使用二级指针这样的高级技巧来简化操作的方法,那么,有没有更简单明了的遍历方式呢?答案是有的。这里隆重推荐我一直在使用的链表遍历框架

    当删除链表结点时,既需要访问当前结点,也需要访问前一个结点。既然这样,我们不妨使用两个指针来遍历链表,curr 指针指向当前结点,prev 指针指向前一个结点。这样两个指针的语义明确,也让你写出的代码更易理解。

    0450aab852a64c5c548a7bc238ddaf98.png

    用代码写出来,链表遍历的框架是这样的:

    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        // 进行操作,prev 表示前一个结点,curr 表示当前结点
        if (prev == null) {
            // curr 是头结点时的操作
        } else {
            // curr 不是头结点时的操作
        }
        prev = curr;
        curr = curr.next;
    }

    在遍历的过程中,需要一直维护 prevcurr 的前一个结点。curr 是循环中的主指针,整个循环的起始和终止条件都是围绕 curr 进行的。prev 指针作为辅助指针,实际上就是记录 curr 的上一个值。

    在每一轮遍历中,可以根据需要决定是否使用 prev 指针。注意 prev 可能为 null(此时 curr是头结点),在使用前需要先进行判断。

    20759fd4fd8e1e93ed76b46124df4775.png

    76b69ee5f5e08773d8e68c68f41a10b1.png

    下面,我们看一看如何用这个链表遍历的框架来解决本期的例题:反转链表。

    本期例题:反转链表的解法

    反转链表的题目会有一个隐藏的要求:不能创建新的链表结点,只是在原有结点上修改链表指针。这样的原地操作会比生成一个新的链表要难很多。

    9ef047d822d57bf7ef5604545f156293.png

    Step 1 套用框架

    这道题实际上就是一个典型的链表的遍历-处理的操作,于是我们套用使用上面所讲的链表遍历框架。要反转链表,实际上就是要反转所有相邻结点之间的指针。那么,整体的代码框架应该是:

    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        // 反转 prev 和 curr 之间的指针
        prev = curr;
        curr = curr.next;
    }

    可以看到,遍历的框架已经将整体的思路架构了出来,我们知道按照如此的方式一定能遍历到所有相邻的结点对,也知道遍历结束后循环一定能正常退出。接下来只需要关注每一步如何反转结点之间的指针即可。

    Step 2 写好单步操作

    单步操作是“反转 prevcurr 之间的指针”。这里涉及到指针指向的改变,需要小心指针丢失的问题。在思考的时候,要考虑到和前一步、后一步的链接。

    假设现在遍历到了链表中部的某个结点。链表应该会分成两个部分: prev 指针之前的一半链表已经进行了反转;curr 之后的一半链表还是原先的顺序。这次循环将让 curr 的指针改为指向 prev,就将当前结点从后一半链表放进了前一半链表。

    cc484069a2f89ab393ca5db18af0ca46.png

    bfa0008906d428ce9a7e3fbebc90cd96.png

    c0e58bb64a7590f0f8fc3ac5335d6806.png

    而头结点的特殊情况是,全部链表都还未进行反转,即前一半链表为空。显然 curr.next 应当置为 null。

    81eccd41466eee2c635b89bba8e931a1.png

    27208f51279f8dc6936352f8bffd4f2a.png

    将单步操作放入代码框架,我们就得到了一份初版的解题代码:

    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        if (prev == null) {
            curr.next = null;
        } else {
            curr.next = prev;
        }
        prev = curr;
        curr = curr.next;
    }

    Step 3 细节处理

    上面的代码已经基本上比较完整了,但是还存在着明显的错误,那就是存在指针丢失的问题。

    我们使用 curr.next = prev 来反转指针,但这会覆盖掉 curr.next 本来存储的值。丢掉这个指针之后,链表的后续结点就访问不到了!

    71e57858a993becc118c85efbe40bed8.png

    要解决指针丢失的问题也很简单,使用一个临时指针保存 curr 的下一个结点即可。如下图所示:

    f66e23c329d58e67ab1a6ac9ac8ae06e.png

    不过这样一来,我们遍历框架中更新指针的操作也要随之进行微调。框架本来就不是一成不变的,需要根据实际题目灵活调整。

    根据以上两点的细节处理,我们修改得到完整版的代码:

    ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode cnext = curr.next;
            if (prev == null) {
                curr.next = null;
            } else {
                curr.next = prev;
            }
            prev = curr;
            curr = cnext;
        }
        return prev;
    }

    上述代码中,if 的两个分支实际上可以优化合并,这里为了清晰起见仍然保留分支。

    总结

    总结起来,我们解决这一类单链表问题时,遵循的步骤是:

    1. 判断问题是否为链表遍历-修改,套用链表遍历框架
    2. 思考单步操作,将代码加入遍历框架
    3. 检查指针丢失等细节

    有很多更复杂的链表题目都以反转链表为基础。下面列出了 LeetCode 上几道相关的题目:

    • 203 - Remove Linked List Elements 是一道直白的链表删除题目
    • 445 - Add Two Numbers II 以反转链表为基础解题
    • 92 - Reverse Linked List II 反转部分链表

    希望本文的讲解能让你在写链表类题目时更得心应手。

    展开全文
  • 将关键字 const 加在形参表之后,就可以将成员函数声明为常量: double avg_price() const;   const 成员不能改变其所操作的对象的数据成员。const 必须同时出现在声 明和定义中,若只出现在其中一处,就会...
  • 最近在学习c++ 2.0(11/14/17)新特性,刚写上几行代码,就遇到编译器报错:C2563 在形参表中不匹配, 代码如下: // C11TEST.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <...
  • C++ 带默认值的形参

    2017-04-10 13:05:38
    1. 有默认值的形参应处于形参表的右部 2. 对参数默认值的指定只在函数的声明处有意义  在定义时指定默认值是没有意义的,除非该函数定义同时也提供函数声明的作用 3. 在不同源文件中,对同一个函数的声明可以对...
  • C++链表

    2018-12-03 15:18:00
    c++链表与C语言链表的主要区别: 在定义链表节点(结构体)之后,用class将其封装(封装相关函数以及一个结点指针,用来指向整个链表)。进而相关函数可通过使用该结点指针而减少函数形参的使用(递归函数除外)。 ...
  • C/C++优先级

    2012-12-06 15:49:40
    优先级 运算符 名称或含义 使用形式 结合方向 说明 1 [] ...(表达式)/函数名(形参表)   . 成员选择(对象) 对象.成员名
  • [C++] 将成员函数作为函数形参

    千次阅读 2014-02-21 11:29:09
    函数以及函数对象都可以作为参数来增加其它函数的功能,并且通常作为STL算法的第二个版本的最后一个参数。成员函数是否可以作为形参呢?如果可以,形参表如何写呢?
  • c++顺序基本功能

    2016-05-14 11:04:00
    头文件 #define LIST_MAX_SIZE 5#define LISTINCREMENT 2#include<assert.h>...中是模板参数在用模板类的时候必须给出,可以不止一个且不能为空,class和typename的作用是一样的 其后的形参可以声明...
  • “带有默认值的形参必须从参数的最右侧开始连续的不间断的列出”。*2. “如果分别给出了函数声明和函数定义,形参默认值只能出现在函数声明中”。*/#include <iostream>using namespace std;#define MACRO ...
  • 发牢骚之前先温习一下重载函数的定义:出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数。 根据教条主义,@.@~~哦,说错了,根据教材定义,如果有两个名字相同的函数,二者形参名字和...
  • C++简洁的链表创建

    2019-02-26 20:45:00
    C++创建链表和链表基本操作 简单的链表实现,包括创建打印,记录下来,防止以后忘了,往往基础的东西最重要又最易被人忽视。 https://mintlucas.github.io/2019/02/26/C-简洁的链表创建/ 注意形参传递,在create...
  • C++虚函数解析

    2019-10-05 22:20:08
    定义 虚函数必须是基类的非静态成员函数,其访问权限可以是protected或public,在基类的类定义中定义虚函数的一般形式: virtual 函数返回值类型 虚函数名(形参表) { 函数体 }编辑本段作用 虚函数的作用是实现动态...
  • 今天我来用C++封装一下以前写过的顺序以及双循环链表。 一。顺序: #include&lt;iostream&gt; using namespace std; //定义一个内置类型int。 typedef int DataType; //写一个动态顺序: class ...
  • C/C++运算符优先级

    2015-08-17 11:47:17
    优先级 运算符 名称或含义 使用形式 结合方向 说明 1 [] ...(表达式)/函数名(形参表)   . 成员选择(对象) 对象.成员名
  • <返回类型><函数名>(参数) { 函数体 } template T fuc(T x, int y) { T x; //…… } 如果主调函数中有以下语句: double d; int a; fuc(d,a); 则系统将用实参d的数据类型double去代替函数模板中的T生成函数: ...
  • 作者:鲸90830链接:...const类型名 对象名[(构造实参列)];注意:常对象必须要有初值(因为以后不能改值了)。2) 限定作用:定义为const的对象的所有数据成员的值都不能被修改。凡出现调用非cons...
  • 数组形参

    2016-05-27 23:24:17
    C++不存在所谓的数组形参,因为在数组做为参数时,只传入了着地址void average(int ary[12])//形参ary 是一个int *类型在数组做为形参时退化成了指向其首地址的指针,所以会丢失边界,因此声明时最好把它省略掉,而用俩个...
  • (给CPP开发者加星标,提升C/C++技能)来源:鲸90830https://www.cnblogs.com/whale90830/p/10542361.html1) const 对象的一般形式类型名const对象名[(构造实参列)];const类型名 对象名[(构造实参列)];注意:常...
  • 使用C++模板: Template <class或者也可以用typenameT> template <class T>//template是标识符 ...说明: template是一个声明模板的关键字,表示...类型 形参表>可以包含基本数据类型可以包含类类...
  • 笔者刚系统的重学了c++的语法,看到构造函数和初始化表这块,发现这块语法有点复杂且很杂,怕以后忘记,于是写下此篇,以后...类名(形参表) {函数体} }; 1)构造函数名字和类名相同,没有返回类型 ...
  • ),在基类的类定义中定义虚函数的一般形式: virtual 函数返回值类型 虚函数名(形参表) { 函数体 } 虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定...
  • C++虚函数与虚函数

    2017-10-07 21:59:21
    虚函数的定义:  虚函数必须是类的非静态成员函数(且非构造函数),其访问权限是public(可以定义为private or proteceted, 但是对于多态来说,没有意义。... virtual 函数返回值类型 虚函数名(形参表
  • 虚函数的定义:  虚函数必须是类的非静态成员函数(且非... virtual 函数返回值类型 虚函数名(形参表)  { 函数体 }  虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚
  • template 形参名1, class 形参名2, class 形参名n> 返回类型 函数名(参数列表) { ... } 模板形参的定义既可以使⽤class,也可以使⽤typename,含义是相同的。 注:模板未实例化时,代码内部的语法错误检测不

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,126
精华内容 450
关键字:

c++形参表

c++ 订阅