单向链表
基本概念
在基于指针的链式结构中,单向链表是最基本的。
在单向链表中,每个节点都有两个域,一个是用于存放数据元素的域element
,一个用于指向后继节点的指针域link
。
单向链表的第一个节点称为起始节点
,指向起始节点的指针称为头指针
,头指针为NULL的单向链表称为空表
。
例如具有五个节点的单向链表结构如下:
单向链表的节点结构
单向链表的每个节点的结构都具有跟如下定义相似的结构类型;
typedef struct _node
{
T element;
struct _node* link;
}node;
T element
是数据域,可以是一个或多个基本类型的数据,也可以是结构体或者是联合;struct node* link
是指针域,这是一个自引用的结构。
单向链表的操作
构造单向链表节点
用malloc
函数向堆中申请一块node大小的内存,元素element
则采用传参的方式进行填充,如果申请成功则返回节点起始地址,申请失败则返回NULL
。
node* nodeCreate(int element)
{
node* ptr = (node*)malloc(sizeof(node));
if (ptr == NULL)
{
printf("内存分配失败\r\n");
return NULL;
}
printf("创建节点地址:%d\r\n", ptr);
ptr->element = element;
ptr->link = NULL;
return ptr;
}
代码中的node
采用以下类型结构,即将T element;
替换为具体的整形元素。
typedef struct _node
{
int element;
struct _node* link;
}node;
单向链表的插入
单向链表的插入方式按照需求会有好几种方式,例如直接在链表末尾插入,或者按照element
的大小有序进行插入。
单向链表末尾插入
如果不考虑节点中某些数据的有序性,直接在链表的末尾进行插入,这种方式会简单一些,省去了比较的过程。在创建一个节点后,直接从head
开始往后寻找最后那个节点,然后把新建节点的起始地址给末尾节点的link
,若head
为NULL
,则直接把节点的起始地址给head
。
如果是非空链表,查找末尾节点的判定方式是判断
link
是否为NULL
,即if(ptr->link==NULL)
;但是如果是空链表,判断方式则是if(head==NULL)
;这么一分析,似乎插入这一操作还得区别链表是否为空链表;这就有一个边缘情况,能否消除这种边缘情况,将两种情况下的操作统一起来。
node* NodeInsertLast(list* head, node* n)
{
list* ptr = head;
while (*ptr != NULL)
{
ptr = &(*ptr)->link;
}
*ptr = n;
return *head;
}
形参里面的list
为node*
类型,即形参中的head
是指向node
指针类型数据的指针。一般头指针变量的定义是这样的:node* head = NULL
,若形参中的head
类型为node*
还会导致另一个问题,头指针无法以参数的方式传入NodeInsertLast
。
单向链表有序插入
有序插入时不仅会遇到末尾插入面临的两种情况,还会遇到头指针与起始节点之间插入和节点与节点之间插入的情况。
情况更复杂了吗?看起来更复杂了。
如果在链表末尾插入,即当前链表所有节点中的element
元素数值都要比n->element
小的情况,与空表的情况,过程和NodeInsertLast
函数是一致的,只不过多了n->link = *ptr;
这个赋值过程,此时*ptr
值为NULL
。
在p
节点和q
节点之间插入一个节点,既要获得后继节点q
中element
的值,又不能丢失前驱节点p
。第一种方法是定义两个node
指针变量,一个用来指向p
,一个用来指向q
,两个指针同时向后移动,当比较后发现满足条件就在二者之间插入。有没有更简单的方法,像前文消除head
特殊情况一样;而且这里也有边缘情况。
node* NodeInsertOrder(list* head, node* n)
{
list* ptr = head;
while (*ptr != NULL)
{
if ((*ptr)->element > n->element)
break;
ptr = &(*ptr)->link;
}
n->link = *ptr;
*ptr = n;
return *head;
}
同样的,这里的head
是指向node指针数据的指针,这就可以做到以参数的方式传递进来还可以修改头指针的指向。
((*ptr)->element > n->element)
这条语句在访问后驱节点中element
的情况下还保存了前驱节点的信息,即前驱节点link
的地址。在发现后驱节点中element
值要比n->element
大,则跳出while
,此时ptr
存放的是前驱节点link
的地址,所以可以做到在两个节点之间插入新节点,包括头指针和起始节点之间插入的情况。
单向链表的删除
链表节点的删除一般是根据element
的情况来查找的,不可能不顾数据的有效性就删除某个节点。如果有那就是清空整个链表了。
这三种情况也就是删除中间节点和边缘节点,节点的删除和插入其实是类似的,也可以消除边缘情况,用一个函数解决。
void NodeDelete(list* head, int element)
{
list* ptr = head;
node* free_ptr = NULL;
while (*ptr != NULL)
{
if ((*ptr)->element == element)
{
free_ptr = *ptr;
*ptr = (*ptr)->link;
free(free_ptr);
printf("释放节点地址:%d\r\n", free_ptr);
return;
}
ptr = &(*ptr)->link;
}
printf("没有element值为%d的节点!\r\n",element);
}
节点的删除操作就不能像节点插入那样用一个变量解决了,需要free_ptr
进行暂存。如果没有这个中间变量,会出现什么情况:
- 先释放节点,这时就会丢失释放节点的后继节点起始地址。
- 先把释放节点的后继节点起始地址给释放节点的前驱节点则会丢失释放节点的起始地址(比较绕)。
单向链表的清空
当一个链表不需要的时候如何一次性清空所有节点,用NodeDelete
函数进行一个节点一个节点释放效率就低了,而且element
的值是可以改变的,甚至都不知道此时链表节点的element
都有哪些值,直接把NULL
给head
就更不行了。
void NodeClear(list* head)
{
node* ptr = NULL;
while (*head)
{
ptr = (*head)->link;
printf("释放节点地址:%d\r\n", *head);
free(*head);
*head = ptr;
}
}
这个函数的操作过程是将链表从起始节点开始一个节点一个节点释放,最后将末尾节点的link
值(NULL)给了头指针。函数形参如果定义为node*
而不是list*
,则清空函数无法改变头指针的值。当清空函数退出时,头指针仍指向原起始节点所在的起始地址,这个时候是非常危险的,一旦忘记清空头指针,则会访问一段已经释放了的内存。
单向链表的输出
最后为了直观的查看链表的情况,可以实现一个链表输出函数。
void ListPrintf(node* head)
{
node* ptr = head;
while (ptr != NULL)
{
printf("element:%d\r\n", ptr->element);
ptr = ptr->link;
}
}
链表的输出只是进行访问,不进行修改,更不会修改头指针指向,所以形参可以直接传入头指针的值,而不是头指针的地址。
源代码
突然发现MarkDown格式没有哪里可以上传文件,那只能把源码贴出来。
#include <stdio.h>
#include <stdlib.h>
typedef struct _node
{
int element;
struct _node* link;
}node;
typedef node* list;
node* NodeCreate(int element)
{
node* ptr = (node*)malloc(sizeof(node));
if (ptr == NULL)
{
printf("内存分配失败\r\n");
return NULL;
}
printf("创建节点地址:%d\r\n", ptr);
ptr->element = element;
ptr->link = NULL;
return ptr;
}
node* NodeInsertLast(list* head, node* n)
{
list* ptr = head;
while (*ptr != NULL)
{
ptr = &(*ptr)->link;
}
*ptr = n;
return *head;
}
node* NodeInsertOrder(list* head, node* n)
{
list* ptr = head;
while (*ptr != NULL)
{
if ((*ptr)->element > n->element)
break;
ptr = &(*ptr)->link;
}
n->link = *ptr;
*ptr = n;
return *head;
}
void NodeDelete(list* head, int element)
{
list* ptr = head;
node* free_ptr = NULL;
while (*ptr != NULL)
{
if ((*ptr)->element == element)
{
free_ptr = *ptr;
*ptr = (*ptr)->link;
free(free_ptr);
printf("释放节点地址:%d\r\n", free_ptr);
return;
}
ptr = &(*ptr)->link;
}
printf("没有element值为%d的节点!\r\n",element);
}
void NodeClear(list* head)
{
node* ptr = NULL;
while (*head)
{
ptr = (*head)->link;
printf("释放节点地址:%d\r\n", *head);
free(*head);
*head = ptr;
}
}
void ListPrintf(node* head)
{
node* ptr = head;
while (ptr != NULL)
{
printf("element:%d\r\n", ptr->element);
ptr = ptr->link;
}
}
int main()
{
node* head = NULL;
node* p = NULL;
for (size_t i = 1; i < 10; i+=2)
{
p = NodeCreate(i);
if (p != NULL)
NodeInsertLast(&head, p);
}
for (size_t i = 0; i < 10; i += 2)
{
p = NodeCreate(i);
if (p != NULL)
NodeInsertOrder(&head, p);
}
NodeDelete(&head, 0);
NodeDelete(&head, 1);
NodeDelete(&head, 2);
NodeDelete(&head, 3);
NodeDelete(&head, 4);
NodeDelete(&head, 9);
NodeDelete(&head, 9);
NodeDelete(&head,10);
ListPrintf(head);
NodeClear(&head);
if (head == NULL)
printf("链表节点内存全部释放\r\n");
while (1);
}
运行结果如图