【数据结构】线性表之“双链表(带头循环双向链表)”
- 第 99 篇 -
Date: 2025 - 05 - 25
Author: 郑龙浩/仟墨
【数据结构】
续上一篇: 线性表之“单链表”
文章目录
- “双链表(带头双向循环链表)” 的实现:
- 分步解释所有函数:
- test.c
- DListNode.h
- DListNode.c
“双链表(带头双向循环链表)” 的实现:
- 带头:带头结点
- 循环:链表中的结点是循环的,头结点前驱是最后一个结点,最后一个结点后继是头结点
- 双向:每一个结点都有存储前驱结点和后继结点的指针域
分步解释所有函数:
-
申请新的结点
封装这个函数是因为在后边插入的时候多次使用了下面的代码,造成了代码冗余,所以将申请新结点的代码封装起来,用的时候直接调用即可
malloc
申请新的空间,宽度为sizef(DListNode)
- 判断
malloc
内存分配是否成功,失败则退出(概论很小) - 设置结点数据域,将元素
x
存入该结点的data
中 - 初始化指针域,前驱后继全部都指向自己(完成闭环)
- 返回申请的结点的地址
// 1 申请新的结点 DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申请新的结点if (NewNode == NULL) {printf("内存申请失败!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存储数据NewNode->prev = NewNode; // 初始时指向自己NewNode->next = NewNode; // 初始时指向自己return NewNode; }
-
带头结点的双向循环链表初始化 – 创建并返回头结点,即返回哨兵结点
这个函数的作用和
BuyDListNode
很像,但是意思上不一样!-
DListInit()
有很强的语义约束 –> 专门用于初始化,而非开辟新的结点的内存空间 -
返回的头结点指针具有特殊作用(哨兵结点),不是普通节点(普通结点的
data
存储数据,而哨兵结点的data
不存储数据,默认为0)返回创建的头结点指针,该头结点代表一个空链表
-
写函数的时候,可以调用申请结点的函数来实现头节点的创建
步骤:
- 调用
BuyDListNode(0)
创建头结点(data
无实际意义) - 由于
BuyDListNode
已让prev
和next
指向自己,无需额外操作 - 返回头结点指针
// 2 带头结点的双向循环链表初始化 -- 创建并返回头结点,即返回哨兵结点 DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申请新的结点 // 头结点的 data 无意义,仅作哨兵// 因为BuyDListNode(0_中已经指向了自己所以下面代码可写可不写//NewNode->prev = NewNode; // 新结点前驱指向自己//NewNode->next = NewNode; // 新结点后继指向自己return NewNode; // 返回头结点(哨兵结点) }
-
-
双向链表清除 – 只释放头节点以外的结点
清除的意思是保留双链表本身,但是双链表中保存数据的结点要清除(不清除头结点)
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 释放哨兵结点以外的结点(存储数据的结点)
- 初始化指针域,将哨兵结点的前驱和后继全部指向自己,完成闭环
// 3 双向链表清除 -- 只释放头节点以外的结点 void DListClear(DListNode* phead) {assert(phead); // 防止空指针DListNode* cur = phead->next; // 存放当前的结点的地址while (cur != phead) { // 释放处头结点(哨兵结点)以外的结点DListNode* next = cur->next; // 保存下一个结点地址(防止释放后地址丢失)free(cur); // 释放当前结点内存空间cur = next; // 指向下一个结点}cur->next = cur->prev = cur; }
- 检查头结点有效性(assert),若
-
双向链表销毁
在
DListClear
基础上增加头结点内存释放但是销毁的时候要用到二级指针,因为二级指针才能将存放头结点地址的二级指针变量置为NULL
步骤:
- 检查头结点有效性(assert),
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 释放哨兵结点以外的结点(存储数据的结点),使用
DListClear
- 释放头结点内存空间
phead
初始化,即phead
中存储的地址为NULL
// 4 双向链表销毁 -- 销毁要用双指针 void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指针DListClear(*phead); // 只释放头节点以外的结点free(*phead); // 释放头结点*phead = NULL; // 重新置为NULL }
- 检查头结点有效性(assert),
-
头插
头插操作是在 哨兵结点(头结点)与 第一个结点之间插入一个新的结点,并且将新的结点与头结点和原第一个结点连接起来
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 申请新的结点
- 新结点前驱指向头结点
- 新节点后继指向原第一个结点
- 原第一结点前驱指向新的结点
- 头结点后继指向新的结点
// 5 头插 void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULL// 因为带头双向循环链表是有头结点的,而头结点不存储数据始终为NULL,即不存在头结点给头结点添加数据的情况DListNode* NewNode = BuyDListNode(x); // 申请新的结点// 头插就是让新的结点插到 “哨兵结点(头结点)”与“第一个结点”之间DListNode* first = phead->next; // 表示第一个结点NewNode->prev = phead; // 1 新结点前驱指向头结点NewNode->next = first; // 2 新节点后继指向原第一个结点first->prev = NewNode; // 3 原第一结点前驱指向新的结点phead->next = NewNode; // 4 头结点后继指向新的结点 }
- 检查头结点有效性(assert),若
-
尾插
尾插操作就是在最后一个结点后边插入一个新的结点,并且将新节点与原最后一个结点和头结点连接起来
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 申请新的结点
- 新节点前驱指向尾节点
- 新节点后继指向头节点
- 尾节点后继指向新节点
- 头节点前驱指向新节点
// 6 尾插 void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申请新的结点// 尾插就是将新的结点放到 tail 的后边DListNode* tail = phead->prev; // 尾结点NewNode->prev = tail; // 1 新节点前驱指向尾节点NewNode->next = phead; // 2 新节点后继指向头节点tail->next = NewNode; // 3 尾节点后继指向新节点phead->prev = NewNode; // 4 头节点前驱指向新节点 }
- 检查头结点有效性(assert),若
-
头删
头删就是将第一个结点(不是删除哨兵结点)的内存空间释放,并且将头结点与原第二个结点连接起来
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 检查链表是否只有一个头结点(哨兵结点),如果是的话,则不删除,直接
return
- 头结点后继指向第二个结点
- 第二个结点前驱指向头结点
- 释放第一个结点
// 7 头删 void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回// 切记:头删不是删除“头结点(哨兵结点)”,而是删除第一个结点DListNode* first = phead->next; // 第一个结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* second = first->next; // 第二个结点phead->next = second; // 1 头结点后继指向第二个结点second->prev = phead; // 2 第二个结点前驱指向头结点free(first); // 3 释放第一个结点 }
- 检查头结点有效性(assert),若
-
尾删
尾删就是将最后一个结点内存空间释放, 并且将原倒数第二个结点与头结点连接起来
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 检查链表是否只有一个头结点(哨兵结点),如果是的话,则不删除,直接
return
- 头结点前驱指向倒数第二个结点
- 倒数第二个结点后继指向头结点
- 释放尾结点内存空间
// 8 尾删 void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回DListNode* tail = phead->prev; // 尾结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* TailPrev = tail->prev; // 倒数第二个结点phead->prev = TailPrev; // 1 头结点前驱指向倒数第二个结点TailPrev->next = phead; // 2 倒数第二个结点后继指向头结点free(tail); // 3 释放尾结点 }
- 检查头结点有效性(assert),若
-
查找 找到返回结点地址,找不到返回NULL
从第一个结点(不是哨兵结点)到 最后一个结点找结点
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 从第一个结点找到最后一个结点,如果找到了
data
为x
的结点,就返回该结点的地址 - 没找到则返回NULL
// 9 查找 找到返回结点地址,找不到返回NULL DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指针,确保phead不是NULL// 切记:头结点不存储数据,查找无需找头结点DListNode* cur = phead->next;// 从第一个结点到最后一个结点 (结束条件为:cur 走到了头结点)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一个结点}return NULL; // 若没找到,则返回NULL }
- 检查头结点有效性(assert),若
-
在 pos 的前面进行插入
pos 前面插入 数据为 x 的结点,就是在 pos 前驱与 pos 之间插入数据,并将新节点 与 pos 前驱后继连接起来
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 验证pos是否属于某个链表(通过检查闭环)
- 申请新的结点
- 新结点的前驱指向 原pos前驱结点
- 新节点的后继指向 pos结点
- 原pos的前驱结点后继指向 新的结点
- 原pos的前驱指向 新的结点
// 10 在pos的前面进行插入(不包括头结点,因为头结点不存储数据) void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos); // 确保不是孤立节点DListNode* NewNode = BuyDListNode(x); // 申请新的结点// pos 前面插入 数据为 x 的结点,就是在 pos 前驱与 pos 之间插入数据DListNode* PosPrev = pos->prev; // pos 的前驱结点NewNode->prev = PosPrev; // 1 新结点的前驱指向 原pos前驱结点NewNode->next = pos; // 2 新节点的后继指向 pos结点PosPrev->next = NewNode; // 3 原pos的前驱结点后继指向 新的结点pos->prev = NewNode; // 4 原pos的前驱指向 新的结点 }
- 检查头结点有效性(assert),若
-
删除 pos 位置的节点
删除pos结点就是将pos的前驱结点和后继结点连接起来,并且释放pos结点
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 验证pos是否属于某个链表(通过检查闭环)
- 新结点的前驱指向 原pos前驱结点
- 新节点的后继指向 pos结点
- 原pos的前驱结点后继指向 新的结点
- 原pos的前驱指向 新的结点
// 11 删除pos位置的结点(不包括头结点,因为头结点不存储数据) void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL && 断言指向头结点(哨兵结点)// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos); // 确保不是孤立节点// 删除pos结点就是将pos的前驱结点和后继结点连接起来,并且释放pos结点pos->prev->next = pos->next; // 1 pos的前驱结点后继指向pos的后继结点pos->next->prev = pos->prev; // 2 pos的后继结点前驱指向pos的前驱结点free(pos); // 3 释放 pos 结点的内存空间 }
- 检查头结点有效性(assert),若
-
打印
从第一个结点打印到最后一个结点
步骤:
- 检查头结点有效性(assert),若
phead
中存储的地址为NULL,则试图访问phead->next
会导致程序崩溃,所以需要拦截非法操作 - 验证pos是否属于某个链表(通过检查闭环)
- 检查是否只有一个结点(哨兵结点),如果是的话则打印
Empty List
,并且跳过后边代码,直接return
- 检查是否只有一个结点(哨兵结点),如果是的话则打印
- 从第一个结点打印到最后一个结点(结束条件为cur为哨兵结点「头结点」)
- 打印结尾
HEAD
// 12 打印 void DListPrint(DListNode* phead) {assert(phead); // 避免空指针,检查头指针是否为NULLDListNode* cur = phead->next; // 存储当前结点地址// 若是头结点if (phead->next == phead) {printf("Empty List\n");return;}// 从第一个结点打印到最后一个结点(结束条件为cur为哨兵结点「头结点」)while (cur != phead) {printf("%d -> ", cur->data); // 打印当前结点的数据cur = cur->next; // 指向下一个结点}printf("HEAD\n"); // 最后打印结束 }
- 检查头结点有效性(assert),若
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"
void Check1();
int main(void) {Check1();return 0;
}// 测试1
void Check1() {// 创建链表DListNode* L = DListInit(); // 新建链表 -- 必须要让L头结点为DListInit(),如果指向NULL会越界访问// 尾插 1 ~ 5printf("尾插1 2 3 4 5\n");for (int i = 1; i <= 5; i++) {DListPushBack(L, i); // 每次尾插}DListPrint(L); // 打印链表// 头插 1 ~ 5printf("头插11 22 33 44 55\n");for (int i = 1; i <= 5; i++) {DListPushFront(L, i*10 + i); // 每次头插}DListPrint(L); // 打印链表// 头删 5 次printf("头删 5 次\n");for (int i = 1; i <= 5; i++) {DListPopFront(L); // 每次头删}DListPrint(L); // 打印链表// 尾删 2 次printf("尾删 2 次\n");for (int i = 1; i <= 2; i++) {DListPopBack(L); // 每次尾插}DListPrint(L); // 打印链表// 在 3 之前插入 33printf("在 3 之前插入 33\n");DListInsert(DListFind(L, 3), 23);DListPrint(L); // 打印链表// 删除数据为 23 的结点printf("删除数据为 23 的结点\n");DListErase(DListFind(L, 23));DListPrint(L); // 打印链表// 清除双链表(保留哨兵)printf("清除双链表(保留哨兵)\n");DListClear(L);if (L->next == L && L->prev == L) {printf("链表已经清除\n");}// 销毁双链表printf("销毁双链表(不保留哨兵)\n");DListDestroy(&L);if (L == NULL) {printf("链表已经销毁\n");}
}
DListNode.h
#define _CRT_SECURE_NO_WARNINGS
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
typedef int ElemType;
typedef struct DListNode {ElemType data; // 数据struct DListNode* prev; // 指向前驱节点(前驱结点的地址)struct DListNode* next; // 指向后驱结点(后驱结点的地址)
}DListNode;// 申请新的结点
DListNode* BuyDListNode(ElemType x);
// 带头结点的双向循环链表初始化 -- 创建并返回头结点,即返回哨兵结点
DListNode* DListInit();
// 双向链表清除 -- 只释放头节点以外的结点
void DListClear(DListNode* phead);
// 双向链表销毁
void DListDestroy(DListNode** phead);
// 头删头插要重新更新头指针
// 头插
void DListPushFront(DListNode* phead, ElemType x);
// 尾插
void DListPushBack(DListNode* phead, ElemType x);
// 头删
void DListPopFront(DListNode* phead);
// 尾删
void DListPopBack(DListNode* phead);
// 查找 找到返回结点地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x);
// 在phead的前面进行插入
void DListInsert(DListNode* pos, ElemType x);
// 删除phead位置的节点
void DListErase(DListNode* pos);
// 打印
void DListPrint(DListNode* phead);
DListNode.c
#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"// 1 申请新的结点
DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申请新的结点if (NewNode == NULL) {printf("内存申请失败!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存储数据NewNode->prev = NewNode; // 初始时指向自己NewNode->next = NewNode; // 初始时指向自己return NewNode;
}
// 2 带头结点的双向循环链表初始化 -- 创建并返回头结点,即返回哨兵结点
DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申请新的结点 // 头结点的 data 无意义,仅作哨兵// 因为BuyDListNode(0_中已经指向了自己所以下面代码可写可不写//NewNode->prev = NewNode; // 新结点前驱指向自己//NewNode->next = NewNode; // 新结点后继指向自己return NewNode; // 返回头结点(哨兵结点)
}
// 3 双向链表清除 -- 只释放头节点以外的结点
void DListClear(DListNode* phead) {assert(phead); // 防止空指针DListNode* cur = phead->next; // 存放当前的结点的地址while (cur != phead) { // 释放处头结点(哨兵结点)以外的结点DListNode* next = cur->next; // 保存下一个结点地址(防止释放后地址丢失)free(cur); // 释放当前结点内存空间cur = next; // 指向下一个结点}cur->next = cur->prev = cur;
}
// 4 双向链表销毁 -- 销毁要用双指针
void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指针DListClear(*phead); // 只释放头节点以外的结点free(*phead); // 释放头结点*phead = NULL; // 重新置为NULL
}
// 5 头插
void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULL// 因为带头双向循环链表是有头结点的,而头结点不存储数据始终为NULL,即不存在头结点给头结点添加数据的情况DListNode* NewNode = BuyDListNode(x); // 申请新的结点// 头插就是让新的结点插到 “哨兵结点(头结点)”与“第一个结点”之间DListNode* first = phead->next; // 表示第一个结点NewNode->prev = phead; // 1 新结点前驱指向头结点NewNode->next = first; // 2 新节点后继指向原第一个结点first->prev = NewNode; // 3 原第一结点前驱指向新的结点phead->next = NewNode; // 4 头结点后继指向新的结点
}
// 6 尾插
void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申请新的结点// 尾插就是将新的结点放到 tail 的后边DListNode* tail = phead->prev; // 尾结点NewNode->prev = tail; // 1 新节点前驱指向尾节点NewNode->next = phead; // 2 新节点后继指向头节点tail->next = NewNode; // 3 尾节点后继指向新节点phead->prev = NewNode; // 4 头节点前驱指向新节点
}
// 7 头删
void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回// 切记:头删不是删除“头结点(哨兵结点)”,而是删除第一个结点DListNode* first = phead->next; // 第一个结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* second = first->next; // 第二个结点phead->next = second; // 1 头结点后继指向第二个结点second->prev = phead; // 2 第二个结点前驱指向头结点free(first); // 3 释放第一个结点
}
// 8 尾删
void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回DListNode* tail = phead->prev; // 尾结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* TailPrev = tail->prev; // 倒数第二个结点phead->prev = TailPrev; // 1 头结点前驱指向倒数第二个结点TailPrev->next = phead; // 2 倒数第二个结点后继指向头结点free(tail); // 3 释放尾结点
}
// 9 查找 找到返回结点地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指针,确保phead不是NULL// 切记:头结点不存储数据,查找无需找头结点DListNode* cur = phead->next;// 从第一个结点到最后一个结点 (结束条件为:cur 走到了头结点)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一个结点}return NULL; // 若没找到,则返回NULL
}
// 10 在pos的前面进行插入(不包括头结点,因为头结点不存储数据)
void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos); // 确保不是孤立节点DListNode* NewNode = BuyDListNode(x); // 申请新的结点// pos 前面插入 数据为 x 的结点,就是在 pos 前驱与 pos 之间插入数据DListNode* PosPrev = pos->prev; // pos 的前驱结点NewNode->prev = PosPrev; // 1 新结点的前驱指向 原pos前驱结点NewNode->next = pos; // 2 新节点的后继指向 pos结点PosPrev->next = NewNode; // 3 原pos的前驱结点后继指向 新的结点pos->prev = NewNode; // 4 原pos的前驱指向 新的结点
}
// 11 删除pos位置的结点(不包括头结点,因为头结点不存储数据)
void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL && 断言指向头结点(哨兵结点)// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos); // 确保不是孤立节点// 删除pos结点就是将pos的前驱结点和后继结点连接起来,并且释放pos结点pos->prev->next = pos->next; // 1 pos的前驱结点后继指向pos的后继结点pos->next->prev = pos->prev; // 2 pos的后继结点前驱指向pos的前驱结点free(pos); // 3 释放 pos 结点的内存空间
}
// 12 打印
void DListPrint(DListNode* phead) {assert(phead); // 避免空指针,检查头指针是否为NULLDListNode* cur = phead->next; // 存储当前结点地址// 若是头结点if (phead->next == phead) {printf("Empty List\n");return;}// 从第一个结点打印到最后一个结点(结束条件为cur为哨兵结点「头结点」)while (cur != phead) {printf("%d -> ", cur->data); // 打印当前结点的数据cur = cur->next; // 指向下一个结点}printf("HEAD\n"); // 最后打印结束
}