一、定义:
概念:
链表是一种物理存储上非连续、非顺序的但逻辑顺序上是连续的线性存储结构。
特点:
由一系列节点构成,每个节点的生成(malloc)需要动态内存的开辟,它的每个节点由2部分组成
data 数据
next 指向下一个节点的指针(用来存储下一个节点的地址)
分类:
如下图:进行连线组合。
》不带头链表:就是说从第一个节点开始。没有哨兵位
》带头链表:头指的是哨兵位,所谓的哨兵位就是一个标记,放哨的。为了避免循环链表循环进入死循环。在其里面不会存放任何有效元素。以双向链表为例:当该链表的next和head都指向自己时(只有一个哨兵位时)就是空链表,若是head==NULL可以说这个链表不是个有效的链表(不能算是一个双链表)
1、⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
2、带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。
常见:
1、单链表(不带头单向不循环链表)
2、双向链表(带头双向循环链表)
今天要实现的也是这2个链表
二、单链表:
如图:我们将节点的下一个地址存放到这个指针中,逻辑上是连续的,物理上非连续的,因此我们通过指针来实现节点与节点之间的连接
你可以将单链表想象成火车:车头后面连接的车厢就是节点,我们每个车厢有一把钥匙用来连接后面的车厢,最后一个车厢没有钥匙了,因为后面没有车厢了,火车可以根据乘客人员情况调整车厢大小,可以在任意位置加一节车厢,任意插入删除车厢(增删查改);最后没有乘客了,拆掉去维护(销毁)
言归正传:开始一步一步实现:
注意:
火车头是为了方便理解,单链表本质是不带头的,我们将第一个节点叫做头节点(此头结点不等于哨兵位,其实实际上哨兵位才叫头节点,但是为了便于理解,将单链表第一个节点叫做头节点)
清单:
1、尾插
2、尾删
3、头插
4、头删
5、查找值为x的节点
6、指定位置之前插
7、指定位置之后插
8、删除pos节点
9、打印
10、销毁
补充一个函数:建立节点
创建一个节点要用到malloc,我分装写了,因为下面的插入都需要用到这个函数,所以有助于提高效率;减少代码量!!!这是我们创建的节点,一开始不确定next指向谁的,所以指向NULL
防止内存泄漏:返回的是结点指针,这个参数虽然被销毁了,但是申请的空间地址返回个调用的函数里面去了,因为malloc申请的空间是在堆区的,函数的创建和销毁是在栈上
//申请空间
SL* SLTByNode(SLDataType x)
{
SL* newnod = (SL*)malloc(sizeof(SL));
if (newnod==NULL)
{
perror("malloc");
return;
}
newnod->data = x;
newnod->next = NULL;
return newnod;
}
1、尾插:
初始情况是:创建一个节点的指针初始化为NULL,也就是什么也没有的情况,然后进行尾插,进入尾插的时候要先断言,不能传NULL过去;我的指针赋值为NULL,只是目前不知道它要干嘛,而不是说一直是空指针,而如果传的NULL过去,就是NULL的地址,NULL这个地址是不能操作的。
这里要用二级指针,可以能会有疑问!其实是因为如果我们传过去的指针的地址最开始的情况存的是NULL,那么说明这个时候的节点什么也没有,要先给plist赋值,也就是我们要修改plist,二级指针可以用来修改一级指针的内容;
头刚开始为NULL的情况刚好就是头插一次
尾插的动作:头的位置是不能变,要记住,当插入以后,plist的位置要回到头结点
//尾插:
void SLTpushBack(SL** phead, SLDataType x)
{
//防止传空指针!!
assert(phead);
//申请空间
SL*newnod = SLTByNode(x);
//非空和空链表 两种
if (*phead == NULL)
{
//头节点给传上来的指针
*phead = newnod;
}
//若不为空,找到尾节点
else
{
SL* ptal = *phead;
while (ptal->next)
{
ptal = ptal->next;
}
ptal->next = newnod;
}
}
2、尾删:
删除尾节点,有2种情况,第一种就是只有头节点:直接删除;第二种就是不止有头节点:遍历到尾节点,删除尾节点并将倒数第二个节点的next赋值为NULL,因为尾被消除了,倒数第二个节点如果指向尾的话,就变成了野狗(野指针)有
//尾删:
void SLTpopBack(SL** phead)
{
//链表不能为空且不能传空指针
assert(phead&&*phead);
//刚好一个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
//多个节点
else {
//两个指针,prov走到尾节点的前一节点,ptal一个走到尾节点
SL* ptal = *phead;
SL* prov = *phead;
while (ptal->next)
{
//将最后一个节点给前一个节点!!
prov = ptal;
ptal = ptal->next;
}
//释放空间
free(ptal);
//将第倒数第二个节点指向的内容赋值为空
prov->next = NULL;
}
}
3、头插:
在头插,比尾插的代码少了很多,就是直接将plist作为尾指针指向的NULL,不要去管头刚开始是否为NULL,因为插好以后,新创建的节点就变成了头指针
代码少了很多,不要遍历了;还省略了判断*phead是否为NULL的操作
//头插:
void SLTpushFron(SL** phead, SLDataType x)
{
//断言
assert(phead);
//先申请
SL* newnod = SLTByNode(x);
newnod->next = *phead;
//头节点是刚刚插入的节点
*phead = newnod;
}
4、头删:
注意的是头指针不能是空指针,若是为空指针则报错;
//头删:
void SLTpopFron(SL** phead)
{
链表不能为空且不能传空指针
assert(phead && *phead);
//将下个节点的钥匙给我;
SL* new = (*phead)->next;
//释放第一个节点的空间
free(*phead);
*phead = new;
}
5、查找值为X的节点:
传个值过去,遍历一次链表就可以了,找到了就返回该节点,没找到返回NUL;这里传的是一级指针,因为不要修改链表的内容,所以传一级指针够了;
//查找
SL* SLFind(SL* phead, SLDataType x)
{
SL* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没找到就传空!
return NULL;
}
6、指定位置之前插:
利用上面的查找,找到pos节点,我们要插入的位置的就是pos节点前
//指定位置之前插:
void SLTInsert(SL** phead,SL*pos, SLDataType x)
{
//断言
assert(phead&&*phead);
assert(pos);
//头插
if (pos == *phead)
{
SLTpushFron(phead, x);
}
else
{
SL* newnod = SLTByNode(x);
SL* pov = *phead;
while (pov->next != pos)
{
pov = pov->next;
}
newnod->next = pos;
pov->next = newnod;
}
}
7、指定位置之后插:
非常简单,不要遍历,直接对pos进行下手
//指定位置后插入
void SLTInsertAfter(SL* pos,SLDataType x)
{
//pos节点不能为NULL,不然不能插入
assert(pos);
SL* newnod = SLTByNode(x);
newnod->next = pos->next;
pos->next = newnod;
}
8、删除pos节点:
要考虑2个如果pos节点是头节点的话,直接进行头删,如果不是头,就得开始遍历了
发现了么 这里的pos传的是一级指针,实参传的也是一级指针,相当于值传递,修改形参不改变实参,这个时候需要我们在调用函数的文件里头,将该实参设置为NULL,当然避免麻烦的话,其实也可以将形参改成二级指针,进行解引用修改
//删除pos节点
void SLIErasert(SL** phead, SL* pos)
{
assert(phead && *phead);
assert(pos);
if (pos == *phead)
{
//头删:
SLTpopFron(phead);
}
else
{
SL* pov = *phead;
while (pov->next != pos)
{
pov = pov->next;
}
//将pos的下个节点地址给pos的前一个节点
pov->next = pos->next;
free(pos);
pos = NULL;
}
}
9、 打印:
遍历一遍就行了
//打印
void SLPrin(SL* phead)
{
//设立phed用来进行指针的操作
SL* pcur = phead;
while (pcur)
{
printf("%d ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
10、销毁单链表:
遍历销毁即可
//销毁链表
void SListDeseToy(SL** phead)
{
assert(phead);
SL* pcur = *phead;
while (pcur != NULL)
{
//先保存下一个节点
SL* pov = pcur->next;
//销毁此时的节点
free(pcur);
pcur = pov;
}
//phead已经空掉,要给个空指针,防止变成野指针
phead = NULL;
}
11、代码实现:
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
//链表由节点组成,车头
typedef struct SListNOde {
SLDataType data;
//因为在里面不能对里面的用改名后的SL来创建
SLDataType* next;
}SL;
//打印
void SLPrin(SL* phead);
//申请空间
SL * SLTByNode(SLDataType x);
//尾插:
void SLTpushBack(SL** phead,SLDataType x);
//尾删:
void SLTpopBack(SL** phead);
//头插:
void SLTpushFron(SL** phead, SLDataType x);
//头删:
void SLTpopFron(SL** Phead);
//查找:
SL* SLFind(SL* phead, SLDataType x);
//指定位置之前插:
void SLTInsert(SL** phead, SL* pos, SLDataType x);
//指定位置之后插入:
void SLTInsertAfter(SL* pos, SLDataType x);
//删除pos节点
void SLIErasert(SL** phead,SL * pos);
//删除pos之后的节点
void SLIErasertAfter(SL* pos);
//销毁节点
void SListDeseToy(SL** phead);
SList.c
#define _CRT_SECURE_NO_WARNINGS 3
#include"SList.h"
//打印
void SLPrin(SL* phead)
{
//设立phed用来进行指针的操作
SL* pcur = phead;
while (pcur)
{
printf("%d ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
//申请空间
SL* SLTByNode(SLDataType x)
{
SL* newnod = (SL*)malloc(sizeof(SL));
if (newnod==NULL)
{
perror("malloc");
return;
}
newnod->data = x;
newnod->next = NULL;
return newnod;
}
//尾插:
void SLTpushBack(SL** phead, SLDataType x)
{
//防止传空指针!!
assert(phead);
//申请空间
SL*newnod = SLTByNode(x);
//非空和空链表 两种
if (*phead == NULL)
{
//头节点给传上来的指针
*phead = newnod;
}
//若不为空,找到尾节点
else
{
SL* ptal = *phead;
while (ptal->next)
{
ptal = ptal->next;
}
ptal->next = newnod;
}
}
//尾删:
void SLTpopBack(SL** phead)
{
//链表不能为空且不能传空指针
assert(phead&&*phead);
//刚好一个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
//多个节点
else {
//两个指针,prov走到尾节点的前一节点,ptal一个走到尾节点
SL* ptal = *phead;
SL* prov = *phead;
while (ptal->next)
{
//将最后一个节点给前一个节点!!
prov = ptal;
ptal = ptal->next;
}
//释放空间
free(ptal);
//将第倒数第二个节点指向的内容赋值为空
prov->next = NULL;
}
}
//头插:
void SLTpushFron(SL** phead, SLDataType x)
{
//断言
assert(phead);
//先申请
SL* newnod = SLTByNode(x);
newnod->next = *phead;
//头节点是刚刚插入的节点
*phead = newnod;
}
//头删:
void SLTpopFron(SL** phead)
{
链表不能为空且不能传空指针
assert(phead && *phead);
//将下个节点的钥匙给我;
SL* new = (*phead)->next;
//释放第一个节点的空间
free(*phead);
*phead = new;
}
//查找
SL* SLFind(SL* phead, SLDataType x)
{
SL* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没找到就传空!
return NULL;
}
//指定位置后插入
void SLTInsertAfter(SL* pos,SLDataType x)
{
//pos节点不能为NULL,不然不能插入
assert(pos);
SL* newnod = SLTByNode(x);
newnod->next = pos->next;
pos->next = newnod;
}
//指定位置之前插:
void SLTInsert(SL** phead,SL*pos, SLDataType x)
{
//断言
assert(phead&&*phead);
assert(pos);
//头插
if (pos == *phead)
{
SLTpushFron(phead, x);
}
else
{
SL* newnod = SLTByNode(x);
SL* pov = *phead;
while (pov->next != pos)
{
pov = pov->next;
}
newnod->next = pos;
pov->next = newnod;
}
}
//删除pos节点
void SLIErasert(SL** phead, SL* pos)
{
assert(phead && *phead);
assert(pos);
if (pos == *phead)
{
//头删:
SLTpopFron(phead);
}
else
{
SL* pov = *phead;
while (pov->next != pos)
{
pov = pov->next;
}
//将pos的下个节点地址给pos的前一个节点
pov->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的节点
void SLIErasertAfter(SL* pos)
{
//pos不能为空,下一个节点也不能为空
assert(pos&&pos->next);
SL* del = pos->next;
//pos的下下个节点给pos
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SListDeseToy(SL** phead)
{
assert(phead);
SL* pcur = *phead;
while (pcur != NULL)
{
//先保存下一个节点
SL* pov = pcur->next;
//销毁此时的节点
free(pcur);
pcur = pov;
}
//phead已经空掉,要给个空指针,防止变成野指针
phead = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 3
#include"SList.h"
void lest02()
{
//先是空指针
SL* pilst = NULL;
SLTpushBack(&pilst, 1);
//SLTpushBack(&pilst, 2); SLTpushBack(&pilst, 3);
SLTpushFron(&pilst, 2);
SLTpushFron(&pilst, 3);
SLTpushFron(&pilst, 4);
SLPrin(pilst);
//SLTpopBack(&pilst);
//SLTpopBack(&pilst);
//SLTpopBack(&pilst);
//SLTpopBack(&pilst);
SLTpopFron(&pilst);
SLPrin(pilst);
//查找!!!
SL* Find = SLFind(pilst, 3);
/*if (Find == NULL)
{
printf("没找到!!\n");
}
else
{
printf("找到了!!\n");
}*/
SLTInsert(&pilst, Find, 6);
SLPrin(pilst);
删除pos节点的数据
//SLIErasert(&pilst, Find);
//SLPrin(pilst);
//删除pos节点之后的数据
/*SLIErasertAfter(Find);
SLPrin(pilst);
SListDeseToy(&pilst);*/
SLTInsertAfter(Find, 12);
SLPrin(pilst);
//SLTpopBack(&pilst);
}
//测试
int main()
{
//lest01();
lest02();
return 0;
}
三、双链表:
这里的头指的是哨兵位,之前提到过哨兵位,就是为了放哨的,防止死循环,头节点和之前单链表的头节点不一样哦!!!
1、双链表的节点:
就是多了一个头指针,指向前一个节点的地址
//数据类型
typedef int LTDataType;
//双向链表的节点
typedef struct ListNode {
LTDataType data;
//指向上一个节点
struct ListNode* next;
//指向前一个节点
struct ListNode* prev;
}LSNode;
2、节点的创建:
和单链表写法差不多,但是需要注意的是因为这个链表是循环的么,所以创建的节点的next和prev指向的都是它自己,如下图
//申请空间(因为都会给节点里存储值,所以哨兵位放置值不管)
LSNode* LTbuyNode(LTDataType x)
{
//申请节点
LSNode* node = (LSNode*)malloc(sizeof(LSNode));
if (node == NULL)
{
perror("malloc");
exit(1);
}
node->data = x;
//保证循环
node->next = node->prev = node;
return node;
}
3、初始化:
初始化有2种写法;第一种我注释掉了,为了与其他函数一样保持接口的一致性,所以没有用参数是二级指针的写法
//初始化
//void LTint(LSNode** pphead)
//{
// assert(pphead);
// //创建哨兵卫,随便传个值进去即可!!
// //二级指针解引用对一级指针进行改变
// *pphead = LTbuyNode(0);
//}
//初始化2写法:
LSNode* LTint()
{
LSNode* pphead = NULL;
pphead = LTbuyNode(0);
return pphead;
}
4、尾插:
插入前,先修改结点的指针指向位置,在去动我们的链表,因为我们是尾插,插到最后一个节点的后面,不就是相当于在哨兵位的前面么,所以直接去动哨兵位即可
传的是一级指针哦,为啥呢??因为我们传的是哨兵位,哨兵位不要修改,所以这里只要传一级指针就够了;
/尾插(不能修改哨兵,所以一级)
void LTPushBack(LSNode* pphead, LTDataType x)
{
//不能传空指针
assert(pphead);
//先创造节点
LSNode* newnode = LTbuyNode(x);
//先修改这个节点的地址
newnode->prev = pphead->prev;
//哨兵存前一个节点就是尾节点地址
newnode->next = pphead;
//顺序不可以交换
//pphead->prev相当于尾节点,此时先改掉尾节点指向的下一个节点地址
pphead->prev->next = newnode;
//再修改哨兵位指向的上一个节点,若是先改掉上个节点地址,则会导致尾节点地址不见了
pphead->prev = newnode;
}
5、头插:
头插需要注意:插入的位置不是在哨兵位前面,你想想尾插,不相当于在哨兵位前头插,那么头插就是插在哨兵位的后面,可以说是对于哨兵位尾插。
思路一样的,先修改要插入的节点,然后通过哨兵位找到d1,修改d1的prev指向,最后修改head的next指向即可;
//头插:
void LTpushFront(LSNode* pphead, LTDataType x)
{
assert(pphead);
LSNode* newnode = LTbuyNode(x);
newnode->prev = pphead;
newnode->next = pphead->next;
pphead->next->prev = newnode;
pphead->next = newnode;
}
6、尾删:
要判断一下是否有节点可以删除,若是没有的话就报错
//尾删:
void LTPopBack(LSNode* pphead)
{
//链表必须有效且不能为 空(pphead==NULL就是无效的双链表)
assert(pphead&&pphead->next!=pphead);
//先单独把尾节点找到并拿出
LSNode* newnode = pphead->prev;
//pphead->prev->prev->next
newnode->prev->next = pphead;
pphead->prev = newnode->prev;
//销毁掉
free(newnode);
newnode = NULL;
}
7、头删:
看了这么多图,相比已经会了,知道了逻辑不就很好做了么,这里就不画图了,主要是思路都一样的
//头删
void LTPopFron(LSNode* pphead)
{
//断言:有效(不是空链表)且不为 空
assert(pphead && pphead->next != pphead);
//先来一个节点接收要删除节点的地址
LSNode* newnode = pphead->next;
newnode->next->prev = pphead;
pphead->next = newnode->next;
//销毁掉
free(newnode);
newnode = NULL;
}
8、查找值为X的节点:
查找写了很多次了,这次主要就是要注意结束循环的条件,当cur到哨兵位就结束了
//查找
LSNode* LTFind(LSNode* pphead, LTDataType x)
{
assert(pphead && pphead->next != pphead);
LSNode* pcu = pphead->next;
while (pcu!=pphead)
{
if (pcu->data == x)
{
return pcu;
}
pcu = pcu->next;
}
//没找到!!
return NULL;
}
9、在pos节点之后插入数据:
其实也可以写一个pos节点之前插入数据,但是逻辑都是差不多的,我就写了这一个,你只要知道了pos节点的位置就可以开始准备插入了,其实只要会尾插、头插,这些衍生出来的插入删除都是小儿科
//在pos之后插入数据
void LTInsert(LSNode* pos, LTDataType x)
{
LSNode* newnode = LTbuyNode(x);
//先动不影响链表的节点
newnode->next = pos->next;
newnode->prev = pos;
//先将pos下个节点的前一个地址改成newnode
pos->next->prev = newnode;
pos->next = newnode;
}
10、删除pos节点:
删除pos和刚刚不是一样的么,但是这里会有疑问了:为啥竟然要修改指针的指向的内容,为啥不传二级指针呢????????????这是因为为了保持函数接口的一致性,我们的代码要给客户使用,如果一下子二级一下子一级要记忆的内容比较多,为了让顾客记忆量减少,所以尽量保持接口一致性,这样有点局限性也是没办法的;没办法!顾客就是上帝;
也不要慌!!!调用完这个函数后,主动将pos变成空指针即可
//删除pos位置的节点(为啥不传二级,因为保持接口一致性)
void LTErase(LSNode* pos)
{
assert(pos);
//先创建一个指针,来接收pos前一个节点地址(也可以不建立)
LSNode* newnode = pos->prev;
newnode->next = pos->next;
free(pos);
pos = NULL;
}
11、打印:
就是注意下循环结束的条件即可
//打印数据
void LTPrin(LSNode* pphead)
{
//pcu表示指向第一个节点的指针
LSNode* pcu = pphead->next;
while (pcu!= pphead)
{
printf("%d ", pcu->data);
pcu = pcu -> next;
}
putchar('\n');
}
12、销毁:
销毁这里哨兵位是没有改变的,和上面一样,要记得在调用完以后自己主动个它 置空
//销毁链表
void LTDesTroy(LSNode* pphead)
{
assert(pphead);
LSNode* pcu = pphead;
while (pcu != pphead)
{
LSNode* next = pcu->next;
free(pcu);
pcu = next;
}
//销毁哨兵的空间
free(pphead);
//因为是值传递,所以无法通过形参将实参改为空,需要手动去改!下面相当于为了好看点
pphead = NULL;
}
13、代码实现:
LIst.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//为了保持接口一致性,所以有些函数传参应该是用二级指针,却是使用的一级指针
//数据类型
typedef int LTDataType;
//双向链表的节点
typedef struct ListNode {
LTDataType data;
//指向上一个节点
struct ListNode* next;
//指向前一个节点
struct ListNode* prev;
}LSNode;
//初始化
//void LTint(LSNode** pphead);
LSNode* LTint();
//查找
LSNode* LTFind(LSNode* pphead, LTDataType x);
//打印数据
void LTPrin(LSNode* pphead);
//销毁链表
void LTDesTroy(LSNode* pphead);
//插入数据
//尾插:
void LTPushBack(LSNode* pphead, LTDataType x);
//头插:
void LTpushFront(LSNode* pphead,LTDataType x);
//在pos之后插入数据
void LTInsert(LSNode* pos, LTDataType x);
//删除数据:
//尾删:
void LTPopBack(LSNode* pphead);
//头删
void LTPopFron(LSNode* pphead);
//删除pos位置的节点
void LTErase(LSNode* pos);
List.c
#define _CRT_SECURE_NO_WARNINGS 3
#include"List.h"
//申请空间(因为都会给节点里存储值,所以哨兵位放置值不管)
LSNode* LTbuyNode(LTDataType x)
{
//申请节点
LSNode* node = (LSNode*)malloc(sizeof(LSNode));
if (node == NULL)
{
perror("malloc");
exit(1);
}
node->data = x;
//保证循环
node->next = node->prev = node;
return node;
}
//初始化
//void LTint(LSNode** pphead)
//{
// assert(pphead);
// //创建哨兵卫,随便传个值进去即可!!
// //二级指针解引用对一级指针进行改变
// *pphead = LTbuyNode(0);
//}
//初始化2写法:
LSNode* LTint()
{
LSNode* pphead = NULL;
pphead = LTbuyNode(0);
return pphead;
}
//查找
LSNode* LTFind(LSNode* pphead, LTDataType x)
{
assert(pphead && pphead->next != pphead);
LSNode* pcu = pphead->next;
while (pcu!=pphead)
{
if (pcu->data == x)
{
return pcu;
}
pcu = pcu->next;
}
//没找到!!
return NULL;
}
//打印数据
void LTPrin(LSNode* pphead)
{
//pcu表示指向第一个节点的指针
LSNode* pcu = pphead->next;
while (pcu!= pphead)
{
printf("%d ", pcu->data);
pcu = pcu -> next;
}
putchar('\n');
}
//销毁链表
void LTDesTroy(LSNode* pphead)
{
assert(pphead);
LSNode* pcu = pphead;
while (pcu != pphead)
{
LSNode* next = pcu->next;
free(pcu);
pcu = next;
}
//销毁哨兵的空间
free(pphead);
//因为是值传递,所以无法通过形参将实参改为空,需要手动去改!下面相当于为了好看点
pphead = NULL;
}
//尾插(不能修改哨兵,所以一级)
void LTPushBack(LSNode* pphead, LTDataType x)
{
//不能传空指针
assert(pphead);
//先创造节点
LSNode* newnode = LTbuyNode(x);
//先修改这个节点的地址
newnode->prev = pphead->prev;
//哨兵存前一个节点就是尾节点地址
newnode->next = pphead;
//顺序不可以交换
//pphead->prev相当于尾节点,此时先改掉尾节点指向的下一个节点地址
pphead->prev->next = newnode;
//再修改哨兵位指向的上一个节点,若是先改掉上个节点地址,则会导致尾节点地址不见了
pphead->prev = newnode;
}
//头插:
void LTpushFront(LSNode* pphead, LTDataType x)
{
assert(pphead);
LSNode* newnode = LTbuyNode(x);
newnode->prev = pphead;
newnode->next = pphead->next;
pphead->next->prev = newnode;
pphead->next = newnode;
}
//在pos之后插入数据
void LTInsert(LSNode* pos, LTDataType x)
{
LSNode* newnode = LTbuyNode(x);
//先动不影响链表的节点
newnode->next = pos->next;
newnode->prev = pos;
//先将pos下个节点的前一个地址改成newnode
pos->next->prev = newnode;
pos->next = newnode;
}
//尾删:
void LTPopBack(LSNode* pphead)
{
//链表必须有效且不能为 空(pphead==NULL就是无效的双链表)
assert(pphead&&pphead->next!=pphead);
//先单独把尾节点找到并拿出
LSNode* newnode = pphead->prev;
//pphead->prev->prev->next
newnode->prev->next = pphead;
pphead->prev = newnode->prev;
//销毁掉
free(newnode);
newnode = NULL;
}
//头删
void LTPopFron(LSNode* pphead)
{
//断言:有效(不是空链表)且不为 空
assert(pphead && pphead->next != pphead);
//先来一个节点接收要删除节点的地址
LSNode* newnode = pphead->next;
newnode->next->prev = pphead;
pphead->next = newnode->next;
//销毁掉
free(newnode);
newnode = NULL;
}
//删除pos位置的节点(为啥不传二级,因为保持接口一致性)
void LTErase(LSNode* pos)
{
assert(pos);
//先创建一个指针,来接收pos前一个节点地址(也可以不建立)
LSNode* newnode = pos->prev;
newnode->next = pos->next;
free(pos);
pos = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 3
#include"List.h"
void lest0()
{
LSNode* plist = NULL;
plist = LTint();
//尾插
LTPushBack(plist,1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
//头插
LTpushFront(plist, 4);
//尾删
//LTPopBack(plist);
//头删
//LTPopFron(plist);
//LTPopFron(plist);
LTPrin(plist);
LSNode* Find = LTFind(plist, 3);
if (Find == NULL)
{
printf("没找到!!\n");
}
else
{
printf("找到了!!\n");
//pos之后插入:
//LTInsert(Find, 8);
LTPrin(plist);
LTErase(Find);
Find = NULL;
}
LTPrin(plist);
//销毁
LTDesTroy(plist);
plist = NULL;
}
int main(){
lest0();
return 0;
}
四、总结:
写完后会发现:单向链表只能通过节点找到下一个节点的位置,而不能找到前一个节点的位置,然而我们双向链表只要知道该节点就能快速的找到前一个节点和下一个节点的位置,但是他们实现的思路基本大差不差,各有各的优点,按需选择即可
感谢 各位观众老爷的 支持!