数据结构:双向链表-从原理到实战完整指南
引言:
在上期接触了链式存储结构并深入理解了单链表后,我们体会到了其动态灵活的特性。然而,单链表在某些查找和删除操作上存在明显不足:当需要访问节点的前驱时,我们往往需要从头开始遍历,效率低下。
为了克服这一痛点,本期我们将聚焦于双向链表。双向链表最大的特点在于,每个节点除了指向后继节点外,还新增了一个指向前驱节点的指针。这种设计赋予了我们在链表中“前后兼顾”的能力,极大地提升了操作的灵活性和效率,尤其是在需要回溯查找时。
接下来的内容,我们将通过代码实践,深入领会双向链表的结构、插入、删除、查找等核心操作,并对比其与单链表的异同,清晰展现双向链表如何成为单链表不可或缺的升级。将这个引言改的简短精炼一点
双向链表
基础结构定义:
#include <stdio.h>
#include <stdlib.h>typedef int ElemType;//结构体定义
typedef struct node {ElemType data;struct node* prev; // 指向前一个节点struct node* next; // 指向下一个节点
}Node;
初始化:
//初始化
Node* initList()
{Node* head = (Node*)malloc(sizeof(Node));head->data = 0;head->next = NULL;return head;
}遍历:
//遍历
void listNode(Node* L)
{Node* p = L->next;while (p != NULL){printf("%d ", p->data);p = p->next;}printf("\n");}
双向链表-头插法
图解表示:

代码表示:
双向链表-头插法核心代码:
//头插法
int insertHead(Node* L, ElemType e)
// L: 指向链表的头节点
// e: 要插入的数据
{Node* p = (Node*)malloc(sizeof(Node));//初始化新节点p->data = e;// 将新节点 p 的 prev 指向头节点 Lp->prev = L;//头节点的next指向新节点的nextp->next = L->next;if (L->next != NULL){//新节点指向(赋值给)头节点的next的prevL->next->prev = p;}//新节点指向(赋值给)头节点的nextL->next = p;return 1;}mian中调用
//初始化
Node* initList()
{Node* head = (Node*)malloc(sizeof(Node));head->data = 0;head->next = NULL;return head;
}结果如下:

双向链表-尾插法
图解表示:

代码表示:
获取链表的尾节点
// 获取链表的尾节点(最后一个实际存储数据的节点)
Node* get_tail(Node* L) {if (L == NULL || L->next == NULL) {// 链表为空或只有一个头节点return L; // 返回头节点,插入尾部时会处理}Node* current = L;while (current->next != NULL) {current = current->next;}return current; // current 现在指向最后一个实际节点
}双向链表-尾插法核心代码:
//尾插法
Node* insertTail(Node* tail, ElemType e)
{Node* p = (Node*)malloc(sizeof(Node));p->data = e;p->prev = tail;tail->next = p;p->next = NULL;return p;
}在main中调用:
int main(int argc, char const* argv[])
{Node* list = initList();Node* tail = get_tail(list);tail = insertTail(tail, 10);tail = insertTail(tail, 20);tail = insertTail(tail, 30);listNode(list);}
结果如下:

双向链表-在指定位置插入数据
图解表示:

代码表示:
双向链表-中间插入核心代码
//指定位置插入
int insertNode(Node* L, int pos, ElemType e)
{Node* p = L;int i = 0;while (i < pos - 1){p = p->next;i++;if (p == NULL){return 0;}}Node* q = (Node*)malloc(sizeof(Node));q->data = e;q->prev = p;q->next = p->next;p->next->prev = q;p->next = q;return 1;
}在main中调用:
int main(int argc, char const* argv[])
{Node* list = initList();Node* tail = get_tail(list);tail = insertTail(tail, 10);tail = insertTail(tail, 20);tail = insertTail(tail, 30);insertNode(list, 2, 15);listNode(list);
}结果如下:

双向链表-删除节点
步骤如下:
1.找到要删除节点的前置节点p
2.用指针q记录要删除的节点
3.通过改变p的后继节及要删除节点的下一个节点的前驱实现删除
4.释放删除节点的空间
图解表示:

代码表示:
双链表删除节点核心代码:
//删除节点
int deleteNode(Node* L, int pos)
{Node* p = L;int i = 0;while (i < pos - 1){p = p->next;i++;if (p == NULL){return 0;}}Node* q = p->next;p->next = q->next;q->next->prev = p;free(q);return 1;
}
在main中调用:
int main(int argc, char const* argv[])
{Node* list = initList();//insertHead(list, 10);//insertHead(list, 20);//insertHead(list, 30);//listNode(list);Node* tail = get_tail(list);tail = insertTail(tail, 10);tail = insertTail(tail, 20);tail = insertTail(tail, 30);//listNode(list);insertNode(list, 2, 15);listNode(list);deleteNode(list,2);listNode(list);}结果如下:

释放双向链表
//释放链表
void freeList(Node* L)
{Node* p = L->next;Node* q;while (p != NULL){q = p->next;free(p);p = q; }L->next = NULL;
}完整代码
#include <stdio.h>
#include <stdlib.h>typedef int ElemType;//结构体定义
typedef struct node {ElemType data;struct node* prev; // 指向前一个节点struct node* next; // 指向下一个节点
}Node;//初始化
Node* initList()
{Node* head = (Node*)malloc(sizeof(Node));head->data = 0;head->next = NULL;return head;
}//头插法
int insertHead(Node* L, ElemType e)
// L: 指向链表的头节点
// e: 要插入的数据
{Node* p = (Node*)malloc(sizeof(Node));//初始化新节点p->data = e;// 将新节点 p 的 prev 指向头节点 Lp->prev = L;//头节点的next指向新节点的nextp->next = L->next;if (L->next != NULL){//新节点指向(赋值给)头节点的next的prevL->next->prev = p;}L->next = p;return 1;
}// 获取链表的尾节点(最后一个实际存储数据的节点)
Node* get_tail(Node* L) {if (L == NULL || L->next == NULL) {// 链表为空或只有一个头节点return L; // 返回头节点,插入尾部时会处理}Node* current = L;while (current->next != NULL) {current = current->next;}return current; // current 现在指向最后一个实际节点
}//尾插法
Node* insertTail(Node* tail, ElemType e)
{Node* p = (Node*)malloc(sizeof(Node));p->data = e;p->prev = tail;tail->next = p;p->next = NULL;return p;
}//指定位置插入
int insertNode(Node* L, int pos, ElemType e)
{Node* p = L;int i = 0;while (i < pos - 1){p = p->next;i++;if (p == NULL){return 0;}}Node* q = (Node*)malloc(sizeof(Node));q->data = e;q->prev = p;q->next = p->next;p->next->prev = q;p->next = q;return 1;
}//删除节点
int deleteNode(Node* L, int pos)
{Node* p = L;int i = 0;while (i < pos - 1){p = p->next;i++;if (p == NULL){return 0;}}Node* q = p->next;p->next = q->next;q->next->prev = p;free(q);return 1;
}//遍历
void listNode(Node* L)
{Node* p = L->next;while (p != NULL){printf("%d ", p->data);p = p->next;}printf("\n");}//释放链表
void freeList(Node* L)
{Node* p = L->next;Node* q;while (p != NULL){q = p->next;free(p);p = q; }L->next = NULL;
}int main(int argc, char const* argv[])
{Node* list = initList();insertHead(list, 10);insertHead(list, 20);insertHead(list, 30);listNode(list);Node* tail = get_tail(list);tail = insertTail(tail, 10);tail = insertTail(tail, 20);tail = insertTail(tail, 30);//listNode(list);insertNode(list, 2, 15);listNode(list);deleteNode(list,2);listNode(list);}双向链表 vs. 单链表:异同对比表
| 特性/操作 | 单链表 (Singly Linked List) | 双向链表 (Doubly Linked List) | 主要差异 |
|---|---|---|---|
| 节点结构 | data (数据域) + next (后继指针) | data (数据域) + next (后继指针) + prev (前驱指针) | 双向链表增加前驱指针 |
| 存储空间 | 每个节点比双向链表节点占用空间更少 | 每个节点比单链表节点占用空间更多 | 双向链表额外开销 |
| 遍历方向 | 只能从头节点向前遍历 | 可以从头节点向前遍历,也可以从尾节点向后遍历 | 遍历方向的灵活性 |
| 查找前驱节点 | 效率低:需要从头节点开始遍历 | 效率高:通过 prev 指针可直接访问前驱节点 | 访问前驱的便捷性 |
| 删除节点 | 效率受限:若已知待删除节点,仍需查找其前驱才能完成删除;若只知道值,则需要两次遍历(一次找前驱,一次找节点) | 效率高:若已知待删除节点,直接利用 prev 和 next 指针完成删除,无需查找前驱 | 删除操作的便捷性 |
| 插入节点 | 在已知节点的前/后插入,或在头/尾插入,操作相似 | 在已知节点的前/后插入,或在头/尾插入,操作相似 | 操作复杂度相似 |
| 实现复杂度 | 相对简单 | 相对复杂,需要维护 prev 指针的正确性 | 双向链表实现难度稍高 |
| 应用场景 | 简单的列表、栈、队列等,对前驱访问需求不高 | 需要频繁进行双向查找、查找前驱、或在任意位置高效删除的场景,如浏览器历史记录、LRU 缓存等 | 对访问方向的需求 |
- 最大的区别在于节点结构:双向链表增加了
prev指针,使其能够双向访问。 - 主要优势体现在访问前驱和删除操作:双向链表在这两方面都比单链表更高效和便捷。
- 代价是更高的空间开销和稍高的实现复杂度。
性能对比分析
| 操作类型 | 单链表 | 双向链表 | 性能提升 |
|---|---|---|---|
| 查找前驱 | O(n) | O(1) | 显著 |
| 删除已知节点 | O(n) | O(1) | 显著 |
| 插入到已知节点前 | O(n) | O(1) | 显著 |
| 反向遍历 | 不支持 | O(n) | 新增功能 |
| 空间占用 | 较小 | 增加约33% | 代价 |
总结
双向链表通过增加前驱指针的代价,换来了在多个操作场景下的性能提升。特别适合需要频繁进行双向遍历、前驱访问和任意位置删除的场景。在实际应用中,应根据具体需求权衡空间与时间的取舍,选择最适合的链表结构。
双向链表的实现虽然比单链表复杂,但其提供的操作灵活性和性能优势,使其成为数据结构工具箱中不可或缺的重要组成部分。
