一、prev前一个
prev 是 previous 的缩写,意思是“前一个”。
在双向链表的节点结构中,prev 是一个指针,它指向当前节点的上一个节点。
一、直观理解
c
struct DNode {
int data; // 数据
struct DNode *next; // 指向下一个节点
struct DNode *prev; // 指向上一个节点 ← 这个就是 prev
};
图示
text
prev指向 next指向
←─── ───→
┌──────┐ ┌──────┐ ┌──────┐
│ Node │ → │ Node │ → │ Node │
│ A │ ← │ B │ ← │ C │
└──────┘ └──────┘ └──────┘
↑ ↑ ↑
prev prev prev
(指向null) (指向A) (指向B)
二、prev 的具体作用
1. 反向遍历(从尾到头)
c
// 从最后一个节点往前遍历
DNode *p = tail;
while (p != NULL) {
printf("%d ", p->data);
p = p->prev; // 通过 prev 向前走
}
2. 删除节点(不需要找前驱)
c
// 假设要删除 p 指向的节点 p->prev->next = p->next; // 让前一个节点的 next 跳过 p p->next->prev = p->prev; // 让后一个节点的 prev 跳过 p free(p);
关键:没有 prev,你就不知道"前一个节点是谁"。
3. 插入节点
c
// 在 p 之前插入新节点 newNode newNode->prev = p->prev; newNode->next = p; p->prev->next = newNode; p->prev = newNode;
三、对比单向链表(没有 prev)
c
// 单向链表节点
struct Node {
int data;
struct Node *next; // 只有 next,没有 prev
};
限制:
-
❌ 无法反向遍历
-
❌ 删除节点必须从头找到前驱
-
❌ 无法直接知道"上一个是谁"
四、一个完整的例子
c
#include <stdio.h>
#include <stdlib.h>
struct DNode {
int data;
struct DNode *prev;
struct DNode *next;
};
int main() {
// 创建三个节点
struct DNode *n1 = malloc(sizeof(struct DNode));
struct DNode *n2 = malloc(sizeof(struct DNode));
struct DNode *n3 = malloc(sizeof(struct DNode));
n1->data = 10;
n1->prev = NULL; // 头节点的 prev 是 NULL
n1->next = n2;
n2->data = 20;
n2->prev = n1; // n2 的 prev 指向 n1
n2->next = n3;
n3->data = 30;
n3->prev = n2; // n3 的 prev 指向 n2
n3->next = NULL; // 尾节点的 next 是 NULL
// 反向遍历(通过 prev)
printf("反向遍历: ");
struct DNode *p = n3; // 从尾节点开始
while (p != NULL) {
printf("%d ", p->data);
p = p->prev; // 通过 prev 向前移动
}
// 输出:30 20 10
return 0;
}
五、prev 的常见面试/代码陷阱
陷阱1:修改 prev 前要判空
c
// ❌ 危险(如果 p 是头节点,p->prev 是 NULL)
p->prev->next = p->next; // 崩溃!
// ✅ 正确
if (p->prev != NULL)
p->prev->next = p->next;
else
head = p->next; // p 是头节点,更新 head
陷阱2:插入时忘记维护 prev
c
// ❌ 错误:只改了 next,没改 prev
newNode->next = p->next;
p->next = newNode;
// ✅ 正确:双向都要维护
newNode->prev = p;
newNode->next = p->next;
if (p->next != NULL)
p->next->prev = newNode;
p->next = newNode;
六、总结
| 问题 | 答案 |
|---|---|
| prev 是什么 | 指向前一个节点的指针 |
| prev 的作用 | 反向遍历、O(1)删除、O(1)插入 |
| 单向链表有 prev 吗 | 没有 |
| 头节点的 prev 是什么 | NULL |
| 删除时为什么要用 prev | 让前一个节点绕过当前节点 |
一句话记忆:
prev就是双向链表里的"倒车镜",让你知道后面来的车(节点)是谁,从而不需要掉头去找。
二、单向链表 vs 双向链表
先说结论:
双向链表删除“已知节点”确实比单向链表方便很多(O(1) vs O(n)),
但这个“方便”是有代价的:内存翻倍 + 维护两个指针的复杂度。
下面从 6 个维度深入讲清楚。
一、删除操作的本质区别(你关心的核心)
✅ 双向链表:删除任意节点(O(1))
已知条件:已经拿到要删除的节点 p
c
// 假设不是头 / 尾(完整通用写法)
p->prev->next = p->next;
if (p->next)
p->next->prev = p->prev;
free(p);
✅ 不需要遍历
✅ 3~4 行代码搞定
❌ 单向链表:删除任意节点(O(n))
已知条件:已经拿到要删除的节点 p
c
// 你必须从头找 p 的前一个节点
Node *prev = head;
while (prev->next != p) {
prev = prev->next; // 最坏情况 O(n)
}
prev->next = p->next;
free(p);
❌ 必须从头遍历
❌ 无法“原地删除”
这是两者在删除操作上最本质、最直观的区别
二、为什么会有这个差异?
原因非常底层:
单向链表的“信息不对称”
-
每个节点只知道 “后面是谁”
-
不知道 “前面是谁”
-
删除时需要修改前一个节点的
next,但你找不到前一个节点
双向链表的信息完整
-
每个节点知道:
-
前一个节点(
prev) -
后一个节点(
next)
-
-
删除时可以双向调整
一句话:
单向链表删除 = 需要“找人”
双向链表删除 = 直接“操作邻居”
三、一个更现实的例子:删除“中间某个值”
场景
链表:10 → 20 → 30 → 40
删除 30
双向链表
c
p = find(head, 30); // O(n) // 删除 O(1) p->prev->next = p->next; p->next->prev = p->prev; free(p);
总复杂度:O(n) 查找 + O(1) 删除
单向链表
c
prev = NULL;
p = head;
while (p && p->data != 30) {
prev = p;
p = p->next;
}
// 删除 O(1)
prev->next = p->next;
free(p);
总复杂度:O(n) 查找 + O(1) 删除
👉 在“按值删除”场景下,两者查找成本一样
👉 区别只在:是否提前持有节点指针
四、内存与性能的真实代价(很多人忽略)
内存对比(64位系统)
| 类型 | 数据 | next | prev | 总大小 |
|---|---|---|---|---|
| 单向链表节点 | 4B | 8B | 无 | 12B |
| 双向链表节点 | 4B | 8B | 8B | 20B |
结论:
-
双向链表 内存多 67%
-
100 万个节点 → 多 8MB(看似不多,但在嵌入式 / 内核态很敏感)
缓存局部性(重要但常被忽略)
-
单向链表:遍历时只访问
next -
双向链表:
-
遍历时访问
next -
删除 / 插入时访问
prev+next -
随机访问更差
-
👉 双向链表 更可能触发 cache miss
五、哪些操作单向链表反而“更简单 / 更稳”?
| 操作 | 单向链表 | 双向链表 |
|---|---|---|
| 反转链表 | 经典 3 指针,稍复杂 | 更复杂(要同时交换 prev/next) |
| 插入到头部 | ✅ 简单 | ✅ 简单(但多一步 prev) |
| 判断环 | ✅ 快慢指针 | ✅ 同样能做 |
| 合并两个有序链表 | ✅ 简单 | ✅ 简单 |
| 找倒数第 k 个 | 快慢指针(O(n)) | 可从 tail 直接走(O(k)) |
👉 单向链表在面试题 / 竞赛 / 简单场景中更常用
六、真实项目中的选择(别纸上谈兵)
✅ 用单向链表的场景(大部分情况)
-
✅ 哈希桶(拉链法)
-
✅ 栈、队列
-
✅ 邻接表(图算法)
-
✅ 内存池的空闲块管理
-
✅ 一次性遍历的批处理
理由:
内存省 + 代码简单 + 足够用
✅ 用双向链表的场景
-
✅ LRU 缓存淘汰算法(经典场景)
-
✅ 浏览器的前进 / 后退
-
✅ 操作系统的进程调度(可快速删除任意进程)
-
✅ 实现
deque(双端队列) -
✅ 编辑器 Undo / Redo
理由:
必须 O(1) 删除任意节点
七、一个真实代码级的对比(建议你跑一遍)
双向链表删除(标准写法)
c
void delete_node(DNode **head, DNode *del) {
if (*head == NULL || del == NULL) return;
if (*head == del)
*head = del->next;
if (del->next)
del->next->prev = del->prev;
if (del->prev)
del->prev->next = del->next;
free(del);
}
单向链表删除(等价场景)
c
void delete_node(Node **head, Node *del) {
if (*head == NULL || del == NULL) return;
if (*head == del) {
*head = del->next;
free(del);
return;
}
Node *prev = *head;
while (prev->next != del)
prev = prev->next;
prev->next = del->next;
free(del);
}
👉 后者多了一个 while 循环
八、最终结论(给你一个清晰的决策树)
text
是否需要频繁删除“已知节点”?
│
├─ 是 → 双向链表(LRU / 内核 / 编辑器)
│
└─ 否 → 继续问:
│
├─ 内存紧张? → 单向链表
├─ 需要反向遍历? → 双向链表
├─ 代码要极简? → 单向链表
└─ 综合 → 单向链表(默认选择)
一句话总结(面试 / 回答用)
双向链表删除任意节点的时间复杂度是 O(1),单向链表是 O(n),但双向链表付出的是双倍内存和更复杂的指针维护代价。工程上优先单向链表,只在必须快速随机删除时使用双向链表。

3065

被折叠的 条评论
为什么被折叠?



