目录
前言:
链表的定义与结构
单链表的接口实现
显示单链表
创建新结点
单链表尾插
头插的实现简单示例图
尾插经典错误示例1
尾插经典错误示例2
尾插函数的最终实现
单链表头插
单链表尾删
单链表头删
单链表查找
单链表在pos位置之前插入数据x
编辑
单链表在pos位置之后插入数据x
单链表删除pos位置的值
单链表删除pos位置之后的值
顺序表存在如下问题:
- 顺序表在中间位置或者头部进行元素插入或者删除时,由于需要拷贝数据,导致时间复杂度为O(N)效率比较低
- 增容需要申请新空间,拷贝数据,释放旧空间,尤其是异地扩容,会有不小的消耗;
- 增容一般呈两倍增长,势必会有一定空间的浪费;
上述问题的解决方案会采用另一种线性结构 —— 链表
定义:链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的;
理解思路:链表中的每一个数据元素都是独立存储的,当需要存储一个数据元素时则向内存的堆区申请一块空间存储当前数据,每一个数据元素通过地址串联在一起;对于链表这个结构而言,不仅需要存储当前的数据元素,而且需要存储下一个元素的地址,这样才能实现数据元素的串联;通常将待存储的数据元素和下一个数据的地址合起来称为链表中的一个节点;
示例图:
注意事项:
- 链式结构在逻辑上是连续的,但是在物理结构上不一定连续;
- 结点一般都是在内存的堆区申请;
- 堆上申请的空间是按照一定的策略分配,俩次申请的空间可能连续,也可能不连续;
typedef int SLTDatatype;
typedef struct SListNode
{
SLTDatatype data;//数据域
struct SListNode* next;//存放下一个结点的地址-指针域
}SListNode;
//此结构体大小为8字节
int main()
{
//堆区开辟8字节的空间
SListNode* p1 = (SListNode*)malloc(sizeof(SListNode));
//结构体前四个字节存放数据
p1->data = 10;
SListNode* p2 = (SListNode*)malloc(sizeof(SListNode));
p2->data = 20;
SListNode* p3 = (SListNode*)malloc(sizeof(SListNode));
p3->data = 30;
//结构体后四个字节存放下一个结点的地址
p1->next = p2;
p2->next = p3;
p3->next = NULL;
return 0;
}
监视窗口:
由于结构体的大小为8字节,从上图可以看出申请的空间并不一定连续;
数据结构无非是对数据进行管理,要实现数据的增删查改,因此链表的基本功能也是围绕数据的增删查改而展开;
顺序表传参时指针必不为空指针,而链表传参指针可能为空指针;
当链表中没有元素时,头指针所指向的就是空指针;(不可断言指针为空)
当链表中存在元素时,最后一个结点的指针域必存在空指针,当指针域的元素为空,停止打印;
void PrintSList(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL )
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void Test1()
{
SListNode* p1 = (SListNode*)malloc(sizeof(SListNode));
p1->data = 10;
SListNode* p2 = (SListNode*)malloc(sizeof(SListNode));
p2->data = 20;
SListNode* p3 = (SListNode*)malloc(sizeof(SListNode));
p3->data = 30;
p1->next = p2;
p2->next = p3;
p3->next = NULL;
PrintSList(p1);
}
int main()
{
Test1();
return 0;
}
运行结果:
单链表进行头插尾插时,插入的是一个结点,这个结点包括SLTDatatype数据和SLTDatatype*的指针,为防止代码过于冗余,单独封装为BuySListNode()函数来创建新结点;
SListNode* BuySListNode(SLTDatatype x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc failed");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void Test2()
{
SListNode* n1 = BuySListNode(10);
SListNode* n2 = BuySListNode(20);
SListNode* n3 = BuySListNode(30);
n1->next = n2;
n2->next = n3;
PrintSList(n1);
}
int main()
{
Test2();
return 0;
}
运行结果:
实现尾插之前,我们先看一个头插的简单的示例,并且认知实现尾插时的各种常见错误,分析具体原因,从而实现真正的尾插函数;
假设1:链表中存在元素,如下图所示,如何实现头插?
首先利用BuySListNode()函数来创建一个新结点newnode;
其次只需要 newnode->next中的NULL变成pList,即newnode->next=pList;
最后使pList指向newnode,即pList=newnode;这样便完成链表的头插元素;
假设2: 链表中没有元素,如何实现头插?
1. 首先利用BuySListNode()函数来创建一个新结点newnode;
2. 其次只需要 newnode->next中的NULL变成pList,即newnode->next=pList;
3.最后使pList指向newnode,即pList=newnode;这样便完成链表的头插元素;
如上所得,链表中结点是否存在,对于头插的实现逻辑上没有任何区别;
假设链表中存在结点,如何实现尾插?
- 利用BuySListNode()函数来创建一个新结点newnode;
- 找到最后一个元素(尾结点);
- 尾结点中的指针域赋值为newnode;
//下述代码正确吗?
void SLTpushback(SListNode* phead, SLTDatatype x)
{
SListNode* newnode = BuySListNode(x);
SListNode* tail = phead;
//查找尾结点
while (tail != NULL)
{
tail = tail->next;
}
//尾插
tail = newnode;
}
当调用SLTpushback()函数,操作系统会为该函数开辟函数栈帧,而phead(形式参数),tail(局部变量),newnode(局部变量)的值全部保存在栈空间,但是形式参数只是实际参数的一份临时拷贝,即值是相同的,空间是独立的,改变形参并不会影响实参; 局部变量进入作用域创建,出作用域由于操作系统回收空间自动销毁;
如上图所示,上述代码不仅没有实现尾插节点的功能 ,还导致了内存泄漏;
尾插正确做法1(链表中存在元素)
结点是由malloc()函数在堆区申请的空间,改变堆区的数据随着函数栈帧的销毁数据并不会销毁,而形式参数,局部变量在栈空间上创建,函数栈帧销毁后随之销毁;只需要改变尾结点的指针域(存放于堆区),使得其指向新结点,从而就可以实现尾插;
//尾插正确思路(假设链表中存在元素)
void SLTpushback(SListNode* phead, SLTDatatype x)
{
SListNode* newnode = BuySListNode(x);
SListNode* tail = phead;
while ((tail->next) != NULL)
{
tail = tail->next;
}
//修改尾结点的指针域
tail->next = newnode;
}
假设链表中没有结点,如何实现尾插?
- 利用BuySListNode()函数来创建一个新结点newnode;
- 头指针pList赋值为newnode;
链表为空实现尾插物理图
//下述代码正确吗?
void SLTpushback(SListNode* phead, SLTDatatype x)
{
SListNode* newnode = BuySListNode(x);
//链表为空,没有结点
if (phead == NULL)
{
phead = newnode;
}
//链表不为空,存在结点
else
{
SListNode* tail = phead;
while ((tail->next) != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
当链表为空时,希望实参pList指向新结点,但是phead为形式参数,对形参phead的修改不会影响实参plist, 而且phead出作用域自动销毁;具体见下图所示
上述代码存在问题,它不但丢失了新创建的结点,而且没有修改实参pList,没有实现尾插;
综合上述所有考虑,当链表为空时,为了能够修改实参pList(头指针),我们只能传址调用,那么就该采用二级指针来接收参数 ;
//尾插新结点
void SLTpushback(SListNode** pphead, SLTDatatype x)
{
SListNode* newnode = BuySListNode(x);
//链表为空,没有结点
if (*pphead == NULL)
{
*pphead = newnode;
}
//链表不为空,存在结点
else
{
SListNode* tail = *pphead;
while ((tail->next) != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void Test()
{
SListNode* pList = NULL;
SLTpushback(&pList, 1);
SLTpushback(&pList, 2);
SLTpushback(&pList, 3);
PrintSList(pList);
}
int main()
{
Test();
return 0;
}
运行结果:
正如前文所述,单链表中结点是否存在,对于头插的实现逻辑上没有任何区别,但是由于头插的过程中需要不断的改变单链表中的头结点,所以传参时需要头指针的地址,方便修改实参;
//头插新结点
void SLTpushfront(SListNode** pphead, SLTDatatype x)
{
SListNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void Test()
{
SListNode* pList = NULL;
SLTpushfront(&pList, 10);
SLTpushfront(&pList, 20);
SLTpushfront(&pList, 30);
SLTpushfront(&pList, 40);
PrintSList(pList);
}
int main()
{
Test();
return 0;
}
运行结果:
链表的核心特性在于前后关联,不能只考虑单个结点,如果只考虑单个结点,该结点前后的结点容易出现错误;比如实现尾删时,如果只想找到链表最后一个元素,进而释放尾结点的空间,但是尾结点的前一个节点的指针域并未置空,仍然指向一块未知空间,造成野指针;
假设链表中存在两个及两个以上的结点,如何实现尾删 ?
通过上述三个图,发现好像当单链表尾删传参时,并不需要二级指针;
假设链表中存在1个结点,如何实现尾删 ?
当链表中只有一个节点时,根本不存在前一个节点,上述针对多结点的方式已经失效;
当尾删掉这个节点之后,需要将pList置为空指针,如何改变pList ?
因此传参时只能传址调用,只有传址才能改变pList中的值;
所以当进行尾删时,只能传递二级指针;
当链表为空时不能进行尾删操作,因此需要对链表进行判空;
void SLTpopback(SListNode** pphead)
{
//单链表为空
assert(*pphead != NULL);
//单链表只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);//其实*pphead就是pList
*pphead = NULL;
}
//单链表有多个结点
else
{
SListNode* tailPrev = NULL;//记录尾结点的前一个结点
SListNode* tail = *pphead; //寻找尾结点
while (tail->next!=NULL)
{
tailPrev = tail;
tail = tail->next;
}
free(tail);
tailPrev->next = NULL;
}
}
void SLTpopback(SListNode** pphead)
{
//单链表为空
assert(*pphead != NULL);
//单链表只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);//其实*pphead就是pList
*pphead = NULL;
}
//单链表有多个结点
else
{
SListNode* tail = *pphead;
while (tail->next->next!=NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void Test()
{
SListNode* plist = NULL;
SLTpushback(&plist, 11);
SLTpushback(&plist, 22);
SLTpushback(&plist, 33);
PrintSList(plist);
SLTpopback(&plist);
PrintSList(plist);
SLTpopback(&plist);
PrintSList(plist);
SLTpopback(&plist);
PrintSList(plist);
}
int main()
{
Test();
return 0;
}
运行结果:
对链表进行头删操作,需要对链表的头结点进行改变,所以传参时需要传址调用,需要头指针的地址,因此参数为二级指针;链表为空不能进行头删操作,因此需要对链表进行判空;
case 1: 如果直接释放首结点,通过pList无法查找到首节点的下一个结点;
case2: 如果让pList直接指向首结点的下一个节点,由于首结点丢失,无法释放首结点;
case 2:
//单链表头删
void SLTpopfront(SListNode** pphead)
{
//判空
assert(*pphead!=NULL);
//非空
SListNode* newhead = (*pphead)->next;
free(*pphead);
*pphead = newhead;
}
void Test()
{
SListNode* plist = NULL;
SLTpushback(&plist, 10);
SLTpushback(&plist, 20);
SLTpushback(&plist, 30);
PrintSList(plist);
SLTpopfront(&plist);
PrintSList(plist);
SLTpopfront(&plist);
PrintSList(plist);
SLTpopfront(&plist);
PrintSList(plist);
}
int main()
{
Test();
return 0;
}
运行结果:
按照数据进行查找,找到返回结点位置,找不到返回空指针;
SListNode* SListFind(SListNode* phead, SLTDatatype x)
{
assert(phead != NULL);
SListNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void Test()
{
SListNode* plist = NULL;
SLTpushback(&plist, 10);
SLTpushback(&plist, 20);
SLTpushback(&plist, 30);
SLTpushback(&plist, 40);
SLTpushback(&plist, 50);
printf("请输入要查找的值\n");
int x = 0;
scanf("%d", &x);
SListNode* pos = SListFind(plist, x);
if (pos != NULL)
{
pos->data = pos->data * 10;
}
PrintSList(plist);
}
int main()
{
Test();
return 0;
}
运行结果:
查找函数的接口既实现了查找的功能,又承担了修改数据的功能;
case 1: pos位置处的结点就是首节点;需要改变头指针的值,传参时应该传址调用;
case 2:pos位置处的结点非首结点 ;需要查找pos位置的前一个节点,将前一个结点的指针域置为newnode;
void SListInsert(SListNode** pphead, SListNode* pos, SLTDatatype x)
{
assert(pos != NULL);
if (*pphead == pos)
{
SLTpushfront(pphead, x);
}
else
{
SListNode* posprev = *pphead;
while (posprev->next != pos)
{
posprev = posprev->next;
}
SListNode* newnode = BuySListNode(x);
newnode->next = pos;
posprev->next = newnode;
}
}
void Test9()
{
SListNode* plist = NULL;
SLTpushback(&plist, 1);
SLTpushback(&plist, 2);
SLTpushback(&plist, 3);
SLTpushback(&plist, 4);
SLTpushback(&plist, 5);
int x = 0;
scanf("%d", &x);
SListNode* pos = SListFind(plist, x);
if (pos != NULL)
{
SListInsert(&plist, pos, x*10);
}
PrintSList(plist);
}
int main()
{
Test9();
return 0;
}
运行结果:
当在pos位置之后插入数据,不需要查找前一个结点,只需要根据pos的位置就可完成插入;
void SListInsertAfter(SListNode* pos, SLTDatatype x)
{
assert(pos != NULL);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void Test()
{
SListNode* plist = NULL;
SLTpushback(&plist, 1);
SLTpushback(&plist, 2);
SLTpushback(&plist, 3);
SLTpushback(&plist, 4);
SLTpushback(&plist, 5);
int x = 0;
scanf("%d", &x);
SListNode* pos = SListFind(plist, x);
if (pos != NULL)
{
SListInsertAfter(pos, x * 10);
}
PrintSList(plist);
}
int main()
{
Test();
return 0;
}
运行结果:
假设pos位置处的结点非首结点
- 查找pos位置处的前一个结点
- 将pos处的前一个节点的指针域赋值为pos后一个节点 ;
- 释放pos处的空间
假设pos位置为首结点,直接按照头删的逻辑处理;
void SListEarse(SListNode** pphead, SListNode* pos)
{
assert(pos != NULL);
if (*pphead == pos)
{
SLTpopfront(pphead);
}
else
{
SListNode* posprev = *pphead;
while (posprev->next != pos)
{
posprev = posprev->next;
}
posprev->next = pos->next;
free(pos);
}
}
void Test()
{
SListNode* plist = NULL;
SLTpushback(&plist, 1);
SLTpushback(&plist, 2);
SLTpushback(&plist, 3);
SLTpushback(&plist, 4);
SLTpushback(&plist, 5);
PrintSList(plist);
int x = 0;
scanf("%d", &x);
SListNode* pos = SListFind(plist, x);
if (pos != NULL)
{
SListEarse(&plist, pos);
}
PrintSList(plist);
}
int main()
{
Test();
return 0;
}
运行结果:
void SListEarseAfter(SListNode* pos)
{
assert(pos);
// 检查pos是否是尾节点
assert(pos->next);
SListNode* posnext = pos->next;
pos->next = posnext->next;
free(posnext);
posnext = NULL;
}
void Test()
{
SListNode* plist = NULL;
SLTpushback(&plist, 1);
SLTpushback(&plist, 2);
SLTpushback(&plist, 3);
SLTpushback(&plist, 4);
SLTpushback(&plist, 5);
PrintSList(plist);
int x = 0;
scanf("%d", &x);
SListNode* pos = SListFind(plist, x);
if (pos != NULL)
{
SListEarseAfter(pos);
}
PrintSList(plist);
}
int main()
{
Test()
return 0;
}
运行结果: