双向链表:从结构到增删改查
一、双向链表:比单向链表更灵活的线性结构
1.1 双向链表的核心结构
双向链表的每个节点包含三个部分:
data:数据域,存储业务数据;next:后继指针,指向下一个节点;prev:前驱指针,指向上一个节点。
用 C 语言结构体定义:
typedef int data_t; // 数据类型别名,方便修改 // 双向链表节点结构 typedef struct node { data_t data; // 数据域 struct node *next; // 后继指针(向后) struct node *prev; // 前驱指针(向前) } node_t;核心优势:
- 支持正向遍历(
head → ... → tail)和逆向遍历(tail → ... → head); - 删除节点时无需知道前驱节点(
prev直接指向它),操作更高效。
二、双向链表的基础操作:创建 + 增删改查
2.1 1. 创建空双向链表
空链表仅包含一个头节点,prev和next都设为NULL:
// 创建空双向链表(返回头节点) node_t *creat_doublist(void) { node_t *phead = (node_t *)malloc(sizeof(node_t)); if (phead == NULL) { printf("内存分配失败!\n"); return NULL; } phead->prev = NULL; // 头节点前驱为NULL phead->next = NULL; // 头节点后继为NULL return phead; }2.2 2. 头插法:在链表头部插入节点
头插法是双向链表最基础的插入方式,新节点直接插在头节点后:
// 头插法:在链表头部插入数据 void doublist_insert_head(node_t *phead, data_t data) { // step1: 创建新节点并赋值 node_t *p_new = (node_t *)malloc(sizeof(node_t)); p_new->data = data; p_new->prev = NULL; p_new->next = NULL; // step2: 链接指针(核心步骤) p_new->next = phead->next; // 新节点后继指向原首节点 p_new->prev = phead; // 新节点前驱指向头节点 phead->next = p_new; // 头节点后继指向新节点(新节点成为首节点) // 如果原链表非空,原首节点的前驱要指向新节点 if (p_new->next != NULL) { p_new->next->prev = p_new; } }指针变化图解:
原链表:head ↔ 2 ↔ 1 ↔ 3 → NULL 插入后:head ↔ 4 ↔ 2 ↔ 1 ↔ 3 → NULL2.3 3. 尾插法:在链表尾部插入节点
尾插法需要先找到尾节点,再将新节点链接到尾部:
// 尾插法:在链表尾部插入数据 void doublist_insert_tail(node_t *phead, data_t data) { // step1: 创建新节点并赋值 node_t *p_new = (node_t *)malloc(sizeof(node_t)); p_new->data = data; p_new->prev = NULL; p_new->next = NULL; // step2: 找到尾节点 node_t *p = phead; while (p->next != NULL) { p = p->next; } // step3: 链接指针 p_new->next = p->next; // 新节点后继为NULL(尾节点特征) p_new->prev = p; // 新节点前驱指向尾节点 p->next = p_new; // 原尾节点后继指向新节点 }2.4 4. 遍历打印:正向 + 逆向
双向链表支持两种遍历方式:
// 正向遍历打印(从首节点到尾节点) void print_doublist(node_t *phead) { if (phead->next == NULL) { printf("链表为空!\n"); return; } node_t *p = phead->next; printf("正向遍历:"); while (p != NULL) { printf("%d ", p->data); p = p->next; } printf("\n"); } // 逆向遍历打印(从尾节点到头节点) void print_doublist_reverse(node_t *phead) { if (phead->next == NULL) { printf("链表为空!\n"); return; } // step1: 先找到尾节点 node_t *p = phead; while (p->next != NULL) { p = p->next; } // step2: 从尾节点逆向打印到头节点 printf("逆向遍历:"); while (p->prev != NULL) { // 直到遇到头节点 printf("%d ", p->data); p = p->prev; } printf("\n"); }2.5 5. 查找节点:根据数据找节点
// 查找节点:根据data值查找,返回节点地址,未找到返回NULL node_t *doublist_find_key(node_t *phead, data_t data) { node_t *p = phead->next; while (p != NULL) { if (p->data == data) { return p; // 找到目标节点 } p = p->next; } return NULL; // 未找到 }2.6 6. 修改节点:根据旧值修改为新值
// 修改节点:将old_data修改为new_data,返回修改后的节点 node_t *doublist_update_key(node_t *phead, data_t old_data, data_t new_data) { node_t *p = doublist_find_key(phead, old_data); if (p == NULL) { printf("未找到要修改的节点!\n"); return NULL; } p->data = new_data; return p; }2.7 7. 删除节点:根据数据删除节点
双向链表删除节点的核心是修改前后节点的指针:
// 删除节点:根据data值删除节点 int doublist_delete_key(node_t *phead, data_t data) { node_t *p = doublist_find_key(phead, data); if (p == NULL) { printf("未找到要删除的节点!\n"); return -1; } // step1: 前驱节点指向后继节点 p->prev->next = p->next; // step2: 如果不是尾节点,后继节点的前驱要指向当前节点的前驱 if (p->next != NULL) { p->next->prev = p->prev; } // step3: 释放节点内存 free(p); return 0; }2.8 8. 统计链表长度
// 统计链表有效节点个数 int length(node_t *phead) { int count = 0; node_t *p = phead->next; while (p != NULL) { count++; p = p->next; } return count; }三、完整测试代码
#include <stdio.h> #include <stdlib.h> typedef int data_t; typedef struct node { data_t data; struct node *next; struct node *prev; } node_t; // 1. 创建空链表 node_t *creat_doublist(void) { node_t *phead = (node_t *)malloc(sizeof(node_t)); phead->prev = NULL; phead->next = NULL; return phead; } // 2. 头插法 void doublist_insert_head(node_t *phead, data_t data) { node_t *p_new = (node_t *)malloc(sizeof(node_t)); p_new->data = data; p_new->prev = NULL; p_new->next = NULL; p_new->next = phead->next; p_new->prev = phead; phead->next = p_new; if (p_new->next != NULL) { p_new->next->prev = p_new; } } // 3. 尾插法 void doublist_insert_tail(node_t *phead, data_t data) { node_t *p_new = (node_t *)malloc(sizeof(node_t)); p_new->data = data; p_new->prev = NULL; p_new->next = NULL; node_t *p = phead; while (p->next != NULL) { p = p->next; } p_new->next = p->next; p_new->prev = p; p->next = p_new; } // 4. 正向打印 void print_doublist(node_t *phead) { if (phead->next == NULL) { printf("链表为空!\n"); return; } node_t *p = phead->next; printf("正向遍历:"); while (p != NULL) { printf("%d ", p->data); p = p->next; } printf("\n"); } // 5. 逆向打印 void print_doublist_reverse(node_t *phead) { if (phead->next == NULL) { printf("链表为空!\n"); return; } node_t *p = phead; while (p->next != NULL) { p = p->next; } printf("逆向遍历:"); while (p->prev != NULL) { printf("%d ", p->data); p = p->prev; } printf("\n"); } // 6. 查找节点 node_t *doublist_find_key(node_t *phead, data_t data) { node_t *p = phead->next; while (p != NULL) { if (p->data == data) { return p; } p = p->next; } return NULL; } // 7. 修改节点 node_t *doublist_update_key(node_t *phead, data_t old_data, data_t new_data) { node_t *p = doublist_find_key(phead, old_data); if (p == NULL) { return NULL; } p->data = new_data; return p; } // 8. 删除节点 int doublist_delete_key(node_t *phead, data_t data) { node_t *p = doublist_find_key(phead, data); if (p == NULL) { return -1; } p->prev->next = p->next; if (p->next != NULL) { p->next->prev = p->prev; } free(p); return 0; } // 9. 统计长度 int length(node_t *phead) { int count = 0; node_t *p = phead->next; while (p != NULL) { count++; p = p->next; } return count; } // 10. 销毁链表 void doublist_destroy(node_t *phead) { node_t *p = phead; while (p != NULL) { node_t *temp = p; p = p->next; free(temp); } } // 测试主函数 int main() { node_t *phead = creat_doublist(); // 头插法插入数据 doublist_insert_head(phead, 3); doublist_insert_head(phead, 1); doublist_insert_head(phead, 2); printf("链表长度:%d\n", length(phead)); // 输出3 print_doublist(phead); // 正向:2 1 3 print_doublist_reverse(phead); // 逆向:3 1 2 // 尾插法插入数据 doublist_insert_tail(phead, 4); print_doublist(phead); // 正向:2 1 3 4 // 修改数据 doublist_update_key(phead, 1, 10); print_doublist(phead); // 正向:2 10 3 4 // 删除数据 doublist_delete_key(phead, 10); print_doublist(phead); // 正向:2 3 4 doublist_destroy(phead); return 0; }四、双向链表 vs 单向链表:核心区别
| 特性 | 单向链表 | 双向链表 |
|---|---|---|
| 指针数量 | 1 个(next) | 2 个(next+prev) |
| 遍历方向 | 仅正向 | 正向 + 逆向 |
| 删除节点 | 需要知道前驱节点 | 直接通过prev找到前驱 |
| 内存开销 | 小 | 大(每个节点多一个指针) |
| 适用场景 | 简单遍历、栈实现 | 需要双向操作、队列实现、缓存设计 |
五、总结与学习建议
- 核心抓住指针操作:双向链表的关键是
prev和next的链接,插入 / 删除时一定要画图理解指针变化; - 边界情况要注意:
- 头插 / 尾插时,空链表和非空链表的处理逻辑不同;
- 删除尾节点时,
next为NULL,无需处理后继节点的prev;
- 实践建议:
- 先手写插入 / 删除的步骤(画指针流向),再敲代码;
- 测试边界情况:空链表、单节点链表、首尾节点操作;
- 结合 LeetCode 刷题,比如「回文链表」「链表排序」等题目,双向链表能简化实现。
