0.前言
hello 大家好啊,好久不见。最近几天偷懒了,去补学校的课了,跟不上了/(ㄒoㄒ)/~~
今天复习的是双向带头循环链表。
🐱🐱🐱
话不多说进入正题。
1.双向带头循环链表🐺
**乍一看,双向带头循环链表怎么这么复杂啊?**实现起来一定巨麻烦吧?非也非也。
其结构虽然复杂,但是操作反而简单,这也正是其结构优势。
头结点(也叫哨兵位节点)是多开辟的节点,方便链表的一系列操作。
**注意:**有些书上也许会说让头结点存储链表的节点个数,看起来完美的利用了头结点是不是?
非也非也。如果链表元素类型是char呢?链表超过一定长度之后,头结点存储的数据就不准确了。书上这么写是偏理论的,没考虑到实际工程需要。
如果数据类型是double呢?如果是指针呢?
那显然是不合适的。
链表定义
typedef struct ListNode
{
struct ListNode* next;//指向后面节点
struct ListNode* prev;//指向前面节点
LTDataType data;//存储数据
}ListNode;
话不多说,接下来看链表的代码实现吧:
👇👇👇
2.代码实现🐕
2.1初始化1
双向带头循环链表有2种初始化化方式,主要区别在于是否传参以及是否有返回值。
这种方式的初始化,初始化之后需要返回创建的头结点,但也不需要传参了。
创建的头结点如图所示。
注意:OJ的链表一般都是这样的,最后传一个头结点出去。
ListNode *ListInit()
{
ListNode *pHead = CreatListNode(0); //任意一个值都行
//指向自己,方便插入第一个节点
pHead->next = pHead;
pHead->prev = pHead;
return pHead;
}
2.2初始化2
要修改哨兵位的值,注意传二级指针或者传引用
void ListInit2(ListNode *&pHead)
{
//要修改哨兵位头结点,因此传二级指针或者传引用
assert(pHead);
pHead = CreatListNode(0);
pHead->next = pHead;
pHead->prev = pHead;
}
**注意:**初始化方式不同,测试代码也要相应改变。
ListNode *pList;
ListInit2(pList);
ListNode *pList = ListInit();
2.3创建节点
由于链表的增删查改涉及到创建节点的问题,因此,为了简化代码,我们把创建节点的函数单独写出来,要用到的时候就去调用。
ListNode *CreatListNode(LTDataType x)
{
ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
assert(newNode); //暴力判空
// if (node == NULL) //温柔判空
// {
// printf("CreatListNode Fail\n");
// exit(-1);
// }
newNode->data = x;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
2.4打印
为了更加直观得查看数据,我们写一个打印函数。
链表为空时,只有头结点,pHead->next就是自己,自己==自己,不进循环,就只打印头即可。
void ListPrint(ListNode *pHead)
{
assert(pHead);
ListNode *cur = pHead->next;
printf("Head ");
//cur走到pHead时就结束
while (cur != pHead)
{
printf("<-> %d ", cur->data);
cur = cur->next;
}
printf("<-> Head\n");
}
效果展示:
2.5尾插
万事俱备,那我们就先实现一个最简单的尾插把。
双向带头循环链表的尾插是非常高效的,时间复杂度O(1)。
实现代码时,要先画图,想清楚极端情况,再去写代码。
考虑链表为空,也就是只有一个头结点的情况,也是适用的。
要改变哨兵位这个结构体,传的是结构体的地址(指针)。
无需改变实参pList,因此无需传二级指针或传引用。
这是哨兵位头结点的功劳,如果单链表也带上哨兵位头结点,那么也可以用一级指针解决。
void ListPushBack(ListNode *pHead, LTDataType x)
{
assert(pHead);
ListNode *tail = pHead->prev;
ListNode *newNode = CreatListNode(x);
tail->next = newNode;
newNode->prev = tail;
newNode->next = pHead;
pHead->prev = newN ode;
}
复用插入函数的写法。
这样复用了我们后面写的插入函数,如果不懂且往后看。👇👇👇
void ListPushBack(ListNode* pHead, LTDataType x)
{
ListInsert(pHead, x);
//ListInsert是在pos前面的位置插入
}
//尾插其实相当于在pHead前面插
2.6头插
既然有尾插,那再来一个头插嘛。有头有尾,善始善终。(* ^ _ ^ *)
void ListPushFront(ListNode *pHead, LTDataType x)
{
assert(pHead);
//提前保存pHead的下一个节点比较方便
ListNode *first = pHead->next;
ListNode *newNode = CreatListNode(x);
// pHead newNode first
// 让pHead的next指向newNode,newNode的prev指向pHead
// newNode的next指向first,first的prev指向newNode
pHead->next = newNode;
newNode->prev = pHead;
newNode->next = first;
first->prev = newNode;
}
复用插入函数的写法。
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
LIstInsert(pHead->next, x);
}
//头插相当于在phead->next的前面去插入
2.7头删
void ListPopFront(ListNode *pHead)
{
assert(pHead);
assert(pHead != pHead->next); //只有一个头节点没法删
//提前保存好要删除的节点
ListNode *toDelete = pHead->next;
ListNode *first = toDelete->next;
// pHead toDelete first
free(toDelete);
toDelete = NULL;
//删到只剩头节点时,满足自己指向自己
pHead->next = first;
first->prev = pHead;
}
复用删除函数的写法。
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);//只剩自己时不能删除
ListErase(pHead->next);
}
2.8尾删
先找到尾节点和倒数第二个节点,然后释放尾节点,再让头结点和倒数第二个节点链接。
考虑删得只剩一个节点的情况(不包括头结点),删完恰好符合头结点的情况,自己指向自己。
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead != pHead->next);//只有一个哨兵位时无法删除
//找尾结点和倒数第二个结点
ListNode* tail = pHead->prev;
ListNode* tailPrev = tail->prev;
free(tail);
tail = NULL;
tailPrev->next = pHead;
pHead->prev = tailPrev;
}
复用删除函数的写法。
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);//只剩一个哨兵位时不能删除
ListErase(pHead->prev);
}
2.9查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
2.10插入🤭
接下来实现的时链表操作的最重要的函数,任意位置插入数据的插入函数。
注意:这里实现的插入为了向标准看齐,是在pos前面进行插入的。
注意链接顺序。
如果pos是尾,或者pos是头,也是照样可以的,这里深深体现了双向带头循环的优势。
pos无论是头结点还是尾结点,它的前后均不会为空。
//在pos之前插入
void LIstInsert(ListNode *pos, LTDataType x)
{
assert(pos);
ListNode *newNode = CreatListNode(x);
// prev newNode pos
// 先让pos的prev的next指向newNode
// 再让newNode的prev指向pos的prev
pos->prev->next = newNode;
newNode->prev = pos->prev;
newNode->next = pos;
pos->prev = newNode;
}
第二种写法,提前保存好pos的前一个位置。
个人比较推荐这种写法。
//在pos之前插入
void LIstInsert2(ListNode *pos, LTDataType x)
{
assert(pos);
//提前保存pos的前一个位置
ListNode* posPrev = pos->prev;
ListNode *newNode = CreatListNode(x);
// posPrev newNode pos
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
有了插入函数,我们头插尾插就可以复用插入函数实现了,大大简化代码。
关于头插尾插的复用请看上面👆👆👆
2.11删除👻
删除当前节点。
pos即使头或者尾也没关系,双向带头循环的优势体现。
void ListErase2(ListNode *pos)
{
assert(pos);
// posPrev pos next
ListNode *posPrev = pos->prev, *next = pos->next;
free(pos);
// pos = NULL; // 其实不起作用,需要在外面再手动置空
// 让pos的prev指向next,next的prev指向pos的prev
posPrev->next = next;
next->prev = posPrev;
}
有了删除函数,我们头删尾删就可以复用删除函数实现了,大大简化代码。
关于头删尾删的复用请看上面👆👆👆
关于pos置空的问题
ListErase2 中的 pos置空没起作用。
按理说pos应该置空,但这里置空了也没用,因为这里传的不是二级指针,无法修改实参,形参只是实参的一份临时拷贝。
因此可以传二级指针或者传引用或者在调用函数之后手动置空。
但如果传二级指针接口又不一致了,显得很怪异。
void ListErase(ListNode** ppos);
传引用的写法:
void ListErase(ListNode *&pos)
{
assert(pos);
//提前记录pos的prev
ListNode *posPrev = pos->prev;
ListNode *next = pos->next;
// posPrev pos next
// 让pos的prev指向next,next的prev指向pos的prev
posPrev->next = next;
next->prev = posPrev;
free(pos);
pos = NULL;//传引用,可以直接修改实参的值。
}
通过传引用成功把pos置空了。
**建议:**为了保持接口的一致性,我们最好不要传二级指针或者传引用。调用函数后手动置空,标准也是函数调用后手动置空的,我们要向标准看齐。
关于删除哨兵位
注意,不能删除哨兵位节点,不然会产生访问野指针问题。
ListErase(pList);
ListPrint(pList);
2.12判空
bool ListIsEmpty(ListNode *pHead)
{
assert(pHead);
return pHead->next == pHead ? true : false;
}
2.13计算大小
int ListSize(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
int size = 0;
while (cur != pHead)
{
++size;
cur = cur->next;
}
return size;
}
2.14销毁
// 推荐写法,标准也是函数调用后手动置空的。
void ListDestroy2(ListNode *pHead)
{
assert(pHead);
ListNode *cur = pHead;
while (cur != pHead)
{
ListNode *next = cur->next;
free(cur);
cur = next;
}
free(pHead);//链表的摧毁是要连哨兵位都要摧毁的。
// pHead = NULL; // 不起作用
}
关于free的问题
free一个指针,是把指针指向的那块空间释放,所谓释放,其实就是还给操作系统,但free的那个指针还是指向那块空间的,因此需要把指针置空,不然就会有野指针的问题。
某些编译器是会把(如VS)是会把那块空间置成随机值的,但标准并没有要求是否要置成随机值。
也因指针要置空的问题,修改的是指针本身,而不是指针指向的空间,因此往往需要传二级指针或者传引用。
但标准里面通常也都是设计成free之后,再手动置空。
3.源代码😀
DoubleLinkList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType; // ListDateType
typedef struct ListNode
{
struct ListNode *next;
struct ListNode *prev;
LTDataType data;
} ListNode;
ListNode *ListInit();
void ListInit2(ListNode *&pHead); //第二种初始化方式
void ListPrint(ListNode *pHead);
void ListPushBack(ListNode *pHead, LTDataType x);
void ListPushFront(ListNode *pHead, LTDataType x);
void ListPopBack(ListNode *pHead);
void ListPopFront(ListNode *pHead);
ListNode *ListFind(ListNode *pHead, LTDataType x);
void LIstInsert(ListNode *pos, LTDataType x);
void LIstInsert2(ListNode *pos, LTDataType x);
void ListErase(ListNode *&pos);
void ListErase2(ListNode *pos);
bool ListIsEmpty(ListNode *pHead);
int ListSize(ListNode *pHead);
void ListDestroy2(ListNode *pHead);
void ListDestroy(ListNode *&pHead);
DoubleLinkList.cpp
#include "DoubleLinkList.h"
ListNode *CreatListNode(LTDataType x)
{
ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
assert(newNode); //暴力判空
// if (node == NULL) //温柔判空
// {
// printf("CreatListNode Fail\n");
// exit(-1);
// }
newNode->data = x;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
// 2种初始化方式
//要修改哨兵位头结点,因此传二级指针或者传引用
void ListInit(ListNode *&pHead)
{
assert(pHead);
pHead = CreatListNode(0);
//要指向自己
pHead->next = pHead;
pHead->prev = pHead;
}
ListNode *ListInit()
{
ListNode *pHead = CreatListNode(0); //任意一个值都行
//指向自己,方便插入第一个节点
pHead->next = pHead;
pHead->prev = pHead;
return pHead;
}
void ListPushBack(ListNode *pHead, LTDataType x)
{
assert(pHead);
ListNode *tail = pHead->prev;
ListNode *newNode = CreatListNode(x);
tail->next = newNode;
newNode->prev = tail;
newNode->next = pHead;
pHead->prev = newNode;
}
void ListPrint(ListNode *pHead)
{
assert(pHead);
ListNode *cur = pHead->next;
printf("Head ");
// cur走到pHead时就结束
while (cur != pHead)
{
printf("<-> %d ", cur->data);
cur = cur->next;
}
printf("<-> Head\n");
}
void ListPopBack(ListNode *pHead)
{
assert(pHead);
assert(pHead != pHead->next); //只有一个头结点时无法删除
//找尾结点和倒数第二个结点
ListNode *tail = pHead->prev;
ListNode *tailPrev = tail->prev;
free(tail);
tail = NULL;
tailPrev->next = pHead;
pHead->prev = tailPrev;
}
ListNode *ListFind(ListNode *pHead, LTDataType x)
{
assert(pHead);
ListNode *cur = pHead->next;
while (cur != pHead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos之前插入
void LIstInsert(ListNode *pos, LTDataType x)
{
assert(pos);
ListNode *newNode = CreatListNode(x);
// prev newNode pos
// 先让pos的prev的next指向newNode
// 再让newNode的prev指向pos的prev
pos->prev->next = newNode;
newNode->prev = pos->prev;
newNode->next = pos;
pos->prev = newNode;
}
//在pos之前插入
void LIstInsert2(ListNode *pos, LTDataType x)
{
assert(pos);
//提前保存pos的前一个位置
ListNode *posPrev = pos->prev;
ListNode *newNode = CreatListNode(x);
// posPrev newNode pos
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
void ListPushFront(ListNode *pHead, LTDataType x)
{
assert(pHead);
//提前保存pHead的下一个节点比较方便
ListNode *first = pHead->next;
ListNode *newNode = CreatListNode(x);
// pHead newNode first
// 让pHead的next指向newNode,newNode的prev指向pHead
// newNode的next指向first,first的prev指向newNode
pHead->next = newNode;
newNode->prev = pHead;
newNode->next = first;
first->prev = newNode;
}
void ListPopFront(ListNode *pHead)
{
assert(pHead);
assert(pHead != pHead->next); //只有一个头节点没法删
//提前保存好要删除的节点
ListNode *toDelete = pHead->next;
ListNode *first = toDelete->next;
// pHead toDelete first
free(toDelete);
toDelete = NULL;
//删到只剩头节点时,满足自己指向自己
pHead->next = first;
first->prev = pHead;
}
void ListErase2(ListNode *pos)
{
assert(pos);
// posPrev pos next
ListNode *posPrev = pos->prev, *next = pos->next;
free(pos);
// pos = NULL; // 其实不起作用,需要在外面再手动置空
// 让pos的prev指向next,next的prev指向pos的prev
posPrev->next = next;
next->prev = posPrev;
}
// void ListErase2(ListNode *pos)
// {
// assert(pos);
// //提前记录pos的prev
// ListNode *posPrev = pos->prev;
// ListNode *next = pos->next;
// // posPrev pos next
// // 让pos的prev指向next,next的prev指向pos的prev
// posPrev->next = next;
// next->prev = posPrev;
// free(pos);
// pos = NULL;
// }
void ListErase(ListNode *&pos)
{
assert(pos);
//提前记录pos的prev
ListNode *posPrev = pos->prev;
ListNode *next = pos->next;
// posPrev pos next
// 让pos的prev指向next,next的prev指向pos的prev
posPrev->next = next;
next->prev = posPrev;
free(pos);
pos = NULL;
}
int ListSize(ListNode *pHead)
{
assert(pHead);
ListNode *cur = pHead->next;
int size = 0;
while (cur != pHead)
{
++size;
cur = cur->next;
}
return size;
}
bool ListIsEmpty(ListNode *pHead)
{
assert(pHead);
return pHead->next == pHead ? true : false;
}
void ListDestroy2(ListNode *pHead)
{
assert(pHead);
ListNode *cur = pHead;
while (cur != pHead)
{
ListNode *next = cur->next;
free(cur);
cur = next;
}
free(pHead); //链表的摧毁是要连哨兵位都要摧毁的。
// pHead = NULL; // 不起作用
}
//复用Erase,且传引用
// void ListDestroy(ListNode *&pHead)
// {
// assert(pHead);
// ListNode *cur = pHead;
// while (cur != pHead)
// {
// ListNode *next = cur->next;
// ListErase(cur);
// cur = next;
// }
// free(pHead);
// pHead = NULL;
// }
void ListDestroy(ListNode *&pHead)
{
assert(pHead);
ListNode *cur = pHead;
while (cur != pHead)
{
ListNode *next = cur->next;
free(cur);
cur = next;
}
free(pHead);
pHead = NULL;
}
test.cpp
#include "DoubleLinkList.h"
void Test1()
{
ListNode *pList = ListInit();
ListPushBack(pList, 1);
ListPushBack(pList, 2);
ListPushBack(pList, 3);
ListPushBack(pList, 4);
ListPrint(pList);
}
void Test2()
{
ListNode *pList;
ListInit2(pList);
ListPushBack(pList, 1);
ListPushBack(pList, 2);
ListPushBack(pList, 2);
ListPushBack(pList, 4);
ListPrint(pList);
ListPopBack(pList);
ListPrint(pList);
ListPopBack(pList);
ListPrint(pList);
ListPopBack(pList);
ListPrint(pList);
ListPopBack(pList);
ListPrint(pList);
}
void Test3()
{
ListNode *pList = ListInit();
ListPushBack(pList, 1);
ListPushBack(pList, 2);
ListPushBack(pList, 3);
ListPushBack(pList, 4);
ListPrint(pList);
ListNode *pos = ListFind(pList, 3);
if (pos)
{
LIstInsert(pos, 20);
}
ListPrint(pList);
ListNode *pos2 = ListFind(pList, 4);
if (pos2)
{
LIstInsert2(pos2, 40);
}
ListPrint(pList);
}
void Test4()
{
ListNode *pList = ListInit();
ListPushBack(pList, 1);
ListPushBack(pList, 2);
ListPushBack(pList, 3);
ListPushBack(pList, 4);
ListPrint(pList);
ListPushFront(pList, 4);
ListPushFront(pList, 3);
ListPushFront(pList, 2);
ListPushFront(pList, 1);
ListPrint(pList);
ListPopFront(pList);
ListPopFront(pList);
ListPrint(pList);
}
void Test5()
{
ListNode *pList = ListInit();
ListPushBack(pList, 1);
ListPushBack(pList, 2);
ListPushBack(pList, 3);
ListPushBack(pList, 4);
ListPrint(pList);
ListNode *pos = ListFind(pList, 1);
if (pos)
{
ListErase2(pos);
}
ListPrint(pList);
printf("%d\n", ListSize(pList));
printf("%d\n", ListIsEmpty(pList));
ListDestroy(pList);
}
int main(int argc, char const *argv[])
{
Test5();
system("pause");
return 0;
}
4.总结
以一言蔽之,写代码之前,一定要多动手画图,考虑清楚极端情况再去写代码,不要想到一点就直接上手写,尽可能地减少调试的次数。
画图我强烈推荐Windows自带的画图工具,超级好用!!!
5.尾声
🌹🌹🌹
写文不易,如果有帮助烦请点个赞~ 👍👍👍
Thanks♪(・ω・)ノ🌹🌹🌹
😘😘😘
👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接
附联系方式(2076188013)(QQ)