一、前言
单链表仅有后继指针next,只能单向访问,核心痛点的是找前驱节点需从头遍历、反向操作繁琐,且应对回文链表等算法题时逻辑笨重。双向链表新增前驱指针prior,专门解决这些问题,大幅提升灵活性和效率。
双向链表 vs 单链表
优点 :直接通过prior找前驱,无需遍历;已知节点删除效率O(1);支持反向遍历;简化回文判断、节点交换等算法题逻辑;头尾操作统一。
缺点:多一个指针,占用更多内存;插入/删除需修改两个指针,易出错;代码逻辑略复杂。

二、结构体设计
1. 结构体定义
cpp
typedef struct DLNode {
ELEMTYPE data; //数据域
struct DLNode* next; //后继指针
struct DLNode* prior; //前驱指针
} DLNode, * PDLNode;
2. 字段讲解: data存储数据;next实现正向遍历;prior实现反向遍历,是双向链表的核心。
**3. 设计说明:**采用带头结点设计,头结点不存有效数据,统一插入/删除逻辑,避免空链表等特殊情况的冗余判断。
三、函数接口
cpp
// 工具函数
void Init_DoubleList(DLNode* plist); // 初始化
bool IsEmpty(DLNode* plist); // 判空
int Get_Length(DLNode* plist); // 获取长度
bool Clear(DLNode* plist); // 清空
bool Destroy1(DLNode* plist); // 销毁(循环头删)
bool Destroy2(DLNode* plist); // 销毁(双指针)
bool Show(DLNode* plist); // 打印
DLNode* Search(DLNode* plist, ELEMTYPE val); // 按值查找
// 核心函数(插入)
bool Insert_Head(DLNode* plist, ELEMTYPE val); // 头插
bool Insert_Tail(DLNode* plist, ELEMTYPE val); // 尾插
bool Insert_Pos(DLNode* plist, ELEMTYPE val, int pos); // 按位置插
// 核心函数(删除)
bool Del_Head(DLNode* plist); // 头删
bool Del_Tail(DLNode* plist); // 尾删
bool Del_Pos(DLNode* plist, int pos); // 按位置删
bool Del_Val(DLNode* plist, ELEMTYPE val); // 按值删(第一次)
bool Del_Val_All(DLNode* plist, ELEMTYPE val); // 按值删(全部)
四、核心函数
**插入核心:**先连新节点指针,再改旧节点指针,注意空链表边界。
**删除核心:**先断链,再释放内存,处理头尾节点特殊情况。
(一)插入函数
1. 头插
cpp
bool Insert_Head(DLNode* plist, ELEMTYPE val) {
assert(plist != nullptr);
DLNode* pnewnode = new DLNode;
pnewnode->data = val;
pnewnode->next = plist->next;
pnewnode->prior = plist;
if (!IsEmpty(plist)) {
plist->next->prior = pnewnode;
}
plist->next = pnewnode;
return true;
}
**思路:**安全校验→申请新节点→按"新节点连前后→旧节点改指针"顺序操作,空链表无需修改原首节点前驱。
**易错点:**指针修改顺序不能乱,空链表需跳过原首节点前驱修改步骤。

2. 尾插
cpp
bool Insert_Tail(DLNode* plist, ELEMTYPE val) {
assert(plist != nullptr);
DLNode* pnewnode = new DLNode;
pnewnode->data = val;
DLNode* p = plist;
while (p->next != nullptr) {
p = p->next;
}
pnewnode->next = p->next;
pnewnode->prior = p;
p->next = pnewnode;
return true;
}
**思路:**遍历找到尾节点→新节点与尾节点双向关联,无需判断空链表(空链表时p指向头结点,逻辑仍成立)。
**说明:**尾插是按位置插的特例(pos=链表长度)。

3. 按位置插(核心)
cpp
bool Insert_Pos(DLNode* plist, ELEMTYPE val, int pos) {
assert(plist != nullptr);
int len = Get_Length(plist);
assert(pos >= 0 && pos <= len);
if (pos == 0) return Insert_Head(plist, val);
if (pos == len) return Insert_Tail(plist, val);
DLNode* pnewnode = new DLNode;
pnewnode->data = val;
DLNode* p = plist;
for (int i = 0; i < pos; i++) {
p = p->next;
}
pnewnode->next = p->next;
pnewnode->prior = p;
p->next->prior = pnewnode;
p->next = pnewnode;
return true;
}
思路
1. 安全校验: 除了断言plist不为空,还要判断pos的合法性 ------pos不能小于 0,也不能大于链表长度(插入位置可以是 0~len,0 是头插,len 是尾插)。
2. 特例复用: 如果是头插或尾插,直接调用前面写好的Insert_Head和Insert_Tail,减少代码冗余,避免重复出错。
3. 通用插入逻辑(中间位置插入)
- 先找到插入位置的前一个节点
p(遍历 pos 次,从头部开始) - 按照 "新节点先连前后,再改原有节点" 的顺序修改指针,和头插的指针修改逻辑完全一致
**4. 本质:**头插(pos=0)、尾插(pos=len)是按位置插入的两个特殊情况,中间位置插入是通用情况,掌握通用逻辑,就能应对所有插入场景。
重点
**1. 指针挂接逻辑:**和头插完全一致,记住 "先连新节点的 next 和 prior,再修改插入位置前后节点的指针",避免指针丢失。
2. 合法性判断: pos的范围必须是0<=pos<=len,如果pos>len,插入位置超出链表范围,会导致操作空指针;如果pos<0,插入位置非法,直接断言报错。
【图片标注】此处可插入 "按位置插入(中间位置)示意图",标注插入位置、p指针的位置、指针修改的顺序,清晰展示通用插入逻辑。
(二)删除函数
1. 头删
cpp
bool Del_Head(DLNode* plist) {
assert(plist != nullptr);
if (IsEmpty(plist)) return false;
DLNode* p = plist->next;
plist->next = p->next;
if (p->next != nullptr) {
p->next->prior = plist;
}
delete p;
return true;
}
**思路:**判空→定位首节点→断链→释放内存,删除后有节点则修改新首节点前驱。
**易错点:**空链表不能删,删除最后一个节点无需修改新首节点前驱。


2. 尾删
cpp
bool Del_Tail(DLNode* plist) {
assert(plist != nullptr);
if (IsEmpty(plist)) return false;
DLNode* p = plist;
while (p->next != nullptr) {
p = p->next;
}
DLNode* q = p->prior;
q->next = p->next;
delete p;
return true;
}
**思路:**判空→找到尾节点及其前驱→断链→释放尾节点,逻辑统一无需额外边界判断。
3. 按位置删(核心)
cpp
bool Del_Pos(DLNode* plist, int pos) {
assert(plist != nullptr);
int len = Get_Length(plist);
assert(pos >= 0 && pos < len);
if (IsEmpty(plist)) return false;
if (pos == 0) return Del_Head(plist);
if (pos == len - 1) return Del_Tail(plist);
DLNode* p = plist;
for (int i = 0; i < pos; i++) {
p = p->next;
}
DLNode* q = p->prior;
q->next = p->next;
p->next->prior = q;
delete p;
return true;
}
思路
1. 安全校验: pos的合法性的判断和插入不同 ------ 删除时pos不能大于等于链表长度(有效节点数为 len,pos 范围是 0~len-1),否则删除位置非法。
2. 特例复用: pos=0(头删)、pos=len-1(尾删),直接调用Del_Head和Del_Tail,减少代码冗余。
3. 中间位置删除
- 遍历找到待删除节点
p(pos 次遍历),用q指向p的前驱节点 - 断链操作:让
q的next跳过p,指向p的下一个节点;同时让p的下一个节点的prior指向q,彻底断开p与链表的关联
4. 释放内存: delete p并赋值 NULL,避免野指针。
关键点
1. pos合法性: 删除时pos不能等于 len(插入时可以等于 len),否则会操作空指针;
2. 断链顺序: 先修改前驱节点的next,再修改后继节点的prior,顺序不影响,但必须都修改,否则链表双向关联断裂。
4. 按值删(第一次出现)
cpp
bool Del_Val(DLNode* plist, ELEMTYPE val) {
assert(plist != nullptr);
if (IsEmpty(plist)) return false;
DLNode* p = Search(plist, val);
if (p == nullptr) return false;
DLNode* q = p->prior;
q->next = p->next;
if (p->next != nullptr) {
p->next->prior = q;
}
delete p;
return true;
}
**思路:**借助Search函数定位节点→断链(处理尾节点特例)→释放内存,未找到则返回false。
5. 按值删(全部)
cpp
bool Del_Val_All(DLNode* plist, ELEMTYPE val) {
assert(plist != nullptr);
if (IsEmpty(plist)) return false;
while (Search(plist, val) != nullptr) {
DLNode* p = Search(plist, val);
DLNode* q = p->prior;
q->next = p->next;
if (p->next != nullptr) {
p->next->prior = q;
}
delete p;
}
return true;
}
**思路:**循环调用Search查找节点,找到则删除,直至无匹配节点,避免死循环。
五、工具函数
1. 初始化
void Init_DoubleList(DLNode* plist) {
assert(plist != nullptr);
plist->next = nullptr;
plist->prior = nullptr;
}
**思路:**头结点前后驱置空,使链表处于空状态,避免空指针操作。
2. 判空
bool IsEmpty(DLNode* plist) {
assert(plist != nullptr);
return plist->next == nullptr;
}
**思路:**头结点next为空,即无有效节点,判空逻辑简洁高效。
3. 获取有效长度
int Get_Length(DLNode* plist) {
assert(plist != nullptr);
int count = 0;
for (DLNode* p = plist->next; p != nullptr; p = p->next) {
count++;
}
return count;
}
**思路:**遍历有效节点计数,不统计头结点,遍历至空节点停止。
4. 清空
bool Clear(DLNode* plist) {
assert(plist != nullptr);
Destroy2(plist);
return true;
}
**思路:**复用Destroy2逻辑,删除所有有效节点,保留头结点,回归初始化状态。
5. 销毁1(循环头删)
bool Destroy1(DLNode* plist) {
assert(plist != nullptr);
while (!IsEmpty(plist)) {
Del_Head(plist);
}
return true;
}
**思路:**循环头删,直至链表为空,实现简单,适合新手。
6. 销毁2(双指针配合)
bool Destroy2(DLNode* plist) {
assert(plist != nullptr);
DLNode* p = plist->next;
DLNode* q = nullptr;
while (p != nullptr) {
q = p->next;
delete p;
p = q;
}
plist->next = nullptr;
return true;
}
**思路:**双指针遍历,先记录下一个节点,再释放当前节点,效率高于Destroy1。
7. 打印
bool Show(DLNode* plist) {
assert(plist != nullptr);
for (DLNode* p = plist->next; p != nullptr; p = p->next) {
printf("%d ", p->data);
}
printf("\n");
return true;
}
**思路:**正向遍历有效节点并打印,换行提升可读性,方便调试。
8. 查找
DLNode* Search(DLNode* plist, ELEMTYPE val) {
assert(plist != nullptr);
DLNode* p = plist->next; while (p != nullptr) {
if (p->data == val) {
return p;
}
p = p->next;
}
return nullptr;
}
**思路:**正向遍历,找到第一个匹配节点返回指针,未找到返回nullptr,供删除函数使用。
六、总结
-
双向链表核心:前驱指针(prior)+ 后继指针(next),解决单链表痛点,支持双向遍历。
-
插入关键:指针挂接顺序(先连新节点)、空链表/头尾边界判断,避免指针丢失。
-
删除关键:先断链、再释放内存,处理尾节点等边界,避免内存泄漏。
-
核心关联:头插/尾插是按位置插的特例,头删/尾删是按位置删的特例,掌握通用逻辑即可应对所有场景。
-
新手避坑:做好断言和判空,指针修改顺序不能乱,删除后及时释放内存。