前言
双向链表是线性表的重要链式存储结构,相比单链表,其每个节点增加了前驱指针,支持双向遍历和已知节点的O(1)删除,在浏览器前进后退、LRU缓存、双向遍历场景中应用广泛。本文从结构定义、核心操作、完整代码、特性分析、应用场景五个维度,全面讲解双向链表的知识点,所有代码均附带详细注释,可直接编译运行。
一、双向链表的核心结构
1. 节点结构
双向链表的每个节点包含前驱指针(prev)、数据域(data)、后继指针(next) 三部分:
• prev:指向当前节点的上一个节点,头节点prev为NULL
• data:存储节点的有效数据
• next:指向当前节点的下一个节点,尾节点next为NULL
2. 结构体定义(C/C++)
本文基于带头节点的双向链表实现(带头节点可统一空链表和非空链表的操作,减少边界判断冗余),基础int类型节点定义如下:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义双向链表节点结构体
typedef struct DNode {
int data; // 数据域:存储节点数据
struct DNode* prev; // 前驱指针:指向当前节点的上一个节点
struct DNode* next; // 后继指针:指向当前节点的下一个节点
} DNode, *DLinkList; // DNode为节点类型,DLinkList为节点指针类型(链表)
3. 逻辑结构
• 空链表:头节点->prev=NULL + 头节点->next=NULL
• 非空链表:NULL <- 节点1 <-> 节点2 <-> 节点3 -> NULL
二、双向链表的核心操作(全代码+详细注释)
双向链表操作的核心原则:先处理后继指针,再处理前驱指针(避免指针丢失),所有操作均围绕带头节点实现,以下为全量核心操作代码。
- 初始化链表
创建头节点,初始化其前驱和后继指针为NULL,返回初始化结果。
cpp
// 初始化双向链表(带头节点)
bool InitDLinkList(DLinkList *L) {
*L = (DNode*)malloc(sizeof(DNode)); // 为头节点分配内存
if (*L == NULL) { // 内存分配失败(如内存溢出),初始化失败
return false;
}
(*L)->prev = NULL; // 头节点无前驱,prev置空
(*L)->next = NULL; // 空链表,头节点无后继,next置空
return true;
}
- 判断链表是否为空
利用带头节点特性,仅需判断头节点的后继指针是否为NULL。
cpp
// 判断双向链表是否为空
bool IsEmpty(DLinkList L) {
return L->next == NULL; // 空链表返回true,非空返回false
}
- 销毁链表
从头节点开始,依次释放所有节点内存,最终将头指针置空,避免野指针和内存泄漏。
cpp
// 销毁双向链表(释放所有节点内存,包括头节点)
bool DestroyDLinkList(DLinkList *L) {
DNode *p = *L; // p指向当前要释放的节点,初始为头节点
DNode *q = NULL; // q保存p的后继节点(防止释放后找不到下一个节点)
while (p != NULL) {
q = p->next; // 先保存后继节点地址
free(p); // 释放当前节点内存
p = q; // p移动到下一个节点
}
*L = NULL; // 头指针置空,避免野指针
return true;
}
- 清空链表
释放所有数据节点,保留头节点,后续可继续使用链表,与销毁的核心区别是保留头节点。
cpp
// 清空双向链表(保留头节点,删除所有数据节点)
bool ClearDLinkList(DLinkList L) {
DNode *p = L->next; // p指向第一个数据节点
DNode *q = NULL; // 保存p的后继节点
while (p != NULL) {
q = p->next;
free(p);
p = q;
}
L->next = NULL; // 头节点后继置空,恢复空链表状态
return true;
}
- 获取链表长度
从第一个数据节点开始正序遍历,统计数据节点个数(头节点不计入长度)。
cpp
// 获取双向链表长度(仅统计数据节点,头节点不计)
int GetLength(DLinkList L) {
int len = 0;
DNode *p = L->next; // p指向第一个数据节点
while (p != NULL) { // 遍历至尾节点结束
len++;
p = p->next;
}
return len;
}
- 按位置查找节点
找到第i个数据节点,返回其指针;位置非法时返回NULL(i从1开始)。
cpp
// 按位置查找:获取第i个数据节点的指针(i从1开始)
// 成功返回节点指针,失败(位置非法/空链表)返回NULL
DNode* GetElem(DLinkList L, int i) {
if (i < 1 || IsEmpty(L)) { // 位置小于1或空链表,直接返回NULL
return NULL;
}
int j = 1;
DNode *p = L->next; // p指向第一个数据节点
while (p != NULL && j < i) { // 未到第i个节点且未遍历完
p = p->next;
j++;
}
return p; // 遍历结束后,p为NULL则位置非法,否则为目标节点
}
- 按值查找节点
正序遍历链表,找到首个数据域等于e的节点,返回其指针;无匹配节点返回NULL。
cpp
// 按值查找:获取首个数据域为e的节点指针
DNode* LocateElem(DLinkList L, int e) {
if (IsEmpty(L)) { // 空链表无匹配节点
return NULL;
}
DNode *p = L->next;
while (p != NULL) {
if (p->data == e) { // 找到匹配节点,直接返回
return p;
}
p = p->next;
}
return NULL; // 遍历结束无匹配
}
- 插入节点(第i个节点前插入)
双向链表插入核心步骤:先连新节点的后继,再连前驱,避免指针丢失。
cpp
// 插入节点:在第i个数据节点**之前**插入数据为e的新节点
bool ListInsert(DLinkList L, int i, int e) {
if (i < 1) { // 插入位置小于1,非法
return false;
}
DNode *p = GetElem(L, i-1); // 找到第i-1个节点(插入位置的前驱)
if (p == NULL) { // 第i-1个节点不存在(i超过长度+1),插入失败
return false;
}
// 1. 创建新节点并初始化数据域
DNode *s = (DNode*)malloc(sizeof(DNode));
if (s == NULL) { // 内存分配失败,插入失败
return false;
}
s->data = e;
// 2. 先连新节点的后继,再连前驱(核心顺序,防止指针丢失)
s->next = p->next; // 新节点的后继 = 前驱节点的原后继
if (p->next != NULL) { // 若前驱不是尾节点,修改原后继的前驱为新节点
p->next->prev = s;
}
p->next = s; // 前驱节点的后继 = 新节点
s->prev = p; // 新节点的前驱 = 前驱节点
return true;
}
- 按位置删除节点
删除核心步骤:先修改前驱的后继,再修改后继的前驱,最后释放节点内存。
cpp
// 删除节点:删除第i个数据节点
bool ListDelete(DLinkList L, int i) {
if (i < 1 || IsEmpty(L)) { // 位置非法或空链表,删除失败
return false;
}
DNode *q = GetElem(L, i); // 找到第i个待删除节点
if (q == NULL) { // 待删除节点不存在,删除失败
return false;
}
// 1. 待删除节点的前驱 指向 待删除节点的后继
q->prev->next = q->next;
// 2. 若待删除节点不是尾节点,其后继 指向 待删除节点的前驱
if (q->next != NULL) {
q->next->prev = q->prev;
}
// 3. 释放待删除节点内存
free(q);
return true;
}
- 已知节点的O(1)删除(双向链表独有优势)
单链表已知节点删除需O(n)遍历找前驱,双向链表可通过prev直接获取前驱,实现O(1)时间复杂度删除。
cpp
// 已知节点p,O(1)时间删除该节点(双向链表独有优势)
// 注意:p必须是链表中存在的有效节点
bool DeleteNode(DLinkList L, DNode *p) {
if (p == NULL || L == NULL) { // 节点或链表为空,删除失败
return false;
}
// 1. 前驱节点的后继 指向 待删除节点的后继
p->prev->next = p->next;
// 2. 若待删除节点不是尾节点,后继节点的前驱 指向前驱节点
if (p->next != NULL) {
p->next->prev = p->prev;
}
// 3. 释放节点内存
free(p);
return true;
}
- 双向遍历(正序+逆序)
双向链表的核心优势之一,支持从头到尾和从尾到头两种遍历方式,逆序遍历需先找到尾节点。
cpp
// 正序遍历双向链表:从第一个数据节点到尾节点
void TraverseForward(DLinkList L) {
if (IsEmpty(L)) {
printf("链表为空,无数据可遍历!\n");
return;
}
DNode *p = L->next;
printf("正序遍历:");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 逆序遍历双向链表:从尾节点到第一个数据节点(双向链表独有)
void TraverseBackward(DLinkList L) {
if (IsEmpty(L)) {
printf("链表为空,无数据可遍历!\n");
return;
}
DNode *p = L->next;
// 先找到尾节点(next为NULL的节点)
while (p->next != NULL) {
p = p->next;
}
printf("逆序遍历:");
// 从尾节点向前遍历,至头节点停止
while (p != L) {
printf("%d ", p->data);
p = p->prev;
}
printf("\n");
}
- 链表创建(尾插法+头插法)
尾插法:按数据顺序创建,适合常规存储(先进先出)
cpp
// 尾插法创建双向链表:arr为数据数组,n为数组长度
bool CreateDLinkList_Tail(DLinkList *L, int arr[], int n) {
if (!InitDLinkList(L)) { // 先初始化链表
return false;
}
DNode *tail = *L; // 尾指针,初始指向头节点,始终指向尾节点
for (int i = 0; i < n; i++) {
// 创建新节点
DNode *s = (DNode*)malloc(sizeof(DNode));
if (s == NULL) {
return false;
}
s->data = arr[i];
// 插入到尾节点后
s->next = tail->next;
s->prev = tail;
tail->next = s;
tail = s; // 尾指针后移,指向新的尾节点
}
return true;
}
头插法:创建后数据逆序,适合实现栈(后进先出)
// 头插法创建双向链表:arr为数据数组,n为数组长度
bool CreateDLinkList_Head(DLinkList *L, int arr[], int n) {
if (!InitDLinkList(L)) { // 先初始化链表
return false;
}
for (int i = 0; i < n; i++) {
DNode *s = (DNode*)malloc(sizeof(DNode));
if (s == NULL) {
return false;
}
s->data = arr[i];
// 插入到头节点后,与普通插入逻辑一致
s->next = (*L)->next;
if ((*L)->next != NULL) {
(*L)->next->prev = s;
}
(*L)->next = s;
s->prev = *L;
}
return true;
}
三、完整测试代码(可直接编译运行)
将上述所有操作整合,测试功能正确性,可直接复制到VS Code、Dev-C++、Clion等编译器运行。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义双向链表节点结构体
typedef struct DNode {
int data;
struct DNode* prev;
struct DNode* next;
} DNode, *DLinkList;
// 初始化双向链表(带头节点)
bool InitDLinkList(DLinkList *L) {
*L = (DNode*)malloc(sizeof(DNode));
if (*L == NULL) {
return false;
}
(*L)->prev = NULL;
(*L)->next = NULL;
return true;
}
// 判断双向链表是否为空
bool IsEmpty(DLinkList L) {
return L->next == NULL;
}
// 销毁双向链表(释放所有节点内存,包括头节点)
bool DestroyDLinkList(DLinkList *L) {
DNode *p = *L;
DNode *q = NULL;
while (p != NULL) {
q = p->next;
free(p);
p = q;
}
*L = NULL;
return true;
}
// 清空双向链表(保留头节点,删除所有数据节点)
bool ClearDLinkList(DLinkList L) {
DNode *p = L->next;
DNode *q = NULL;
while (p != NULL) {
q = p->next;
free(p);
p = q;
}
L->next = NULL;
return true;
}
// 获取双向链表长度(仅统计数据节点,头节点不计)
int GetLength(DLinkList L) {
int len = 0;
DNode *p = L->next;
while (p != NULL) {
len++;
p = p->next;
}
return len;
}
// 按位置查找:获取第i个数据节点的指针(i从1开始)
DNode* GetElem(DLinkList L, int i) {
if (i < 1 || IsEmpty(L)) {
return NULL;
}
int j = 1;
DNode *p = L->next;
while (p != NULL && j < i) {
p = p->next;
j++;
}
return p;
}
// 按值查找:获取首个数据域为e的节点指针
DNode* LocateElem(DLinkList L, int e) {
if (IsEmpty(L)) {
return NULL;
}
DNode *p = L->next;
while (p != NULL) {
if (p->data == e) {
return p;
}
p = p->next;
}
return NULL;
}
// 插入节点:在第i个数据节点**之前**插入数据为e的新节点
bool ListInsert(DLinkList L, int i, int e) {
if (i < 1) {
return false;
}
DNode *p = GetElem(L, i-1);
if (p == NULL) {
return false;
}
DNode *s = (DNode*)malloc(sizeof(DNode));
if (s == NULL) {
return false;
}
s->data = e;
s->next = p->next;
if (p->next != NULL) {
p->next->prev = s;
}
p->next = s;
s->prev = p;
return true;
}
// 删除节点:删除第i个数据节点
bool ListDelete(DLinkList L, int i) {
if (i < 1 || IsEmpty(L)) {
return false;
}
DNode *q = GetElem(L, i);
if (q == NULL) {
return false;
}
q->prev->next = q->next;
if (q->next != NULL) {
q->next->prev = q->prev;
}
free(q);
return true;
}
// 已知节点p,O(1)时间删除该节点
bool DeleteNode(DLinkList L, DNode *p) {
if (p == NULL || L == NULL) {
return false;
}
p->prev->next = p->next;
if (p->next != NULL) {
p->next->prev = p->prev;
}
free(p);
return true;
}
// 正序遍历双向链表
void TraverseForward(DLinkList L) {
if (IsEmpty(L)) {
printf("链表为空,无数据可遍历!\n");
return;
}
DNode *p = L->next;
printf("正序遍历:");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 逆序遍历双向链表
void TraverseBackward(DLinkList L) {
if (IsEmpty(L)) {
printf("链表为空,无数据可遍历!\n");
return;
}
DNode *p = L->next;
while (p->next != NULL) {
p = p->next;
}
printf("逆序遍历:");
while (p != L) {
printf("%d ", p->data);
p = p->prev;
}
printf("\n");
}
// 尾插法创建双向链表
bool CreateDLinkList_Tail(DLinkList *L, int arr[], int n) {
if (!InitDLinkList(L)) {
return false;
}
DNode *tail = *L;
for (int i = 0; i < n; i++) {
DNode *s = (DNode*)malloc(sizeof(DNode));
if (s == NULL) {
return false;
}
s->data = arr[i];
s->next = tail->next;
s->prev = tail;
tail->next = s;
tail = s;
}
return true;
}
// 头插法创建双向链表
bool CreateDLinkList_Head(DLinkList *L, int arr[], int n) {
if (!InitDLinkList(L)) {
return false;
}
for (int i = 0; i < n; i++) {
DNode *s = (DNode*)malloc(sizeof(DNode));
if (s == NULL) {
return false;
}
s->data = arr[i];
s->next = (*L)->next;
if ((*L)->next != NULL) {
(*L)->next->prev = s;
}
(*L)->next = s;
s->prev = *L;
}
return true;
}
// 主函数:测试所有操作
int main() {
DLinkList L;
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
// 1. 尾插法创建链表
if (CreateDLinkList_Tail(&L, arr, n)) {
printf("===== 尾插法创建链表成功 =====\n");
TraverseForward(L); // 正序:1 2 3 4 5
TraverseBackward(L);// 逆序:5 4 3 2 1
printf("链表长度:%d\n\n", GetLength(L)); // 5
}
// 2. 插入节点:在第3个节点前插入6
if (ListInsert(L, 3, 6)) {
printf("===== 在第3个节点前插入6 =====\n");
TraverseForward(L); // 1 2 6 3 4 5
printf("\n");
}
// 3. 删除节点:删除第6个节点
if (ListDelete(L, 6)) {
printf("===== 删除第6个节点 =====\n");
TraverseForward(L); // 1 2 6 3 4
printf("\n");
}
// 4. 按值查找+已知节点删除
DNode *p = LocateElem(L, 6);
if (p != NULL) {
printf("===== 找到值为6的节点,执行O(1)删除 =====\n");
DeleteNode(L, p);
TraverseForward(L); // 1 2 3 4
printf("\n");
}
// 5. 清空链表
if (ClearDLinkList(L)) {
printf("===== 清空链表 =====\n");
printf("链表是否为空:%s\n\n", IsEmpty(L) ? "是" : "否"); // 是
}
// 6. 销毁链表
if (DestroyDLinkList(&L)) {
printf("===== 销毁链表 =====\n");
printf("链表销毁成功!\n");
}
return 0;
}
运行结果
cpp
===== 尾插法创建链表成功 =====
正序遍历:1 2 3 4 5
逆序遍历:5 4 3 2 1
链表长度:5
===== 在第3个节点前插入6 =====
正序遍历:1 2 6 3 4 5
===== 删除第6个节点 =====
正序遍历:1 2 6 3 4
===== 找到值为6的节点,执行O(1)删除 =====
正序遍历:1 2 3 4
===== 清空链表 =====
链表是否为空:是
===== 销毁链表 =====
链表销毁成功!
四、核心特性
时间复杂度
• O(1):初始化/判空/销毁(已知节点)、已知节点的插入/删除
• O(n):按位置插删、按值/位置查找、正/逆序遍历、头/尾插法创建
空间复杂度
• 单操作:O(1)(仅常数个辅助指针)
• 链表创建:O(n)(存储节点数据与双指针域)
优点
-
支持正/逆序双向遍历,灵活性远超单链表
-
已知节点时O(1)删除,解决单链表性能瓶颈
-
带头节点实现,统一空/非空链表操作,减少边界判断
-
前驱/后继指向明确,头/尾节点边界处理更友好
缺点
-
每个节点多一个前驱指针,空间开销大、存储密度低
-
插删需同时维护prev/next,操作繁琐易出指针错误
-
节点内存占用稍大,存在轻微内存分配失败风险
五、与单链表核心对比
|--------|----------------|------------|
| 特性 | 双向链表 | 单链表 |
| 节点结构 | prev+data+next | data+next |
| 遍历方式 | 正/逆序双向遍历 | 仅正序遍历 |
| 已知节点删除 | O(1)(核心优势) | O(n)(需找前驱) |
| 空间开销 | 较大(双指针) | 较小(单指针) |
| 操作复杂度 | 稍高(维护双指针) | 较低(维护单指针) |
| 存储密度 | 较低 | 较高 |
| 核心优势 | 双向遍历、O(1)删已知节点 | 实现简单、空间开销小 |
六、典型应用场景
适配双向遍历或频繁删除已知节点的需求:
-
浏览器前进/后退、文件管理器上下级切换
-
LRU缓存淘汰算法(快速删除最久未使用节点)
-
聊天记录上下翻页、有序链表动态维护
七、高频错误与避坑指南
问题多集中在指针维护和边界判断,对应解决方案:
-
插删指针顺序错误→ 插入遵循先后继、后前驱原则
-
删除尾节点越界→ 操作前判断node->next != NULL
-
逆序遍历终止错误→ 以p != 头节点为终止条件,避免访问NULL
-
野指针问题→ 释放节点/销毁链表后,及时将相关指针置NULL
-
内存泄漏→ 所有malloc后必须检查返回值是否为NULL
-
混淆头/数据节点→ 遍历从头节点->next开始,头节点仅作哨兵
八、总结
双向链表通过增加前驱指针,以少量空间开销,换取双向遍历和O(1)删除已知节点的核心优势,是单链表的优化版本。
其学习核心为:理解指针指向关系,牢记插删的指针操作顺序,做好边界判断;所有操作本质都是对prev和next指针的合理维护,抓住指针即掌握双向链表核心。掌握后可拓展学习循环双向链表、LRU缓存等进阶应用,落地实际开发。