题目描述
删除链表的倒数第 n 个结点,返回头结点。
思路解析
先遍历数出长度 len,再从头走 len−n 步找到待删节点的前驱——逻辑没错,但要把链表走两趟。能不能一趟就搞定?
转折:从 dummy 哨兵出发,让 fast 先独自走 n+1 步,于是 fast 比 slow 领先 n+1 个节点。然后两个一起走,当 fast 跨出尾部(变 null)时,slow 正好落在「倒数第 n 个」的前一个,删它就方便了。
起点 · slow=fast=dummy(头):slow、fast 都从哨兵后的头节点 1 出发(图上重合在头)。下面每一步分开移动看得清楚。
fast 先走第 1 步:fast 单独走第 1 步到 2,slow 还在 1。先把 fast 拉开。
fast 先走第 2 步 · 拉开间距 n:fast 再走第 2 步到 3。fast 比 slow 领先 n = 2 个节点(slow 在 1、fast 在 3)。这个间距之后会一直保持,两个指针同步走。
同步走第 1 步 · slow→2, fast→4:现在两个一起走一步:slow 到 2,fast 到 4。间距仍是 2,丝毫不变。
判循环条件 · fast 还没出界:进下一轮前先看条件 while fast:fast 在 4(还不是 null),条件成立,再走一步。slow 在 2、fast 在 4,间距还是 2。
同步走第 2 步 · fast 到尾:再一起走一步:slow 到 3,fast 到最后一个 5。下一步 fast 会跨出尾部变 null,while fast 不成立、循环停。slow 停在 3,它的下一个 del=4 正是倒数第 2 个——要删的就是它。
删除 · slow.next = slow.next.next:执行 slow.next = slow.next.next:把 4 摘掉(虚线划掉),3 的箭头越过它直接接到 5。链表变成 1 → 2 → 3 → 5。
边界 · 若要删的是头结点:反向想个边界:若 n=5 要删倒数第 5 个(就是头节点 1)。多亏从 dummy 哨兵出发,slow 会停在 dummy 上,dummy.next 越过 1 接到 2,头节点照样能删——这正是哨兵存在的意义。
让两个指针保持 n 的间距一起走,领头的到尾、跟随的就到了「倒数第 n」的位置——这把「从后数」巧妙转成「一起走到头」。加哨兵则让删头不再是特例。
参考代码(Python)
1def removeNthFromEnd(self, head, n):
2 dummy = ListNode(0, head) # 哨兵,统一处理删头
3 slow = fast = dummy
4 for _ in range(n + 1): # fast 先走 n+1 步
5 fast = fast.next
6 while fast: # 同步走到 fast 出界
7 slow = slow.next
8 fast = fast.next
9 slow.next = slow.next.next # 摘除倒数第 n 个
10 return dummy.next复杂度分析
- 时间复杂度:O(L) —— L 是链表长度,指针总共扫一趟
- 空间复杂度:O(1) —— 只用 dummy、slow、fast 几个指针
套路模板
记住骨架:哨兵 + fast 先走 k+1 步 + 同步到出界,slow 就停在倒数第 k 的前驱。删除、找倒数第 k 个都套它。
1dummy = ListNode(0, head) # 删除类先挂哨兵
2slow = fast = dummy
3for _ in range(k + 1): # 先拉开 k+1 的间距
4 fast = fast.next
5while fast: # 一起走到 fast 出界
6 slow, fast = slow.next, fast.next
7# 此时 slow 在倒数第 k 个的前一个易错点
- 错误写法:fast 只先走 n 步、循环用 while fast.next → 正确写法:从 dummy 走 n+1 步、循环用 while fast(两种写法都对但必须配套:基准点和步数错配 1,slow 就会停错位置删错节点)
- 错误写法:不加哨兵,删头时崩 → 正确写法:用 dummy 指向 head(删的是头结点时它没有前驱,哨兵恰好补上这个前驱)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。