题目描述
思路解析动画文字版
头插、删头都会动到「第一个节点」。如果直接拿 head 操作,每次都要特判头节点。哑头 dummy 永远站在最前、永不被删,让头节点和中间节点一视同仁,代码立刻清爽。
维护一个 size 记录当前节点个数:get/delete 越界直接返回,addAtTail 其实就是 addAtIndex(size)。一个变量省掉一堆边界纠结。
起点 · 空链表:一开始链表是空的:只有一个哑头 D,它的 next 指向 null。size = 0。下面开始一串操作。
addAtHead(1) · pred 站哑头:头插就是在第 0 位前插入。先让 pred 站在第 0 位的前一个,也就是哑头 D 上(走 0 步,原地不动)。
addAtHead(1) · 新节点接线:造一个值为 1 的新节点:先让它的 next 接上 pred 原来的 next(这里是 null),再让 pred.next 指向它。先接后、再接前——顺序反了会断链。size 加到 1。
addAtTail(3) · 等价 addAtIndex(size):尾插就是 addAtIndex(size),现在 size=1,要在第 1 位前插。pred 先回到哑头 D,准备往后走到尾节点的位置。
addAtTail(3) · pred 走 1 步:要插在第 1 位前,pred 就从哑头往后走 1 步,停在节点 1 上。此刻 pred 后面是 null,正好是链表尾。
addAtTail(3) · 尾部接上:同样的接线:新节点 3 的 next 接 pred 原来的 next(null),pred.next 再指向它。链表变成 1→3,size = 2。
addAtIndex(1, 2) · pred 回哑头:现在要在第 1 位前插一个 2,也就是塞进 1 和 3 中间。老规矩,pred 先站回哑头 D。
addAtIndex(1, 2) · pred 走 1 步:往后走 1 步,pred 停在节点 1。新节点要插在 pred 和 pred 后面那个节点(3)之间。
addAtIndex(1, 2) · 中间插入:关键的中间接线:新节点 2 先把 next 接到 pred 原来的下一个(节点 3),pred.next 再改指向 2。于是 1→2→3 接通,谁都没丢。size = 3。
链表当前状态:三次插入后,链表是 1→2→3。注意哑头 D 始终不动,它的 next 才是真正的头。接下来查一个值。
get(1) · cur 从真头出发:get 不用哑头当起点,直接从第 0 个真节点开始数。cur 先指向节点 1(下标 0)。
get(1) · cur 走 1 步:要第 1 个,就让 cur 往后走 1 步,停在下标 1 的节点上。
get(1) · 读出值 = 2:cur 现在指着的节点值就是答案:get(1) = 2。如果 index 越界(<0 或 ≥size),直接返回 -1,连走都不走。
deleteAtIndex(1) · pred 回哑头:删除第 1 个节点,依然是先找它的前一个。pred 站回哑头 D。
deleteAtIndex(1) · pred 走 1 步:pred 往后走 1 步停在节点 1。pred 后面紧跟的节点 2,就是要删掉的那个。
deleteAtIndex(1) · 标红待删:把要删的节点 2 标红。删除的本质就是:让 pred 直接绕过它,指向它的下一个。
deleteAtIndex(1) · 一刀绕过:关键一行:pred.next = pred.next.next,让节点 1 直接指向节点 3,节点 2 被整个绕过,从链表里消失。size 减到 2。
get(1) · cur 回到真头:删完再 get(1):和上次一样,cur 先回到第 0 个真节点(现在是节点 1)。
get(1) · cur 走 1 步:往后走 1 步。删掉 2 之后,下标 1 的位置已经换成了节点 3。
get(1) · 读出值 = 3:删除后链表是 1→3,所以 get(1) = 3。整串操作演完:五种方法,靠的全是「找前驱 + 改指针」这一套。
一句话记牢:无论增还是删,先把 pred 停到目标位置的前一个,插入是「新节点先接后、再接前」,删除是「pred 跳过下一个」。
雷区实演 · 接线顺序反了会断链:假设要在 1 后插新节点。如果先把 pred.next 改指向新节点,pred 原来通往 3 的那根线就被覆盖、再也找不回 3(变成游离节点)。所以必须新节点先 next=pred.next,把 3 接住,再让 pred 指向新节点。
边界三连:记准边界:get/delete 是 index ≥ size 越界;而 addAtIndex 允许 index == size(正好接在尾部),只有 index > size 才忽略。
面试追问 1:用一个变量换掉重复遍历,是工程里常见的「空间/记账换时间」思路。
面试追问 2:单链表尾插是 O(n)。加 tail 指针能降到 O(1),代价是维护成本——面试常考这个权衡。
面试追问 3:本题单/双向都可。双向链表删除更灵活、遍历可选方向,但每个节点多存一根指针。
把链表的指针接线练熟,去 链表专题 连刷同类题;卡住时可以随时在页面里提问「这根指针为什么这么接」,比硬背更扎实。
参考代码
class Node: def __init__(self, val): self.val = val self.next = Noneclass MyLinkedList: def __init__(self): self.dummy = Node(0) self.size = 0 def get(self, index): if index < 0 or index >= self.size: return -1 cur = self.dummy.next for _ in range(index): cur = cur.next return cur.val def addAtHead(self, val): self.addAtIndex(0, val) def addAtTail(self, val): self.addAtIndex(self.size, val) def addAtIndex(self, index, val): if index > self.size: return if index < 0: index = 0 pred = self.dummy for _ in range(index): pred = pred.next node = Node(val) node.next = pred.next pred.next = node self.size += 1 def deleteAtIndex(self, index): if index < 0 or index >= self.size: return pred = self.dummy for _ in range(index): pred = pred.next pred.next = pred.next.next self.size -= 1复杂度
- get / addAtIndex / delete:O(index),最坏要从头走到目标位置
- addAtHead:O(1),index=0,pred 原地不动
- 空间复杂度:O(n),n 个节点各占一块
可套用的代码模板
把这套「dummy + 走到前驱 + 改指针」的骨架记死,几乎所有单链表增删题都是它的变体——换的只是「走几步」和「改哪根指针」。
# 万能起手:哑头 dummy + sizeself.dummy = Node(0); self.size = 0# 走到第 index 位的前驱 predpred = self.dummyfor _ in range(index): pred = pred.next# 插入:新节点先认下家,pred 再认它node.next = pred.next; pred.next = node # 顺序别反# 删除:pred 跳过下一个pred.next = pred.next.next易错点
面试追问把动画讲成自己的话
追问为什么要维护 size,不能每次遍历数长度吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
K 个一组翻转链表
LeetCode 25 · 困难 · 沿着 链表套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题