单链表与双链表

一、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位系统)

类型数据nextprev总大小
单向链表节点4B8B12B
双向链表节点4B8B8B20B

结论

  • 双向链表 内存多 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),但双向链表付出的是双倍内存和更复杂的指针维护代价。工程上优先单向链表,只在必须快速随机删除时使用双向链表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值