📖 前言:本期我们将讨论链表的另外一种结构————双向链表,相比单链表那可谓是优势多多。
🎓 作者:HinsCoder
📦 作者的GitHub:代码仓库
📌 往期文章&专栏推荐:
🕒 1. 单链表的缺点
上一期我们学习了单链表(单向不带头非循环链表),发现它虽然相比顺序表是有一定的优势的,但是它的缺陷也是不可忽视的。
- 假如找一个数据只能从前向后依次遍历,而且链表中一旦一个结点的地址丢失了,这个结点往后所有的结点都找不到了;
- PopBack(尾删),Insert(任插),Erase(任删)的时间复杂度都是O(N),都是需要从头开始找前一个结点;
- 几乎每个接口都需要判断链表是否为空,比较麻烦。
针对这些缺陷的解决方案是什么呢?————就是双链表(双向带头循环链表)
🕒 2. 链表的分类
链表分别有六个特点:
- 单向、双向
- 带头、不带头
- 循环、非循环
⚡ 注意: 头结点(哨兵位)是不能存储有效数据的
🕘 2.1 带头结点(哨兵位)的优点
不管链表如何,都有一个头结点在那里,但是这个头结点不存储有效数据
,尾插的话直接链接在头结点后面就行,这样我们几乎用不到二级指针了,因为我们并不会改变外面的指针,而是在这个结点后面去链接,哪怕链表为空,所以带头结点的链表尾插判断会更简单。
- 头删也是如此,如果是不带头单链表,删除一个数据的话,需要定义一个
del
结点,让del
指向被删结点,之后将被删结点后一个与头指针链接起来,然后释放del
,由于需要动pHead
,因此需要二级指针。 - 如果是带头结点链表的话,删除第一个结点,然后将头结点链接到第二个结点上即可,无需二级指针
⌛ 小结:
- 带头结点不需要改变传过来的指针,也就意味着不需要传二级指针。
- 头结点是不存储有效数据的,更不能存储链表的长度。
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
- 无头单向非循环链表:
结构简单
,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构
,如哈希桶、图的邻接表等等。另外这种结构在笔试面试
中出现很多。 - 带头双向循环链表:
结构最复杂
,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
🕒 3. 双向带头循环链表的接口实现
🕘 3.1 定义双链表
typedef int DLTDataType;
typedef struct DListNode
{
struct DListNode* next;
struct DListNode* prev;
DLTDataType data;
}DLTNode;
🕘 3.2 初始化双链表
说起初始化,我们应该会马上想到以下代码的传参方式,
void DListInit(DLTNode** pphead);
int main()
{
DLTNode* plist = NULL;
DListInit(&plist);
return 0;
}
我们思考一下,既然带头结点的链表都不需要传二级指针,那么在初始化这块传二级指针岂不是很突兀?我们想想看,有什么不用传二级指针的方式。
想到了,返回值不就行了。
上代码
DLTNode* DListInit()
{
DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
if (guard == NULL)
{
perror("malloc fail");
exit(-1);
}
guard->next = guard; //记得指向自己
guard->prev = guard;
return guard;
}
int main()
{
//DLTNode* plist = NULL;
//DListInit(&plist);
DLTNode* plist = DListInit();
return 0;
}
🕘 3.3 创建新结点
思路:x就是待插入数据,前驱和后驱指针先置为空,方便链接。
DLTNode* BuyDLTNode(DLTDataType x)
{
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->next = NULL; //刚malloc的结点置空就好
node->prev = NULL;
node->data = x;
return node;
}
🕘 3.4 尾插双链表
以前我们的单链表都需要找尾,时间复杂度为O(n),现在带头双链表找尾可谓易如反掌,直接phead->prev
就找到了
思路:如图链接形成循环,不需要判空哦~
void DListPushBack(DLTNode* phead, DLTDataType x)
{
assert(phead);
DLTNode* newnode = BuyDLTNode(x);
DLTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
🕘 3.5 打印双链表
⚡ 注意:因为不像单链表的结束标记是NULL
,因为是循环
结构,所以这里的结束标记为返回到头结点
就结束。
思路:打印是不需要打印phead
的,让cur指向第一个数据的结点,当cur=phead
的时候结束,这样把空链表的情况也考虑到了。
void DListPrint(DLTNode* phead)
{
assert(phead);
printf("phead<=>");
DLTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
🕘 3.6 头插双链表
观察下面的代码,这样写对吗❓
void DListPushFront(DLTNode* phead, DLTDataType x)
{
assert(phead);
DLTNode* newnode = BuyDLTNode(x);
phead->next = newnode;
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
}
这样先写phead->next = newnode;
这句代码就会把后续结点给弄丢了,要注意顺序。
void DListPushFront(DLTNode* phead, DLTDataType x)
{
assert(phead);
// 先链接newnode和phead->next之间的关系
DLTNode* newnode = BuyDLTNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
还有更优的代码,可以不考虑先后顺序
思路:头插也就是在第一个有效数据前,phead
头结点后面插入,那么既然是链表就很容易找到phead
后面的结点,记做first,然后将newnode
和头结点链接上,然后再和first
接上。
void DListPushFront(DLTNode* phead, DLTDataType x)
{
assert(phead);
// 不关心顺序
DLTNode* newnode = BuyDLTNode(x);
DLTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
🕘 3.7 双链表判空
bool DListEmpty(DLTNode* phead)
{
assert(phead);
/*
if (phead->next == phead)
return true;
else
return false;
*/ //比较繁琐
return phead->next == phead; 一步到位
}
🕘 3.8 尾删双链表
思路:尾删和头删也是很相似的,需要两个指针,一个指针指向链表最后一个结点(tail),另外一个指向倒数第二个结点(prev),然后将prev和头结点链接,释放tail。删空的情况也兼顾到了。
void DListPopBack(DLTNode* phead)
{
assert(phead);
assert(!DListEmpty(phead));
DLTNode* tail = phead->prev;
DLTNode* prev = tail->prev;
prev->next = phead;
phead->prev = prev;
free(tail);
tail = NULL;
}
🕘 3.9 头删双链表
思路:定义两个指针,first
指向第一个结点,second
指向第二个结点,先将头结点phead
和第二个数据second
链接,然后释放first
。
void DListPopFront(DLTNode* phead)
{
assert(phead);
assert(!DListEmpty(phead));
DLTNode* first = phead->next;
DLTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
}
🕘 3.10 统计双链表有效结点个数
思路:遍历统计
⚡ 注意:有些书可能会这么写,在头结点存入结点个数,在类型int
情况下没有问题,可一旦用了不是int
类型来定义头结点,那就有溢出风险了。
size_t DListSize(DLTNode* phead)
{
assert(phead);
size_t n = 0;
DLTNode* cur = phead->next;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
🕘 3.11 查找双链表结点
思路:遍历查找
DLTNode* DListFind(DLTNode* phead, DLTDataType x)
{
assert(phead);
size_t n = 0;
DLTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
🕘 3.12 任意位置前插入结点
思路:先通过Find
查找函数找到pos
,并返回的指针,然后找到pos
的前一个结点(prev
),下一步开始链接。
void DListInsert(DLTNode* pos, DLTDataType x)
{
assert(pos);
DLTNode* prev = pos->prev;
DLTNode* newnode = BuyDLTNode(x);
// prev newnode pos;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
🕤 3.12.1 【复用】尾插双链表
void DListPushBack(DLTNode* phead, DLTDataType x)
{
assert(phead);
DListInsert(phead, x);
}
🕤 3.12.2 【复用】头插双链表
void DListPushFront(DLTNode* phead, DLTDataType x)
{
assert(phead);
DListInsert(phead, x);
}
🕘 3.13 任意位置删除结点
思路:用Find
返回pos
的指针,然后对pos
的前后进行标记,分别是prev
和next
,链接prev
和next
,释放掉pos
即可。
void DListErase(DLTNode* pos)
{
assert(pos);
DLTNode* prev = pos->prev;
DLTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
🕤 3.13.1 【复用】尾删双链表
void DListPopBack(DLTNode* phead)
{
assert(phead);
DListErase(phead->prev);
}
🕤 3.13.2 【复用】头删双链表
void DListPopFront(DLTNode* phead)
{
assert(phead);
assert(!DListEmpty(phead));
DListErase(phead->next);
}
🕘 3.13 销毁双链表
思路:
- 可以传二级,内部置空头结点
- 也可以考虑用一级指针,让调用DListDestroy的人置空(保持接口的一致性)
void DListDestroy(DLTNode* phead)
{
assert(phead);
DLTNode* cur = phead->next;
while (cur != phead)
{
DLTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
🕒 4. 完整源码
// DList.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int DLTDataType;
typedef struct DListNode
{
struct DListNode* next;
struct DListNode* prev;
DLTDataType data;
}DLTNode;
//void DListInit(DLTNode** pphead);
DLTNode* DListInit();
void DListDestroy(DLTNode* phead);
void DListPrint(DLTNode* phead);
void DListPushBack(DLTNode* phead,DLTDataType x);
void DListPushFront(DLTNode* phead, DLTDataType x);
void DListPopBack(DLTNode* phead);
void DListPopFront(DLTNode* phead);
bool DListEmpty(DLTNode* phead);
size_t DListSize(DLTNode* phead);
DLTNode* DListFind(DLTNode* phead,DLTDataType x);
// 在pos之前插入
void DListInsert(DLTNode* pos, DLTDataType x);
// 删除pos位置
void DListErase(DLTNode* pos);
// DList.c
#include"DList.h"
DLTNode* DListInit()
{
DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
if (guard == NULL)
{
perror("malloc fail");
exit(-1);
}
guard->next = guard; //记得指向自己
guard->prev = guard;
return guard;
}
DLTNode* BuyDLTNode(DLTDataType x)
{
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->next = NULL; //刚malloc的结点置空就好
node->prev = NULL;
node->data = x;
return node;
}
void DListPrint(DLTNode* phead)
{
assert(phead);
printf("phead<=>");
DLTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
void DListPushBack(DLTNode* phead, DLTDataType x)
{
assert(phead);
DLTNode* newnode = BuyDLTNode(x);
DLTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
//DListInsert(phead, x);
}
void DListPushFront(DLTNode* phead, DLTDataType x)
{
assert(phead);
/*
DLTNode* newnode = BuyDLTNode(x);
phead->next = newnode;
newnode->prev = phead;
//注意顺序
newnode->next = phead->next;
phead->next->prev = newnode;
*/
/* 先链接newnode和phead->next之间的关系
DLTNode* newnode = BuyDLTNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
//正确,但又更优写法
phead->next = newnode;
newnod e->prev = phead;
*/
// 不关心顺序
DLTNode* newnode = BuyDLTNode(x);
DLTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
//DListInsert(phead->next, x);
}
void DListPopBack(DLTNode* phead)
{
assert(phead);
assert(!DListEmpty(phead));
DLTNode* tail = phead->prev;
DLTNode* prev = tail->prev;
prev->next = phead;
phead->prev = prev;
free(tail);
tail = NULL;
//DListErase(phead->prev);
}
void DListPopFront(DLTNode* phead)
{
assert(phead);
assert(!DListEmpty(phead));
DLTNode* first = phead->next;
DLTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
//DListErase(phead->next);
}
bool DListEmpty(DLTNode* phead)
{
assert(phead);
/*
if (phead->next == phead)
return true;
else
return false;
*/
return phead->next == phead;
}
size_t DListSize(DLTNode* phead)
{
assert(phead);
size_t n = 0;
DLTNode* cur = phead->next;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
DLTNode* DListFind(DLTNode* phead, DLTDataType x)
{
assert(phead);
size_t n = 0;
DLTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void DListInsert(DLTNode* pos, DLTDataType x)
{
assert(pos);
DLTNode* prev = pos->prev;
DLTNode* newnode = BuyDLTNode(x);
// prev newnode pos;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void DListErase(DLTNode* pos)
{
assert(pos);
DLTNode* prev = pos->prev;
DLTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
//pos = NULL;
}
// 可以传二级,内部置空头结点
// 也可以考虑用一级指针,让调用DListDestroy的人置空(保持接口的一致性)
void DListDestroy(DLTNode* phead)
{
assert(phead);
DLTNode* cur = phead->next;
while (cur != phead)
{
DLTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
//phead = NULL;
}
// test.c
#include"DList.h"
void TestList1()
{/*
DLTNode* plist = NULL;
DListInit(&plist);*/
DLTNode* plist = DListInit();
DListPushBack(plist, 1);
DListPushBack(plist, 2);
DListPushBack(plist, 3);
DListPushBack(plist, 4);
DListPrint(plist);
DListPushFront(plist, 10);
DListPushFront(plist, 20);
DListPushFront(plist, 30);
DListPushFront(plist, 40);
DListPrint(plist);
DListPrint(plist);
DListPopBack(plist);
DListPopBack(plist);
DListPopBack(plist);
DListPopBack(plist);
DListPrint(plist);
DListPopBack(plist);
DListPopBack(plist);
DListPopBack(plist);
DListPopBack(plist);
DListPrint(plist);
//DListPopBack(plist);
DListDestroy(plist);
plist = NULL;
}
void TestList2()
{
DLTNode* plist = DListInit();
DListPushBack(plist, 1);
DListPushBack(plist, 2);
DListPushBack(plist, 3);
DListPushBack(plist, 4);
DListPrint(plist);
DListPopFront(plist);
DListPopFront(plist);
DListPrint(plist);
DListPopFront(plist);
DListPopFront(plist);
DListPrint(plist);
DListDestroy(plist);
plist = NULL;
}
void TestList3()
{
DLTNode* plist = DListInit();
DListPushBack(plist, 1);
DListPushBack(plist, 2);
DListPushBack(plist, 3);
DListPushBack(plist, 4);
DListPrint(plist);
// ...
}
int main()
{
TestList2();
return 0;
}
OK,以上就是本期知识点“双链表”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
🎉如果觉得收获满满,可以点点赞👍支持一下哟~