C语言中,我们在使用数组时,会需要对数组进行插入和删除的操作,这时就需要移动大量的数组元素,但在C语言中,数组属于静态内存分配,数组在定义时就必须指定数组的长度或者初始化。这样程序一旦运行,数组的长度就不能再改变,若想改变,就只能修改源代码。实际使用中数组元素的个数也不能超过数组元素的最大长度,否则就会发生下标越界的错误(这是新手在初学C语言时肯定会遇到的问题,相信老师也会反复强调!!!但这种问题肯定会遇到,找半天找不到错误在哪,怪我咯???)。另外如果数组元素的使用低于最大长度,又会造成系统资源的浪费,会导致降低空间使用效率。
那有没有更合理的使用系统资源的方法呢?比如,但需要添加一个元素时,程序就可以自动的申请内存空间并添加新的元素,而当需要减少一个元素时,程序又可以自动地释放该元素占用的内存空间。我们聪明的祖先早就意识到了这个问题,于是就有了动态数据结构--链表结构(Linked list)。它主要是利用动态内存分配、使用结构体并配合指针来实现的一种数据结构。
链表有三种不同的类型:单向链表,双向链表以及循环链表。今天我们只对单向链表做详细的说明。
链表中最简单的一种是单向链表,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值(NULL)。
单向链表的存储结构
/*单向链表的代码表示*/ struct node { int data; // 数据域 struct node *next; // 指向下一个节点的指针 };
接下来进入正题,分别详细讲一下单向链表的插入、删除节点以及插入节点操作。
- 单向链表的建立
建立一个单向链表,我们可以使用向链表中添加节点的方式。首先,要为新建的节点动态申请内存空间,让指针变量指向这个新建节点,然后将新建节点添加到链表中,这时,我们需要考虑以下两种情况:
(1)若原链表为空,则将新建节点设置为头节点
(2)若原链表为非空,则将新建节点添加到表尾
具体代码如下:
#include "stdio.h" #include "stdlib.h" struct link *AppendNode(struct link *head); void DisplayNode(struct link *head); void DeleteMemory(struct link *head); struct link { int data; struct link *next; }; int main(int argc, char const *argv[]) { int i = 0; char c; struct link *head = NULL; //链表头指针 printf("Append a new node(y/n)?"); scanf("%c", &c); while(c == 'Y' || c == 'y'){ head = AppendNode(head); //向head为头指针的链表末尾添加节点 DisplayNode(head); printf("Append a new node(y/n)?"); scanf(" %c", &c); i++; } printf("%d new nodes have been appened!\n"); DeleteMemory(head); return 0; } // 新建一个节点并添加到链表末尾,返回添加节点后的链表的头指针 struct link *AppendNode(struct link *head){ struct link *p = NULL, *pr = head; int data; p = (struct link *)malloc(sizeof(struct link)); // 通过malloc函数动态的申请内存,注意结构体占用内存的大小只能用sizeof()获取 if (p == NULL){ printf("No enough memory to allocate!\n"); exit(0); } if (head == NULL){ //原链表为空 head = p; }else{ // 原链表为非空,则将新建节点添加到表尾 while(pr->next != NULL){ // 如果pr指向的不是表尾,则移动pr直到指向表尾 pr = pr->next; } pr->next = p; } printf("Input node data:"); scanf("%d",&data); // 输入新建节点的数据 p->data = data; p->next = NULL; // 将新建节点置为表尾 return head; } // 显示链表中所有的节点 void DisplayNode(struct link *head){ struct link *p = head; int j = 1; while(p != NULL){ // p不在表尾,循环打印节点的值 printf("%5d%10d\n", j, p->data); p = p->next; j++; } } //释放head指向的链表中所有节点占用的内存 void DeleteMemory(struct link *head){ struct link *p = head, *pr = NULL; while(p != NULL){ // p不在表尾,释放节点占用的内存 pr = p; // 在pr中保存当前节点的指针 p = p->next; // p指向下一个节点 free(pr); // 释放pr指向的当前节点占用的内存 } }
代码运行结果如下:
2. 单向链表的删除操作
链表的删除操作就是将待删除的节点从链表中断开,那么待删除节点的上一个节点就成为尾节点。在删除节点时,我们要考虑一下4种情况:
(1)若原链表为空,则不执行任何操作,直接退出程序
(2)若待删除节点是头节点,则将head指向当前节点的下一个节点,再删除当前节点
(3)若待删除节点不是头节点,则将前一节点的指针域指向当前节点的下一节点,即可删除当前节点。当待删除节点是尾节点时,由于p->next=NULL,因此执行pr->next = p->next后,pr->next的值也变为了NULL,从而使pr所指向的节点由倒数第二个节点变成尾节点。
(4)若待删除的节点不存在,则退出程序
注意:节点被删除后,只表示将它从链表中断开而已,它仍占用着内存,必须要释放这个内存,否则会出现内存泄漏。
删除一个节点的代码如下:
// 从head指向的链表中删除一个节点,返回删除节点后的链表的头指针 struct link *DeleteNode(struct link *head, int nodeData) { struct link *p = head, *pr = head; if (head == NULL) // 若原链表为空,则退出程序 { printf("Linked Table is empty!\n"); return head; } while(nodeData != p->data && p->next != NULL) // 未找到待删除节点,且没有到表尾 { pr = p; // 在pr中保存当前节点的指针 p = p->next; // p指向当前节点的下一节点 } if (nodeData == p->data) // 若当前节点就是待删除节点 { if (p == head) // 若待删除节点为头节点 { head = p->next; // 将头指针指向待删除节点的下一节点 } else // 若待删除节点不是头节点 { pr->next = p->next; // 让前一节点的指针指向待删除节点的下一节点 } free(p); // 释放为已删除节点分配的内存 } else // 没有找到节点值为nodeData的节点 { printf("This Node has not been found!\n"); } return head; // 返回删除节点后的链表头指针 }
3. 单链表的插入操作
向一个链表中插入一个新节点时,首先要新建一个节点,并将新建节点的指针域初始化为空NULL,然后在链表中寻找适当的位置执行节点插入操作,此时需要考虑下面4种情况:
(1)若原链表为空,则将新建节点p作为头节点,让head指向新节点p
(2)若原链表为非空,折按新建节点的值的大小(假设原链表已按节点值升序排列)确定插入新节点的位置。若在头结点前插入新节点,则将新节点的指针域指向原链表的头结点,并且让head指向新节点p
(3)若在原链表中间插入新节点,则将新节点p的指针域指向下一节点,并且让前一节点的指针域指向新建节点p
(4)若在表尾插入新节点,则将尾节点的指针域指向新节点p
具体代码如下:
// 在已按升序排列的链表中插入一个新节点,返回插入节点后的链表头指针 struct link *InsertNode(struct link *head, int nodeData) { struct link *pr = head, *p = head, *temp = NULL; p = (struct link *)malloc(sizeof(struct link)); // 给新建节点动态申请内存空间 if (p == NULL) // 若动态申请内存失败,则退出程序 { printf("No enough memory!\n"); exit(0); } p->next = NULL; // 将新建节点的指针域初始化为空 p->data = nodeData; // 将新建节点的数据域初始化为nodeData if (head == NULL) // 若原链表为空 { head = p; // 将新建节点作为头节点 } else // 若原链表为非空 { // 未找到新建节点的插入位置并且没有到尾节点 while(pr->data < nodeData && pr->next != NULL) { temp = pr; // 在temp中保存当前节点pr的指针 pr = pr->next; // pr跳到下一节点 } // 找到需要插入的位置 if (pr->data >= nodeData) { if (pr == head) // 若当前节点为头节点,则将新建节点插入头节点之前 { p->next = head; // 将新节点的指针域指向原链表的头节点 head = p; // head指向新建节点 } else // 在原链表中插入新节点 { pr = temp; p->next = pr->next; // 新建节点的指针域指向当前节点的下一节点 pr-next = p; // 当前节点的下一节点指向新节点 } } else // 新建节点的值为最大值,插在原链表尾部 { pr->next = p; // 原链表的尾节点指向新节点 } } return head; // 返回插入新节点后的链表的头指针 }
到此,对于单链表的操作已经介绍完了。通过写这篇博客,我也深刻学习了单链表的结构和一些主要操作,在写作的过程中也翻阅了很多资料,让我意识到数据结构的重要性,不懂数据结构,你永远只能当一个码农。
共勉!
哦,忘了今天是圣诞节,也是那个卖火柴的小女孩冻死170周年 :(
----最后编辑于2016/12/25----
原文地址:http://blog.itisfun.xyz/archives/360