链表基础:哨兵节点 与 O(1) 删除
链表 (linked list) 不像数组那样把元素放在连续内存里,而是每个 node 自带一个 next 指向下一个。代价是不能随机访问第 i 个(要从头逐个 next 前进,);好处是只要持有某个节点的引用,在它附近插入 / 删除只需改写几个指针,
完成——数组做同样的事要搬移大段元素。本页讲三个围绕指针的基本功:用 哨兵 (dummy head) 消除「删头节点」的特判;不持有前驱也能 O(1) 删除指定节点;以及 Linus 用来定义 “good taste” 的 二级指针 (pointer-to-pointer)——让删头与删中间收敛成同一行、彻底没有特判。
哨兵 (sentinel / dummy head) 是什么?在真正的头节点前面挂一个不存数据的占位节点 dummy,让 dummy.next 永远指向链表头。这样「删除头节点」就和「删除中间节点」长得一模一样——都是 prev.next = cur.next,prev 初始就是
dummy,不再需要为「要删的是不是第一个」单独写一段 if。关掉上面的开关,把要删的值设成第一个,对比不带哨兵时代码多出的特判。
O(1) 删除的「覆盖后继」技巧:正常删一个节点要先找到它的前驱(
遍历)。但若只持有待删节点 p 本身、不知道表头:把后继的值复制到 p 里 (p.val = p.next.val),再让 p 跳过后继 (p.next = p.next.next)——实际删除的是后继节点,对外的效果等同于删掉了
p。
完成。唯一限制:p 是尾节点(没有后继可复制),这时只能
遍历找前驱。
延伸:Linus 的 “good taste”——二级指针 (pointer-to-pointer) 在一次访谈里,Linus 用「从单链表删除一个节点」举例什么才是真正的底层功力。坏品味的写法要为头节点单独写一段 if(因为头节点没有前驱);好品味的写法引入一个指向指针的指针
indirect,它对准的不是节点、而是**「将被改写的那个指针槽」——对头节点这个槽是 head 本身,对中间节点是 prev->next,两者类型都是 Node **。于是删除动作 *indirect = entry->next 对两种情况是同一行**,特殊情况就此消失。上方二级指针 / (owner,key) 槽模式即此思路的可单步演示;它和哨兵 dummy head 是「消除删头特判」的两种手法。
JS 里有没有二级指针? 没有字面意义的取址 &,变量本身不可寻址——let x = head; x = … 只改局部变量,碰不到 head 那个存储位置;对象引用也是按值传递引用。但 Linus 真正用到的能力是**「持有一个可读可写的指针槽」,这在 JS 里有等价物:唯一能被共享、能原地改写的存储单元是对象的属性**,于是 JS 的「指针槽」= (对象, 属性名) 二元组——owner[key] 既能读又能写,正好扮演 *indirect。两个前提:其一,头指针也得是某个对象的属性(如 list.head),才能和 node.next 被
(owner, key) 一致地覆盖;其二,JS 的对象引用本身已提供一级堆上间接性,这「第二级」不是靠地址,而是靠把槽拆成 (对象, 属性名) 来表达。实践中更常用哨兵 dummy head 达到同样效果、可读性更好;(owner, key) 槽式写法是上面那段 C 的忠实翻译。