想必大多数人和我一样,刚开始学数据结构中的单链表还是蛮吃力的,特别是后面的双链表操作更是如此。还有就是在实践代码操作时,你又会感到无从下手,没有思路。造成这样的缘由,还是没有完全把链表吃透,今天刚好看书又看到了这里,总结一下,分享给大家,希望对大家有帮助。
一、链表引入的缘由:
在一开始,不知大家用了这么久的数组,你有没有发现数组存在两个明显的缺陷?1)一个是数组中所有元素的类型必须一致;2)第二个是数组的元素个数必须事先制定并且一旦指定之后不能更改。于是乎为了解决数组的缺陷,先辈们发明的一些特殊方法来解决:a、数组的第一个缺陷靠结构体去解决。结构体允许其中的元素的类型不相同,因此解决了数组的第一个缺陷。所以说结构体是因为数组不能解决某些问题所以才发明的;b、我们希望数组的大小能够实时扩展。譬如我刚开始定了一个元素个数是10,后来程序运行时觉得不够因此动态扩展为20.普通的数组显然不行,我们可以对数组进行封装以达到这种目的;我们还可以使用一个新的数据结构来解决,这个新的数据结构就是链表(几乎可以这样理解:链表就是一个元素个数可以实时变大/变小的数组)。
二、什么是链表?
顾名思义,链表就是用锁链连接起来的表。这里的表指的是一个一个的节点(一个节点可以比喻成大楼里面的空房子一样用来存放东西的),节点中有一些内存可以用来存储数据(所以叫表,表就是数据表);这里的锁链指的是链接各个表的方法,C语言中用来连接2个表(其实就是2块内存)的方法就是指针。它的特点是:它是由若干个节点组成的(链表的各个节点结构是完全类似的),节点是由有效数据和指针组成的。有效数据区域用来存储信息完成任务的,指针区域用于指向链表的下一个节点从而构成链表。
三、单链表中的一些细节:
1、单链表的构成:
a、链表是由节点组成的,节点中包含:有效数据和指针。
b、定义的struct node只是一个结构体,本身并没有变量生成,也不占用内存。结构体定义相当于为链表节点定义了一个模板,但是还没有一个节点,将来在实际创建链表时需要一个节点时用这个模板来复制一个即可。例如:
1 struct node{2 int data;//有效数据34struct node *pNext;//指向下一个节点的指针56 };//构建一个链表的节点。
2、堆内存的申请和使用:
a、先了解一下什么是堆:堆(heap)是种内存管理方式,它的特点是:就是自由管理(随时申请,灵活,大小块随意)。堆内存是操作系统规划给堆管理器(操作系统中的的一段代码,属于操作系统的内存管理单元),来管理的,然后向使用者(用户进程)提供api(malloc和free)来使用堆内存。
b、为什么要使用堆呢?
需要内存容量比较大的时候,需要反复使用及释放时,需要反复使用及释放很多数据结构(譬如链表)的实现都要使用堆内存;它的特点:容量不限(常规使用的需求容量都能满足),申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。如果程序员申请内存并使用没有释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会申请新的内存块,这就叫吃内存),称为内存泄漏。
c、基本概念:
作用域:起作用的区域,也就是可以工作的范围。
代码块:所谓代码块,就是用{}括起来的一段代码。
数据段:数据段存的是数,像全局变量就是存在数据段的。
代码段:存的是程序代码,一般是只读的。
栈(stack):先进后出。C语言中局部变量就分配在栈中。
这里随便也讲一下什么是栈:
栈是一种数据结构,c语言中使用栈来保存局部变量。栈是被发明出来管理内存的;它的特点:是先进后出;而先进先出,它是队列的特点;栈的特点是入口即出口,另外一个口是堵死的。所以先进去的必须后出来队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。在c 语言中的局部变量是用栈来实现的。我们在c中定义一个局部变量时(int a ),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存地址和我们定义的局部变量名a 给关联起来),对应栈的操作时入栈。
注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作);然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作时出栈。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人去写代码去控制。栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,c语言自动完成。分析一个细节:c语言中,定义局部变量时如果未初始化,则值时随机的为什么?定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定,因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没有清零的),所以说使用栈来实现的局部变量定义时如果不显示初始化,值就是脏的。如果你显示初始化会怎样?
c语言是通过一个小手段来实现局部变量的初始化的。比如 int a=10;相当于 int a ;
a=10;
栈的缺点:首先,栈是有大小的。所以栈内存大小不好设置,如果太小怕溢出,太大跑浪费内存;所以栈的溢出危害很大,一定避免。所以我们在c语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时int a[10000])
使用递归来解决问题时一定要注意递归收敛.
d、注意:链表的内存要求比较灵活,不能用栈,也不能用data数据段。只能用堆内存。
使用堆内存来创建一个链表节点的步骤:1、申请堆内存,大小为一个节点的大小(检查申请结果是否正确);2、清理申请到的堆内存;3、把申请到的堆内存当作一个新节点;4、填充你哦个新节点的有效数据和指针区域。
实例:
1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 int main(void) 5{ 6 //创建一个链表节点 7 struct node *p=(struct node*)malloc(sizeof(struct node)); 8 if(NULL==p) 9 {10 printf("malloc error.n");11 }12 //清理申请到的堆内存13 bzero(p,sizeof(struct node));14 //填充节点15 p->data=1;16 p->pNext =NULL;//将来要指向下一个节点的首地址;实际操作时将下 一 个节点malloc返回的指针赋值给这个17}
四、实例演示:
1、单链表的实现:
1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 // 构建一个链表的节点 5 struct node 6 { 7 int data; // 有效数据 8struct node *pNext; // 指向下一个节点的指针 9 };10 int main(void)11 {12// 定义头指针13struct node *pHeader = NULL;14//15// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来16// 创建一个链表节点17struct node *p = (struct node *)malloc(sizeof(struct node));18if (NULL == p)19{20 printf("malloc error.n");21 return -1;22}23// 清理申请到的堆内存24bzero(p, sizeof(struct node));25// 填充节点26p->data = 1;27p->pNext = NULL; // 将来要指向下一个节点的首地址28 // 实际操作时将下一个节点malloc返回的指针赋值给这个2930pHeader = p; // 将本节点和它前面的头指针关联起来 31//33// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来34// 创建一个链表节点35struct node *p1 = (struct node *)malloc(sizeof(struct node));36if (NULL == p1)37{38 printf("malloc error.n");39 return -1;40}41// 清理申请到的堆内存42bzero(p1, sizeof(struct node));43// 填充节点44p1->data = 2;45p1->pNext = NULL; // 将来要指向下一个节点的首地址46 // 实际操作时将下一个节点malloc返回的指针赋值给这个474849p->pNext = p1; // 将本节点和它前面的头指针关联起来 505152//5354//5556// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来5758// 创建一个链表节点5960struct node *p2 = (struct node *)malloc(sizeof(struct node));61if (NULL == p2)62{63 printf("malloc error.n");64 return -1;65}66// 清理申请到的堆内存67bzero(p2, sizeof(struct node));68// 填充节点69p2->data = 3;70p1->pNext = p2; // 将来要指向下一个节点的首地址71 // 实际操作时将下一个节点malloc返回的指针赋值给这个 72//73// 至此创建了一个有1个头指针+3个完整节点的链表。7475// 下面是4.9.3节的代码76// 访问链表中的各个节点的有效数据,这个访问必须注意不能使用p、p1、p2,而只能77// 使用pHeader。7879// 访问链表第1个节点的有效数据80printf("node1 data: %d.n", pHeader->data); 81printf("p->data: %d.n", p->data); // pHeader->data等同于p->data8283// 访问链表第2个节点的有效数据84printf("node2 data: %d.n", pHeader->pNext->data); 85printf("p1->data: %d.n", p1->data); 86// pHeader->pNext->data等同于p1->data8788// 访问链表第3个节点的有效数据89printf("node3 data: %d.n", pHeader->pNext->pNext->data); 90printf("p2->data: %d.n", p2->data); 91// pHeader->pNext->pNext->data等同于p2->data9293return 0;94}
编译结果如下:
1 root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc file2.c2 root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out3 node1 data: 1.4 p->data: 1.5 node2 data: 2.6 p1->data: 2.7 node3 data: 3.8 p2->data: 3.
2、在链表末尾添加元素:
思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。代码实例;
1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 // 构建一个链表的节点 5 struct node 6{ 7int data; // 有效数据 8struct node *pNext; // 指向下一个节点的指针 9 };10// 作用:创建一个链表节点11// 返回值:指针,指针指向我们本函数新创建的一个节点的首地址12struct node * create_node(int data)13{14struct node *p = (struct node *)malloc(sizeof(struct node));15if (NULL == p)16{17 printf("malloc error.n");18 return NULL;19}20// 清理申请到的堆内存21bzero(p, sizeof(struct node));22// 填充节点23p->data = data;24p->pNext = NULL; 25return p;26 }27 void insert_tail(struct node *pH, struct node *new)28 {29// 分两步来完成插入30// 第一步,先找到链表中最后一个节点31struct node *p = pH;32while (NULL != p->pNext)33{34 p = p->pNext; 35// 往后走一个节点36}37// 第二步,将新节点插入到最后一个节点尾部38p->pNext = new;39 }40 int main(void)41 {42// 定义头指针43//struct node *pHeader = NULL; 44// 这样直接insert_tail会段错误。45struct node *pHeader = create_node(1);46insert_tail(pHeader, create_node(2));47insert_tail(pHeader, create_node(3));48insert_tail(pHeader, create_node(4));49 /*50pHeader = create_node(1);51 // 将本节点和它前面的头指针关联起来 52pHeader->pNext = create_node(432); 53// 将本节点和它前面的头指针关联起来 5455pHeader->pNext->pNext = create_node(123); 56// 将来要指向下一个节点的首地址5758// 至此创建了一个有1个头指针+3个完整节点的链表。59 */60 // 访问链表中的各个节点的有效数据,这个访问必须注意不能使用p、p1、p2,而只能61 // 使用pHeader。62// 访问链表第1个节点的有效数据63printf("node1 data: %d.n", pHeader->data); 64//printf("p->data: %d.n", p->data); 65 // pHeader->data等同于p->data66// 访问链表第2个节点的有效数据67printf("node2 data: %d.n", pHeader->pNext->data); 68//printf("p1->data: %d.n", p1->data); 69// pHeader->pNext->data等同于p1->data70// 访问链表第3个节点的有效数据71printf("node3 data: %d.n", pHeader->pNext->pNext->data); 72//printf("p2->data: %d.n", p2->data); 73// pHeader->pNext->pNext->data等同于p2->data74return 0;75}
编译结果:
1root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc file3.c2root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out3node1 data: 1.4node2 data: 2.5node3 data: 3.
3、在第一个节点插入元素:
在代码演示之前,先名两个概念:头指针和头节点
a、什么是头指针?
头指针并不是节点,而是一个普通指针,只占4字节。头指针的类型是struct node *类型的,所以它才能指向链表的节点。一个典型的链表的实现就是:头指针指向链表的第1个节点,然后第1个节点中的指针指向下一个节点,然后依次类推一直到最后一个节点。这样就构成了一个链。
b、什么是头节点?
其实它和一般的节点差不多,只不过要注意的是:第一,它紧跟在头指针后面。第二,头节点的数据部分是空的(有时候不是空的,而是存储整个链表的节点数),指针部分指向下一个节点,也就是第一个节点。
1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 // 构建一个链表的节点 5 struct node 6 { 7int data; // 有效数据 8struct node *pNext; // 指向下一个节点的指针 9 };10 // 作用:创建一个链表节点11 // 返回值:指针,指针指向我们本函数新创建的一个节点的首地址12 struct node * create_node(int data)13 {14struct node *p = (struct node *)malloc(sizeof(struct node));15if (NULL == p)16{17 printf("malloc error.n");18 return NULL;19 }20// 清理申请到的堆内存21bzero(p, sizeof(struct node));22// 填充节点23p->data = data;24p->pNext = NULL; 25return p;26 }27 // 思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。2829 // 计算添加了新的节点后总共有多少个节点,然后把这个数写进头节点中。3031void insert_tail(struct node *pH, struct node *new)32 {33int cnt = 0;34// 分两步来完成插入35// 第一步,先找到链表中最后一个节点36struct node *p = pH;37while (NULL != p->pNext)38{39 p = p->pNext; 40 // 往后走一个节点41 cnt++;42}43// 第二步,将新节点插入到最后一个节点尾部44p->pNext = new;45pH->data = cnt + 1;46 }47void insert_head(struct node *pH, struct node *new)48{49// 第1步: 新节点的next指向原来的第一个节点50new->pNext = pH->pNext;51// 第2步: 头节点的next指向新节点的地址52pH->pNext = new;53// 第3步: 头节点中的计数要加154pH->data += 1;55 }56int main(void)57{58// 定义头指针59//struct node *pHeader = NULL; 60 // 这样直接insert_tail会段错误。61struct node *pHeader = create_node(0);62insert_head(pHeader, create_node(1));63insert_tail(pHeader, create_node(2));64insert_head(pHeader, create_node(3));65 /*66pHeader = create_node(1);6768// 将本节点和它前面的头指针关联起来 69pHeader->pNext = create_node(432); 70// 将本节点和它前面的头指针关联起来 71pHeader->pNext->pNext = create_node(123); 72// 将来要指向下一个节点的首地址73// 至此创建了一个有1个头指针+3个完整节点的链表。74 */75// 访问链表中的各个节点的有效数据,这个访问必须注意不能使用 p、p1、p2,而只能76// 使用pHeader。77// 访问链表头节点的有效数据78printf("beader node data: %d.n", pHeader->data); 79// 访问链表第1个节点的有效数据80printf("node1 data: %d.n", pHeader->pNext->data); 81// 访问链表第2个节点的有效数据82printf("node2 data: %d.n", pHeader->pNext->pNext->data); 83// 访问链表第3个节点的有效数据84printf("node3 data: %d.n", pHeader->pNext->pNext->pNext->data); 85return 0;86}
编译结果;
1 root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc file4.c2 root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out3 beader node data: 3.4 node1 data: 3.5 node2 data: 1.6 node3 data: 2.
五、总结:
通过本次链表的学习,让自己对链表的理解更加深了,接下来双链表的使用会在后面更新,欢迎大家来关注!!
全部0条评论
快来发表一下你的评论吧 !