链表中常用的的两种结构就是无头单向非循环链表和带头双向循环链表,在前面我们已经学习了无头单向非循环链表,今天我们将学习带头双向循环链表。
实际中链表的结构非常多样,一下情况结合起来就有8种链表结构。
上面这些情况有8种组合,如图:
虽然有这么多种链表结构,但是我们最常用还是以下两种结构:
1、无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为复杂数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2、带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。下面我们代码实现就知道了。
代码演示:
//双向链表数据类型重命名:①见名知意;②后期方便更改
typedef int LTDataType;
//双向链表结点的定义
typedef struct ListNode
{
LTDataType data;//存储链表数据
struct ListNode* prev;//存储结点的前驱结点地址
struct ListNode* next;//存储结点的后继结点地址
}LTNode;
解读:
①双向链表的结点由三个成员组成——顾名知义,在双向链表的结点中有两个指针域,一个指向前驱结点,另一个指向后继结点;最后还有一个数据域存储链表的数据。图示:
②双向链表由三个部分组成,所以是复杂结构。在C语言中用结构体struct定义,为了见名知意我们将其取名为ListNode。
③为了后续我们使用方便,我们将链表中的数据类型和链表结点typedef一下。(typedef类型重命名——①见名知意;②一改全改,方便后期使用。)
为什么在前面学习的单链表(无头单向非循环链表,后来我们都简称为单链表了)中我们不用初始化,双向链表中需要呢?
答案是:前面我们学习的是无哨兵位头的单链表,链表为空时,头指针plist直接指向NULL就行了。现在我们学习的双向链表是带哨兵位头的双向链表,链表为空时,头指针plist不是直接指向NULL的是指向哨兵位的,所以我们需要初始化功能——创建一个哨兵位头结点。
初始化的代码实现:
//双向链表的初始化——创建带哨兵位的头结点
LTNode* LTInit(LTNode* phead)
{
//在堆区申请哨兵位的内存空间
phead = (LTNode*)malloc(sizeof(LTNode));
//判断空间是否开辟成功
if (NULL == phead)
{
//开辟失败,打印错误信息,并退出程序
perror("LTInit::malloc");
exit(-1);
}
//malloc并没有初始化空间,我们自己记得将其初始化
phead->next = phead;
phead->prev = phead;
//形参要改变实参:①传实参的地址;②通过返回值。
//为了接口的一致性,我们选择方式②
return phead;
}
测试代码:
//测试双向链表的初始化
void ListText1()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
}
int main()
{
ListText1();
return 0;
}
F10调试起来,F11逐步调试观察,初始化成功,如图:
解读:
①初始化——头指针要指向哨兵位的头结点,即形参要改变实参。形参要改变实参有两种方式:方式①址传递:传实参的地址,在函数体中通过解引用操作改变实参;②通过返回值。因为双向链表的增删查改的大多数接口传的参数都是一级指针,为了接口的一致性,我们这也传一级指针,所以我们选择通过返回值来改变实参。图示:
②双向链表为空时,只有一个哨兵位的头结点,因为是循环的,所以next和prev储存的地址是它自己。(哨兵位的头结点:数据域不存储数据)
为什么需要将新节点的创建单独成一个接口呢?
答案是:当需要插入新数据的时候,我们都需要创建新节点,所以我们将其封装 成一个单独的模块。
思路:链表的结点是按需提供的,所以我们①malloc在堆区申请;②malloc创建的空间不初始化,我们自己记得初始化;③创建好结点,我们通过返回值的方式拿到新节点。
新节点创建的代码实现:
//创建新节点
LTNode* BuyListNode(LTDataType x)
{
//在堆区申请一个新节点
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
//判断是否开辟成功
if (NULL == newnode)
{
perror("BuyListNode::malloc");
exit(-1);
}
//初始化
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
//返回开辟空间的地址
return newnode;
}
链表不支持随机访问,所以我们只能从头开始,逐个遍历链表的结点。
打印的代码实现:
//双向链表的打印
void LTPrint(LTNode* phead)
{
//phead指向哨兵位的头结点,一定不为空
assert(phead);
//打印哨兵位的头结点
printf("phead<==>");
//逐个打印双向链表的结点
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}//当打印到尾结点结束
//循环结束的时候尾结点指向哨兵位的头结点
printf("phead\n");
}
测试代码:
//测试双向链表的打印
void ListText2()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//打印空链表
LTPrint(plist);
}
int main()
{
ListText2();
return 0;
}
运行结果:
解读:
①从头开始逐个打印链表的结点,所以传链表的头指针就可以了。
②哨兵位的头结点数据域是不存储有效数据的,所以真正的遍历是从phead->next(哨兵位的下一个),遍历到尾结点结束——即循环条件cur != phead。
③循环条件为什么是cur != phead?
答案是:双向链表是循环的,循环——①尾结点的next指向哨兵位的头结点;②哨兵位的头结点的prev指向尾结点。
尾插:①创建一个新节点;②找到链表的尾结点;③新节点与链表链接。
尾插的代码实现:
//双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
//phead一定不为空,因为它指向哨兵位
assert(phead);
//创建新节点
LTNode* newnode = BuyListNode(x);
//找到尾结点:phead->prev
/*
//法1:不保存,即通过phead->prev找尾结点,这个时候需要注意链接顺序
// phead phead->prev newnode
//1、新节点先与尾结点链接
phead->prev->next = newnode;
newnode->prev = phead->prev;
//2、新节点与哨兵位的头结点链接
phead->prev = newnode;
newnode->next = phead;
*/
//法2:保存尾结点tail = phead->prev,即可通过指针tail找尾结点,这个时候不需要注意链接顺序
//phead tail newnode
LTNode* tail = phead->prev;
//tail newnode
tail->next = newnode;
newnode->prev = tail;
//newnode phead
newnode->next = phead;
phead->prev = newnode;
}
测试代码:
//测试双向链表的尾插
void ListText3()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//尾插:1 2 3
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
//打印
LTPrint(plist);
}
int main()
{
ListText3();
return 0;
}
运行结果:
解读:
①为什么在前面我们学习单链表的时候,这要传二级指针,我们这里只需要传一级指针既可以了?
答案是:前面我们学的单链表是无哨兵位的头的,当链表为空的时候,尾插要改变头指针,形参的改变要影响实参,所以我们传的是二级指针。现在我们学的双向链表是带哨兵位的头的,链表为空时,哨兵位的头也还是存在的,即头指针一直指向哨兵位的头,所以形参的改变不影响实参,所以我们传一级指针即可。如图。
②为了程序的健壮性,我们断言双向链表的phead不为空,因为phead一直指向哨兵位的头结点。
③找尾结点:因为双向链表是循环的——循环:①尾结点的next指向哨兵位的头结点;②哨兵位的头结点的prev指向尾结点,所以我们不需要遍历链表,直接通过头结点的prev得到尾结点。
④链接:方式1:不保存尾结点,通过phead->prev找尾,但是此方式需要注意链接顺序;方式2:定义一个局部变量指针tail保存尾结点,虽然此方式多了一个变量,但是我们不需要注意链接顺序了。(推荐方式2,不易出错!)如图:
⑤对比我们前面学习的单链表,双向链表虽然结构复杂,但是代码实现简单。
头插:①创建一个新节点;②在哨兵位的头的next的前面插入;③链接。
头插的代码实现:
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//创建新节点
LTNode* newnode = BuyListNode(x);
//找到哨兵位的头结点的后继结点:phead->next
/*
//法1:不保存哨兵位的后继结点,注意链接顺序
//phead newnode phead->next
//1、新节点先与哨兵位的后继结点链接
//newnode phead->next
newnode->next = phead->next;
phead->next->prev = newnode;
//2、新节点再与哨兵位的头结点链接
//phead newnode
phead->next = newnode;
newnode->prev = phead;
*/
//法2:保存哨兵位的后继结点,first = phead->next,优点:不用注意链接顺序,不易出错
//phead newnode first
LTNode* first = phead->next;
//phead newnode
phead->next = newnode;
newnode->prev = phead;
//newnode first
newnode->next = first;
first->prev = newnode;
}
运行结果:
解读:
①双向链表:头指针一直指向带哨兵位的头,头插是在哨兵位的后面插入,所以头插不改变头指针,值传递即可。
②链接:方式1:不保存哨兵位的后继结点,需注意链接顺序;方式2:保存哨兵位的后继结点,不需要注意链接顺序(推荐方式2:不易出错!),如图:
尾删:①断言链表不为空;②找到链表的尾结点和尾结点的前驱结点;③将链表原尾结点的前驱结点变成新的尾结点(即将原尾结点的前驱结点和哨兵位的头链接)并free掉原尾结点
尾删的代码实现:
//双向链表的尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//断言链表不为空,即phead->next != phead
assert(phead->next != phead);
//找到尾结点:tail = phead->prev和尾结点的前驱结点: tail->prev
LTNode* tail = phead->prev;
/*
// 方式1:不保存尾结点的前驱结点,通过尾结点来找,需要注意必须先链接再释放
//phead tail->prev tail
//先链接:phead tail->prev
tail->prev->next = phead;
phead->prev = tail->prev;
//后释放:tail
free(tail);
tail = NULL;//其实没有必要的,因为tail是局部变量
*/
//方式2:保存尾结点的前驱结点,这个时候就不用担心先释放之后找不到尾结点的前驱结点,
// 所以不需要注意先链接再释放的问题,不易出错
LTNode* tailPrev = tail->prev;
//phead tailPrev tail(三者独立)
free(tail);
phead->prev = tailPrev;
tailPrev->next = phead;
}
测试代码:
//测试双向链表的头插和尾删
void ListText4()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//尾删
LTPopBack(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
}
int main()
{
ListText4();
return 0;
}
运行结果:
解读:
①删除就相当于消费,消费要有钱,删除链表要有数据,所以删除一个结点前,我们要判断链表是否为空,双向链表为空时,只有一个哨兵位。所以当哨兵位的next == phead时,即表示链表为空。(assert断言:是当表达式为假时断言,为真不断言)我们一般喜欢用assert来判断一些简单的逻辑判断,因为assert直接结束程序,并报出错误行。
②找尾结点:因为双向链表是循环的,所以我们可以直接通过哨兵位的头结点的prev找到尾结点tail。
③为什么要找尾结点的前驱结点呢?
答案是:要删除原尾结点,不仅仅删除就可以了,要将链表的原尾结点的前驱结点变成新的尾结点。
④将链表的原尾结点的前驱结点变成新的尾结点和释放原尾结点,有两种方式完成。方式1:不保存原尾结点的前驱结点,通过原尾结点来找,这种方式需要注意先链接再释放,因为先释放了原尾结点就不能再通过原尾结点找到它的前驱结点了。方式2:保存原尾结点的前驱结点,这个时候就可以不通过原尾结点来找了,所以就不用注意链接和释放的顺序。(推荐方式2,不易出错)
头删:①断言链表不为空;②找到第一个有效数据结点和第一个有效数据结点的后继结点;③free第一个有效数据结点和创建链表的新链接(即第一个有效数据结点的后继结点变成链表的第一个有效数据结点)
头插的代码实现:
//双向链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//链表不为空
//找到第一个有效数据的结点:即哨兵位的后继结点phead->next
LTNode* first = phead->next;
//找到first后继结点:first->next
/*
//方式1:不保存第一个有效数据的结点的后继结点,通过first找到,需注意先链接再释放
//phead first first->next
//先链接:phead first->next
phead->next = first->next;
first->prev = phead;
//后释放:first
free(first);
*/
//方式2:保存第一个有效数据的结点的后继结点,不需要再通过first了,所以不需要注意链接和释放的顺序
LTNode* firstNext = first->next;
//phead first firstNext
free(first);
phead->next = firstNext;
firstNext->prev = phead;
}
测试代码:
//测试双向链表的头删
void ListText5()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//头删
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);
}
int main()
{
ListText5();
return 0;
}
运行结果:
解读:
①哨兵位的数据域不存放有效数据,所以哨兵位的后继结点才是第一个有效数据的结点。
②找到first = phead->next的后继结点,有两种处理方式。方式1:不保存first的后继结点,通过first找,需要注意必须先链接后释放。方式2:保存first的后继结点,可以不通过first找,所以不需要注意链接和释放的顺序了。
③链表我们可以通过草图来更好的观察链接和释放顺序。
方式1的草图:phead first first->next(先释放first,就找不到first的后继结点了)
方式2的草图:phead first firstNext(三者独立)
查找(修改):①从有效数据的第一个结点开始遍历;②当链表结点数据等于查找数据,即找到返回结点地址;③遍历结束,没有找到返回NULL.
查找(修改)代码实现:
//双向链表的查找(修改)
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
//哨兵位的后继结点才是链表的第一个有效数据结点
LTNode* cur = phead->next;
//遍历链表查找
while (cur != phead)
{
if (x == cur->data)
{
return cur;
}//找到返回结点地址
cur = cur->next;//找不到,向后迭代
}
//遍历结束,没找到返回NULL
return NULL;
}
测试代码:
//测试双向链表的查找(修改)
void ListText6()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//查找有效数据是2的结点,并修改为5
LTNode* ret = LTFind(plist,2);
if (ret)
{
ret->data = 5;
}
LTPrint(plist);
}
int main()
{
ListText6();
return 0;
}
运行结果:
思路:①创建一个新节点;②找到pos的前驱结点;②新节点与链表链接。
pos位置之前插入的代码实现:
//双向链表pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
//找到pos的前驱结点:pos->prev
/*
//方式1:不保存pos的前驱结点,通过pos找,需要注意链接顺序
//pos->prev newnode pos
//1、pos的前驱结点先与新节点链接
//pos->prev newnode
pos->prev->next = newnode;
newnode->prev = pos->prev;
//2、新节点与pos链接
//newnode pos
newnode->next = pos;
pos->prev = newnode;
*/
//方式2:保存pos的前驱结点,不通过pos找,就不需要注意链接顺序了
LTNode* posPrev = pos->prev;
//posPrev newnode pos
//newnode pos
newnode->next = pos;
pos->prev = newnode;
//posPrev newnode
posPrev->next = newnode;
newnode->prev = posPrev;
}
测试代码:
//测试双向链表的pos位置之前插入
void ListText7()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//尾插:4 5 6
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPushBack(plist, 6);
//打印
LTPrint(plist);
//查找有效数据是2的结点,在其前面插入5
LTNode* ret = LTFind(plist, 2);
LTInsert(ret, 5);
LTPrint(plist);
}
int main()
{
ListText7();
return 0;
}
运行结果:
解读:
①双向链表的结点——任意一个结点都有其前驱和后继的地址,所以我们不用传头指针了,知道pos的位置就知道它的前驱结点了。
②为了程序的健壮性,pos指向一个结点,所以一定不为空,我们将其断言。
③找到pos的前驱结点,我们有两种处理方式。方式1:不保存pos前驱结点的位置,通过pos找,这种方式需要注意链接顺序。方式2:保存pos前驱结点的位置,不通过pos找,所以不用注意链接顺序。
④该功能模块还可以在尾插&头插复用:
尾插的复用:
//双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//复用
LTInsert(phead, x);
}
解读:在头前面插入,即相当于尾插。(循环:哨兵位的prev指向尾结点)
头插的复用:
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//复用
LTInsert(phead->next, x);
}
解读:在phead->next之前插入,即是头插。
pos位置删除:①pos不为空;②找到pos的前驱结点和后继结点;③free掉pos并创建新的链接。
pos位置删除代码实现:
//双向链表删除pos位置的结点
void LTErase(LTNode* pos)
{
assert(pos);
//找到pos的前驱结点pos->prev和pos的后继结点pos->next
/*
//方式1:不保存pos的前驱结点和后继结点,通过pos找,需注意先链接再释放
//pos->prev pos pos->next
//先链接:pos->prev pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
//后释放:pos
free(pos);
*/
//方式2:保存pos的前驱结点和后继结点,就可以不通过pos找,所以链接和释放的顺序就没有要求了
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
//posPrev pos posNext(三者独立)
//先释放:pos
free(pos);
//后连接:posPrev posNext
posPrev->next = posNext;
posNext->prev = posPrev;
}
测试代码:
//测试双向链表的pos位置删除
void ListText8()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//头删
LTPopFront(plist);
//尾删
LTPopBack(plist);
//删除2的结点
LTNode* ret = LTFind(plist, 2);
LTErase(ret);
LTPrint(plist);
}
int main()
{
ListText8();
return 0;
}
运行结果:
解读:
①不断言链表是否为空,可能会误删掉哨兵位,这是C语言的缺陷。因为断言的话,就还需要传链表的头指针,没有必要。
②找到pos的前驱结点和后继结点有两种处理方式。方式1:通过pos来找,但是需要注意必须先将pos的前驱结点和后继结点链接成功后,才能释放pos。方式2:保存pos的前驱结点和后继结点,不通过pos找了,所以就不用注意链接和释放的顺序了。
③该功能模块可以在头删&尾删复用:
头删的复用:
//双向链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
//复用
LTErase(phead->next);
}
解读:头删,即我们将phead->next传过去。
尾删的复用:
//双向链表的尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//复用
LTErase(phead->prev);
}
解读:尾删,我们找到尾的位置传过去即可。(循环:哨兵位的prev指向尾结点)
销毁:①先删除完链表中的所有储存有效数据的结点;②再删除哨兵位的头结点。
注意:链表的销毁哨兵位也free了,所以头指针要改变,但是为了接口的一致性,我们是值传递,所以记得注释在主调函数将头指针指向NULL。
销毁的代码实现:
//双向链表销毁(为了保持接口的一致性,我们是值传递,记得free之后,在主调函数将头指针置为空)
void LTDestroy(LTNode* phead)
{
assert(phead);
//1、先删除链表中的所有储存有效数据的结点
LTNode* cur = phead->next;//第一个有效数据结点
while (cur != phead)
{
LTNode* next = cur->next;//保存下一个有效数据结点
free(cur);
cur = next;//向后迭代
}//遍历删除
//2、最后删除哨兵位
free(phead);
//phead = NULL;//值传递,形参的改变并没有影响实参
}
测试代码:
//测试双向链表销毁
void ListText9()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//销毁
LTDestroy(plist);
plist = NULL;//头指针置为空
}
int main()
{
ListText9();
return 0;
}
解读:
①因为双向循环链表遍历结束的条件是cur != phead;所以我们最后删除哨兵位的头结点。
②我们可以通过F10调试起来,观察是否真正的销毁成功。图示:
我们将双向链表模块化,分成三个文件:
①List.c —— 双向链表接口实现模块
②List.h —— 双向链表接口声明模块
③Test.c —— 双向链表接口测试模块
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//双向链表的初始化——创建带哨兵位的头结点
LTNode* LTInit(LTNode* phead)
{
//在堆区申请哨兵位的内存空间
phead = (LTNode*)malloc(sizeof(LTNode));
//判断空间是否开辟成功
if (NULL == phead)
{
//开辟失败,打印错误信息,并退出程序
perror("LTInit::malloc");
exit(-1);
}
//malloc并没有初始化空间,我们自己记得将其初始化
phead->next = phead;
phead->prev = phead;
//形参要改变实参:①传实参的地址;②通过返回值。
//为了接口的一致性,我们选择方式②
return phead;
}
//创建新节点
LTNode* BuyListNode(LTDataType x)
{
//在堆区申请一个新节点
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
//判断是否开辟成功
if (NULL == newnode)
{
perror("BuyListNode::malloc");
exit(-1);
}
//初始化
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
//返回开辟空间的地址
return newnode;
}
//双向链表的打印
void LTPrint(LTNode* phead)
{
//phead指向哨兵位的头结点,一定不为空
assert(phead);
//打印哨兵位的头结点
printf("phead<==>");
//逐个打印双向链表的结点
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}//当打印到尾结点结束
//循环结束的时候尾结点指向哨兵位的头结点
printf("phead\n");
}
双向链表的尾插
//void LTPushBack(LTNode* phead, LTDataType x)
//{
// //phead一定不为空,因为它指向哨兵位
// assert(phead);
// //创建新节点
// LTNode* newnode = BuyListNode(x);
// //找到尾结点:phead->prev
// /*
// //法1:不保存,即通过phead->prev找尾结点,这个时候需要注意链接顺序
// // phead phead->prev newnode
// //1、新节点先与尾结点链接
// phead->prev->next = newnode;
// newnode->prev = phead->prev;
// //2、新节点与哨兵位的头结点链接
// phead->prev = newnode;
// newnode->next = phead;
// */
//
// //法2:保存尾结点tail = phead->prev,即可通过指针tail找尾结点,这个时候不需要注意链接顺序
// //phead tail newnode
// LTNode* tail = phead->prev;
// //tail newnode
// tail->next = newnode;
// newnode->prev = tail;
// //newnode phead
// newnode->next = phead;
// phead->prev = newnode;
//}
//双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//复用
LTInsert(phead, x);
}
//双向链表的头插
//void LTPushFront(LTNode* phead, LTDataType x)
//{
// assert(phead);
// //创建新节点
// LTNode* newnode = BuyListNode(x);
// //找到哨兵位的头结点的后继结点:phead->next
// /*
// //法1:不保存哨兵位的后继结点,注意链接顺序
// //phead newnode phead->next
// //1、新节点先与哨兵位的后继结点链接
// //newnode phead->next
// newnode->next = phead->next;
// phead->next->prev = newnode;
// //2、新节点再与哨兵位的头结点链接
// //phead newnode
// phead->next = newnode;
// newnode->prev = phead;
// */
//
// //法2:保存哨兵位的后继结点,first = phead->next,优点:不用注意链接顺序,不易出错
// //phead newnode first
// LTNode* first = phead->next;
// //phead newnode
// phead->next = newnode;
// newnode->prev = phead;
// //newnode first
// newnode->next = first;
// first->prev = newnode;
//}
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//复用
LTInsert(phead->next, x);
}
双向链表的尾删
//void LTPopBack(LTNode* phead)
//{
// assert(phead);
// //断言链表不为空,即phead->next != phead
// assert(phead->next != phead);
// //找到尾结点:tail = phead->prev和尾结点的前驱结点: tail->prev
// LTNode* tail = phead->prev;
// /*
// // 方式1:不保存尾结点的前驱结点,通过尾结点来找,需要注意必须先链接再释放
// //phead tail->prev tail
// //先链接:phead tail->prev
// tail->prev->next = phead;
// phead->prev = tail->prev;
// //后释放:tail
// free(tail);
// tail = NULL;//其实没有必要的,因为tail是局部变量
// */
//
// //方式2:保存尾结点的前驱结点,这个时候就不用担心先释放之后找不到尾结点的前驱结点,
// // 所以不需要注意先链接再释放的问题,不易出错
// LTNode* tailPrev = tail->prev;
// //phead tailPrev tail(三者独立)
// free(tail);
// phead->prev = tailPrev;
// tailPrev->next = phead;
//}
//双向链表的尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//复用
LTErase(phead->prev);
}
双向链表的头删
//void LTPopFront(LTNode* phead)
//{
// assert(phead);
// assert(phead->next != phead);//链表不为空
// //找到第一个有效数据的结点:即哨兵位的后继结点phead->next
// LTNode* first = phead->next;
// //找到first后继结点:first->next
// /*
// //方式1:不保存第一个有效数据的结点的后继结点,通过first找到,需注意先链接再释放
// //phead first first->next
// //先链接:phead first->next
// phead->next = first->next;
// first->prev = phead;
// //后释放:first
// free(first);
// */
//
// //方式2:保存第一个有效数据的结点的后继结点,不需要再通过first了,所以不需要注意链接和释放的顺序
// LTNode* firstNext = first->next;
// //phead first firstNext
// free(first);
// phead->next = firstNext;
// firstNext->prev = phead;
//}
//双向链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
//复用
LTErase(phead->next);
}
//双向链表的查找(修改)
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
//哨兵位的后继结点才是链表的第一个有效数据结点
LTNode* cur = phead->next;
//遍历链表查找
while (cur != phead)
{
if (x == cur->data)
{
return cur;
}//找到返回结点地址
cur = cur->next;//找不到,向后迭代
}
//遍历结束,没找到返回NULL
return NULL;
}
//双向链表pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
//找到pos的前驱结点:pos->prev
/*
//方式1:不保存pos的前驱结点,通过pos找,需要注意链接顺序
//pos->prev newnode pos
//1、pos的前驱结点先与新节点链接
//pos->prev newnode
pos->prev->next = newnode;
newnode->prev = pos->prev;
//2、新节点与pos链接
//newnode pos
newnode->next = pos;
pos->prev = newnode;
*/
//方式2:保存pos的前驱结点,不通过pos找,就不需要注意链接顺序了
LTNode* posPrev = pos->prev;
//posPrev newnode pos
//newnode pos
newnode->next = pos;
pos->prev = newnode;
//posPrev newnode
posPrev->next = newnode;
newnode->prev = posPrev;
}
//双向链表删除pos位置的结点
void LTErase(LTNode* pos)
{
assert(pos);
//找到pos的前驱结点pos->prev和pos的后继结点pos->next
/*
//方式1:不保存pos的前驱结点和后继结点,通过pos找,需注意先链接再释放
//pos->prev pos pos->next
//先链接:pos->prev pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
//后释放:pos
free(pos);
*/
//方式2:保存pos的前驱结点和后继结点,就可以不通过pos找,所以链接和释放的顺序就没有要求了
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
//posPrev pos posNext(三者独立)
//先释放:pos
free(pos);
//后连接:posPrev posNext
posPrev->next = posNext;
posNext->prev = posPrev;
}
//双向链表销毁(为了保持接口的一致性,我们是值传递,记得free之后,在主调函数将头指针置为空)
void LTDestroy(LTNode* phead)
{
assert(phead);
//1、先删除链表中的所有储存有效数据的结点
LTNode* cur = phead->next;//第一个有效数据结点
while (cur != phead)
{
LTNode* next = cur->next;//保存下一个有效数据结点
free(cur);
cur = next;//向后迭代
}//遍历删除
//2、最后删除哨兵位
free(phead);
//phead = NULL;//值传递,形参的改变并没有影响实参
}
#pragma once
//带头+双向+循环链表增删查改实现
//常用头文件
#include
#include
#include
#include
//双向链表数据类型重命名:①见名知意;②后期方便更改
typedef int LTDataType;
//双向链表结点的定义
typedef struct ListNode
{
LTDataType data;//存储链表数据
struct ListNode* prev;//存储结点的前驱结点地址
struct ListNode* next;//存储结点的后继结点地址
}LTNode;
//双向链表的初始化——创建带哨兵位的头结点
LTNode* LTInit(LTNode* phead);
//创建新节点
LTNode* BuyListNode(LTDataType x);
//双向链表的打印
void LTPrint(LTNode* phead);
//双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x);
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x);
//双向链表的尾删
void LTPopBack(LTNode* phead);
//双向链表的头删
void LTPopFront(LTNode* phead);
//双向链表的查找(修改)
LTNode* LTFind(LTNode* phead, LTDataType x);
//双向链表pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
//双向链表删除pos位置的结点
void LTErase(LTNode* pos);
//双向链表销毁
void LTDestroy(LTNode* phead);
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//测试双向链表的初始化
void ListText1()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
}
//测试双向链表的打印
void ListText2()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//打印空链表
LTPrint(plist);
}
//测试双向链表的尾插
void ListText3()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//尾插:1 2 3
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
//打印
LTPrint(plist);
}
//测试双向链表的头插和尾删
void ListText4()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//尾删
LTPopBack(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
}
//测试双向链表的头删
void ListText5()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//头删
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);
}
//测试双向链表的查找(修改)
void ListText6()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//查找有效数据是2的结点,并修改为5
LTNode* ret = LTFind(plist,2);
if (ret)
{
ret->data = 5;
}
LTPrint(plist);
}
//测试双向链表的pos位置之前插入
void ListText7()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//尾插:4 5 6
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPushBack(plist, 6);
//打印
LTPrint(plist);
//查找有效数据是2的结点,在其前面插入5
LTNode* ret = LTFind(plist, 2);
LTInsert(ret, 5);
LTPrint(plist);
}
//测试双向链表的pos位置删除
void ListText8()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//打印
LTPrint(plist);
//头删
LTPopFront(plist);
//尾删
LTPopBack(plist);
//删除2的结点
LTNode* ret = LTFind(plist, 2);
LTErase(ret);
LTPrint(plist);
}
//测试双向链表销毁
void ListText9()
{
//头指针
LTNode* plist = NULL;
//初始化
plist = LTInit(plist);
//头插:1 2 3
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
//销毁
LTDestroy(plist);
plist = NULL;//头指针置为空
}
int main()
{
ListText9();
return 0;
}
(1) 双向链表结构复杂,但是代码实现简单。为什么呢?
答案是:结构的优势——①链表的任意一个结点都有其前驱结点&后继结点的位置;②带哨兵位的头结点:不用考虑头插&尾插是头指针是否改变,因为头指针一直指向哨兵位;③循环:尾结点的next指向哨兵位的头结点,哨兵位的头结点的prev指向尾结点等等。
(2)能不能在快速写一个链表出来?
答案是:能,当我们快速实现pos位置之前插入&pos位置删除两个接口,头插&尾插与尾删&头删就可以复用就两个接口快速实现。
链表与顺序表优缺点互补的。如下表格:
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理&逻辑都一定连续 | 逻辑连续,物理不一定连续 |
随机访问 | 支持O(1) | 不支持O(N) |
任意位置插入或删除 | 可能需要挪动元素,时间效率低。O(N) | 只需要改变指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念,按需提供 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
CPU高速缓存命中率 | 高(物理地址是连续的) | 低(物理地址不一定连续) |
了解:缓存命中率参考存储体系结构