目录
初步了解链表
在介绍链表之前,让我们来回顾下数组。
我们都知道数组是通过下标来访问数组内容的,下面是一维数组在内存中的分布。
但是实际上,数组下标的访问是指针的后移,因为arr[ i ]<==>* (arr+i),即通过首元素的地址+i向后访问后面的内存
这种访问必须满足数据是连续存储的。而今天我们要介绍的链表,则是逐个向后访问非连续的内存的一种结构。
链表的基本构成
为了能够将分散的内存联系在一起,必不可少的,我们需要像“绳子”一样的东西将其一个个连接起来,而这个“绳子”就是指针。
因此,链表的一个节点由两部分构成,数据域和指针域
其中数据域用来存储数据,例如 int a=10;等,而指针域则是用来存储指针,该指针指向下一块需要访问的内存。
真实结构
1.
(此时创建的就是头指针+节点+表尾的单链表)
2.
(此时创建的是头指针+哨兵位+节点+表尾的单链表)
(哨兵位是指头指针指向一个节点,此节点数据域不存放任何东西)
因此,各个不相连的内存就通过指针联系在一起,构成链表。
链表的组成部分有,头指针,(哨兵位)节点和表尾。这样由前到后组成的结构为单链表。其中头指针就是指向这个链表的指针,用于找到并访问该链表,节点是构成链表的基本单位,表尾的指针域指向空,作为链表的结束。(注:链表有单、双向,带哨兵位和不带哨兵位,循环和非循环的分类,本篇仅介绍‘单链表非循环不带哨兵位’和‘双向链表循环带哨兵位’两种类型)
单链表
单链表的创建
以下为创建的函数的功能
SLTnode* BuylistNode(int x) //创建一个新节点
SLTnde* Creat(int n) //初始化链表
void SlistPrint(SLTnode* Phead) //打印链表
void SlistPushFront(SLTnode** pphead, int x) //头插
void SlistPushBack(SLTnode*Phead, int x) //尾插
void SlistPopfront(SLTnode** pphead) //头删
void SlistPopBack(SLTnode*Phead) //尾删
SLTnode* SlistFind(SLTnode* Phead,int x) //查找
void SlistInsertFront(SLTnode** pphead, SLTnode* pos, int x) //定点前插
void SlistInsertAfter(SLTnode* pos, int x) //定点后插
void SlistErase(SLTnode** pphead,SLTnode* pos) //定点删除
void SlistDestroy(SLTnode** pphead) //销毁
单链表可用自义定结构体创建。
struct slistnode
{
//数据域
int data;
//指针域
struct slistnode* next;
};
为了方便写代码,一般将该结构体重命名,用起来更为方便。
typedef struct slistnode
{
//数据域
int data;
//指针域
struct slistnode* next;
}SLTnode;
链表的初始化
本质上就是创造链表的长度并赋给数据域内容。
SLTnode* BuylistNode(int x)
{
SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
//在堆区开辟一个节点的空间
newnode->data = x;
//为节点数据域赋值
newnode->next = NULL;
//该节点指针域指向空
return newnode;
//返回该节点的指针
}
此时BuylistNode函数就创建了一个表尾,返回值用Phead接收。(此时创建的就是头指针+节点+表尾的单链表)
#include<stdio.h>
#include<stdlib.h>
int main()
{
SLTnode* Phead = BuylistNode(1);
return 0;
}
此时的Phead就为次链表的头指针。后面还可以通过其他形式来加长此链表。
当然,也可以初始就创建一个较为长的链表。
SLTnode* Creat(int n)
{
SLTnode* head = (SLTnode*)malloc(sizeof(SLTnode));
//创建一个单链表的头,并为其开辟空间
SLTnode* Phead = head;
//将该头的指针先保存起来,以防后面head改变时能正常返回链表的头指针
for (int i = 0; i < n; i++) //创建n个节点
{
SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
//为新节点开辟空间
scanf("%d", &newnode->data);
//为数据域赋值
head->next = newnode;//将新建的节点与头连接起来
head = newnode;//head指针向后移,经循环后继续向后增加节点
//看不懂没关系,增加节点会单独拿出来讲
}
head->next = NULL;//经过循环后,此时的head指向表尾,为表尾的指针域赋上空
return Phead;//返回链表的头指针
}
(此时创建的是头指针+哨兵位+节点+表尾的单链表)
(用此时的结构是因为一次性创建连续的单链表有哨兵位会更方便些)
单链表的打印
逐个访问单链表节点,并打印出其数据域存储的内容。
1.无哨兵位的打印
void SlistPrint1(SLTnode* Phead)
{
while (Phead != NULL)
{
printf("%d", Phead->data);
Phead = Phead->next ;
}
}
依次访问链表内容并打印,直到遇到表尾。
2.有哨兵位的打印
void SlistPrint2(SLTnode* Phead)
{
while (Phead != NULL)
{
Phead = Phead->next;
printf("%d", Phead->data);
}
}
此种打印方式直接跳过了哨兵位,对后面的进行打印。
第一种打印
#include<stdio.h>
#include<stdlib.h>
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPrint1(Phead);
//SLTnode* Phead=creat(3);
//SlistPrint2(Phead);
return 0;
}
结果
从监视窗口中看,更容易看出链表结构。
第二种方式打印
#include<stdio.h>
#include<stdlib.h>
int main()
{
//SLTnode* Phead = BuylistNode(1);
//SlistPrint1(Phead);
SLTnode* Phead=creat(3);
SlistPrint2(Phead);
return 0;
}
结果
监视窗口
单链表的增、删、查、改
(本文章以无哨兵位的单链表为例)
1.单链表的头插
头插,即在单链表的最前面的节点前增加一个新节点,具体过程为:创建新节点,新节点指针域指向第一个节点,头指针指向新节点。
void SlistPushFront1(SLTnode* Phead, int x)
{
SLTnode*newnode= BuylistNode(x);//设置新节点
newnode->next = Phead;//新节点指针域指向第一个节点
Phead = newnode;//头指针指向新节点
return Phead;//返回新的头指针
}
这样做必须要返回新的头指针,因为传头指针进入函数时,依然用一级指针接收,为同级传参,也就是说函数内部传过来的只是头指针的一份拷贝,无法改变头指针的指向,因此需要返回新的头指针,并在主函数接收。可以看出,这种方式调用函数非常繁琐、别扭。因此,可以用另一种方式在函数内部也能直接更改头指针的指向,即二级指针。
void SlistPushFront2(SLTnode** pphead, int x)
{
SLTnode*newnode= BuylistNode(x);//设置新节点
newnode->next = *pphead;//新节点指针域指向第一个节点
*pphead = newnode;//头指针指向新节点
}
省去了主函数更新头指针的步骤。
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPushFront2(&Phead, 2);
SlistPrint1(Phead);
return 0;
}
打印结果
2.单链表的尾插
尾插,即在单链表的表尾后增加一个新节点,具体过程为:创建新节点,找到表尾地址,表尾指针域指向新节点。
void SlistPushBack(SLTnode*Phead, int x)
{
SLTnode* newnode = BuylistNode(x);//设置新节点
while (Phead->next != NULL)
{
Phead = Phead->next;
}
//通过循环找到表尾,因为Phead为拷贝,不会影响主函数的头指针
Phead->next = newnode;//表尾指针域指向新节点
}
测试
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPushFront2(&Phead, 2);
SlistPushBack(Phead,3);
SlistPrint1(Phead);
return 0;
}
3.单链表删头
删除第一个节点,具体过程为:记录第二个节点的地址,释放第一个节点,最后让头指针指向第二个节点。(操作前须判断该节点是否已被删完,1、2均未考虑初始为空,有兴趣可自行加入断言或用其他方式处理)
void SlistPopFront(SLTnode** pphead)
{
assert(*pphead != NULL);
//断言,链表为空时,不可能再删了
SLTnode* head = (*pphead)->next;
//记录第二个节点的地址
free(*pphead);
//释放第一个节点
*pphead = head;
//让头指针指向第二个节点
}
测试
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPushFront2(&Phead, 2);
SlistPushBack(Phead,3);
SlistPopFront(&Phead);
SlistPrint1(Phead);
return 0;
}
4.单链表尾删
删除最后一个节点,具体过程为:找到最后一个节点,想办法记录倒数第二个节点的位置,将倒数第二个节点指针域置空,最后释放最后一个节点。
void SlistPopBack(SLTnode*Phead)
{
assert(Phead != NULL);
//断言,链表不能为空
SLTnode* prev = NULL;
//初始化prev,准备记录倒数第二个节点
while (Phead->next != NULL)
{
prev = Phead;
Phead = Phead->next;
}
prev->next = NULL;//倒数第二个指针域置空
free(Phead);//释放最后一个节点
}
测试
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPushFront2(&Phead, 2);
SlistPushBack(Phead,3);
SlistPopBack(Phead);
SlistPrint1(Phead);
return 0;
}
5.单链表查找
根据节点中数据域的值来查找,具体过程为:找到该节点,并返回该节点地址。
SLTnode* SlistFind(SLTnode* Phead,int x)
{
while (Phead != NULL)//能够把全部节点都判断一遍
{
if (Phead->data == x)
{
return Phead;//找到该节点,并返回地址
}
else
{
Phead = Phead->next;//没找到则继续向后进行
}
}
return NULL;
}
测试
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPushBack(Phead,2);
SlistPushBack(Phead,3);
SlistPushBack(Phead,4);
SlistPushBack(Phead,5);
SlistPushBack(Phead,6);
SlistPushBack(Phead,7);
SlistPushBack(Phead,8);
SlistPushBack(Phead,9);
SlistPushBack(Phead,10);
SlistPrint1(SlistFind(Phead,5));
return 0;
}
(虽然实现了查找,但是返回值只能有一个,故此程序只能找到第一个,如果后面还有则不能返回,当然,也可以通过多次调用查找函数来找到后面的地址。另外,为了防止为未找到打印有误,最好在打印函数内加上Phead不为空的断言)
多次调用示例
int main()
{
SLTnode* Phead = BuylistNode(1);
SlistPushBack(Phead,2);
SlistPushBack(Phead,3);
SlistPushBack(Phead,4);
SlistPushBack(Phead,5);
SlistPushBack(Phead,6);
SlistPushBack(Phead,5);
SlistPushBack(Phead,8);
SlistPushBack(Phead,9);
SlistPushBack(Phead,10);
SlistPrint1(SlistFind(Phead,5));
printf("\n");
SlistPrint1(SlistFind(SlistFind(Phead, 5)->next ,5));
//注意:第二次调用函数时传的是第一次函数返回节点的后一个节点的地址
return 0;
}
结果
6.单链表定向插入
(1)前插
根据节点的地址来找到节点,在其前一个位置插入新的节点。(具体细节不再赘述)
void SlistInsertFront(SLTnode** pphead, SLTnode* pos, int x)
{
//pos传的是想要插入节点的位置
assert(*pphead!=NULL);
if (*pphead == pos)
{
SlistPushFront2(pphead, 2);
//若*pphead==pos,相当于头插,直接用头插的函数
}
else
{
SLTnode* head = *pphead;//拷贝头指针,不改动头指针
SLTnode* prev = NULL;//准备记录pos的前一个节点
SLTnode* newnode = BuylistNode(x);
//创建一个新的节点
while (head != pos)
{
assert(head != NULL);
prev = head;//最终记录pos的前一个节点
head = head->next;//不满足条件向后移
}
prev->next = newnode;//满足条件后pos前一个节点指向新节点
newnode->next = pos;//新节点指向pos
}
}
测试
int main()
{
SLTnode* Phead = BuylistNode(1);
//SlistPushFront2(&Phead, 2);
SlistPushBack(Phead,2);
SlistPushBack(Phead,3);
SlistPushBack(Phead,4);
SlistPushBack(Phead,5);
SlistPushBack(Phead,6);
SlistPushBack(Phead,7);
SlistPushBack(Phead,8);
SlistPushBack(Phead,9);
SlistPushBack(Phead,10);
SlistInsertFront(&Phead,SlistFind(Phead, 5),5);
SlistPrint1(Phead);
return 0;
}
结果
(2)后插
void SlistInsertAfter(SLTnode* pos, int x)
{
SLTnode* newnode = BuylistNode(x);
SLTnode* tail = pos->next;
pos->next = newnode;
newnode->next = tail;
}
7.单链表定点删除
void SlistErase(SLTnode** pphead,SLTnode* pos)
{
if (*pphead == pos)//若删除的是第一个节点,则只需调用头删
{
SlistPopFront(pphead);//头删函数
}
else
{
SLTnode* head = *pphead;//拷贝头指针,不改动头指针
SLTnode* prev = NULL;//准备记录pos的前一个节点
while (head != pos)
{
assert(head != NULL);
prev = head;//最终记录pos的前一个节点
head = head->next;//不满足条件向后移
}
SLTnode* tail = head->next;
prev->next =tail ;//满足条件后pos前一个节点指向pos后一个节点
free(head);
}
}
8.销毁链表
逐个访问节点并释放
void SlistDestroy(SLTnode** pphead)
{
assert(*pphead != NULL);
SLTnode* tail = NULL;
while ( *pphead!= NULL)
{
tail = (*pphead)->next ;
free(*pphead);
*pphead = tail;
}
}
双向循环链表
双向循环链表的创建
以下为创建的函数的功能
CSLTnode* CSlistInit() //初始化链表
CSLTnode* CBuylistNode(int x) //建立一个节点
void CSlistPushBack(CSLTnode* CPhead, int x) //尾插
void CSlistPrint(CSLTnode* CPhead) //打印
void CSlistPushFront(CSLTnode* CPhead, int x) //头插
typedef struct cslistnode
{
struct cslistnode* prev;
int data;
struct cslistnode* next;
//成员间顺序无所谓,这样写只是为了与图片更契合
}CSLTnode;
双向循环链表的初始化
CSLTnode* CSlistInit()
{
CSLTnode* CPhead = (CSLTnode*)malloc(sizeof(CSLTnode));
CPhead->next = CPhead;
CPhead->prev = CPhead;//初始哨兵位,头尾指针都指向自身
return CPhead;//返回创建哨兵位的指针
}
建立一个节点
CSLTnode* CBuylistNode(int x)
{
CSLTnode* newhead = (CSLTnode*)malloc(sizeof(CSLTnode));
newhead->data = x;
newhead->next = newhead;
newhead->prev = newhead;
return newhead;
}
双向循环链表的尾插
在链表后插入一个新节点,具体过程为:找到最后一个节点,创建新节点,最后一个节点next指向新节点,新节点的prev指向最后一个节点,新节点的next指向哨兵位,哨兵位的prev指向新节点。
void CSlistPushBack(CSLTnode* CPhead, int x)
{
CSLTnode* newnode = CBuylistNode(x);
//建立新节点
CSLTnode* tail = CPhead;
//记录头指针
while (CPhead->next != tail)
{
CPhead = CPhead->next;
}
//尾结点存的是哨兵位的地址,通过该条件找到尾结点
CPhead->next = newnode;
//尾结点next指向新节点
newnode->prev = CPhead;
//新节点prev指向尾结点
newnode->next = tail;
//新节点next指点哨兵位
tail->prev = newnode;
//哨兵位prev指向新节点
}
测试
建立一个哨兵位后监视明显看出循环性
尾插成功
双向循环链表的打印
void CSlistPrint(CSLTnode* CPhead)
{
CSLTnode* tail = CPhead;
//记录头指针
while (CPhead->next != tail)
{
CPhead = CPhead->next;
printf("%d ", CPhead->data);
}
}
双向循环链表的头插
在链表前插入一个新节点,具体过程为:找到第一个节点节点,创建新节点,第一个节点prev指向新节点,新节点的next指向最后一个节点,新节点的prev指向哨兵位,哨兵位的next指向新节点
void CSlistPushFront(CSLTnode* CPhead, int x)
{
assert(CPhead);
CSLTnode* newnode = CBuylistNode(x);
//创建新节点
CSLTnode* tail = CPhead->next ;
//记录第一个节点的地址
newnode->next = tail;
//新节点next指向第一个节点
tail->prev = newnode;
//第一个节点prev指向新节点
CPhead->next = newnode;
//哨兵位next指向新节点
newnode->prev = CPhead;
//新节点prev指向哨兵位
}
双向循环链表一个节点记录了前后的地址,因此删除较为简单,可参考单链表的删除。