空指针与虚拟头节点
目录
一句话 虚拟头 dummy 是真实存在的节点,有地址,所以可以通过
dummy->next修改它指向的节点;而nullptr只是一个"值",可以用它的值,但不能解引用——读不行,写也不行。
1 我卡住的点
- 反转链表里
pre = nullptr没问题,为什么两两交换里必须new一个 dummy? - 结论:区别在于有没有操作
pre->next(解引用)。
2 精确的规则
空指针的值可以用,空指针本身不能被解引用:
| 代码 | 合法吗 | 解释 |
|---|---|---|
ListNode* pre = nullptr; cur->next = pre; | ✅ | 只把 nullptr 的值赋给 cur->next,操作的是 cur |
pre->next = cur; | ❌ 崩溃 | 解引用空指针并修改 |
int v = pre->val; | ❌ 崩溃 | 解引用空指针,读取也不行 |
ListNode* dummy = new ListNode(0); dummy->next = b; | ✅ | dummy 是真实节点,有地址,可以修改它的 next |
不要记成"可以调用但不能修改" 更准确的说法:可以用空指针的值,但不能解引用空指针。解引用不管是读还是写都会崩。
cur->next = pre之所以安全,是因为这里操作的是cur->next(cur 是真实节点),pre只是被当作一个值。
3 两个场景对照
| 场景 | 为什么 |
|---|---|
反转链表:pre = nullptr 够用 | 全程只把 pre 的值赋给 cur->next,没有 pre->next 这种操作 |
| 两两交换 / 删除节点:必须 new dummy | 需要写 prev->next = b,必须通过真实节点修改 |
3.1 虚拟头模式的标准结构(我的理解)
| |
三个角色分工:
| 角色 | 干什么 | 位置变化 |
|---|---|---|
dummyHead | 哨兵,站在头节点前面 | 永远不动 |
cur | 干活(遍历、删除、交换) | 一直往前走 |
dummyHead->next | 返回用:当前的真实头节点 | 头被删/被换时自动跟着变 |
关键理解:
- 删除、交换过程中真实头可能会变,但不需要单独维护"头指针"——dummy 没动过,
dummyHead->next永远指向当前的真实头。 - 返回时用
dummyHead->next,不能用cur:cur 走到最后已经在链表末尾(甚至 nullptr)了。 - 哨兵不参与数据,所以它的
val随便给(0);它的存在只是为了给头节点提供一个"前一个节点"。 - 一句话:dummy 站岗,cur 干活,dummy->next 交卷。
3.2 dummy 不一定要 new:可以用栈对象
链表连接靠的是 next 指针(存地址),但节点本身可以放在栈上,不用 new:
| |
- 题解常写
new ListNode(0)只是图省事(LeetCode 不检查内存泄漏),本地代码用栈对象更安全,不用手动delete。 - 注意:不能把局部对象的地址返回出去(函数结束就销毁)——但 dummy 不会被返回,返回的是
dummy.next,安全。 - 反转链表反而不能用真实节点当
pre:最后链表尾巴会指向这个假节点,判题器释放时会崩。这再次印证:只读场景用nullptr,要改next才用真实节点。
4 记忆
- dummy = 真实节点 = 有一扇可以打开的门(可以改它的 next)。
- nullptr = 值 = 可以借给别人当目标,但它自己没有任何房间,不能开门(不能解引用)。
5 相关笔记
- Q206 反转链表(pre = nullptr 当虚拟头)
- Q24 两两交换链表中的节点(必须 new dummy)
- 指针交接瞬间(复制指针 vs 插入)
- Q707 设计链表
- 链表常用操作总结
6 自测
- 能说出
cur->next = pre(pre 为空)为什么合法 - 能说出
pre->next = cur(pre 为空)为什么崩溃 - 能判断一道新题:是"读前一个节点的值"还是"改前一个节点的 next",从而决定用 nullptr 还是 dummy
- 能用栈对象写 dummy 版本(
ListNode dummy(0); ListNode* prev = &dummy;) - 能说出返回时为什么用
dummyHead->next而不是cur