LeetCode 剑指 Offer 22. 链表中倒数第k个节点
题目描述

题意分析
链表从尾部向前数,尾节点是倒数第
1个,要求返回倒数第k个节点本身。返回后,这个节点原有的后继关系仍然保留,不需要截断链表,也不是只返回节点值。单链表只能沿
next向后移动,不能从尾部直接倒着数。题目保证链表非空,且1 <= k <= n,其中n是链表长度,因此目标节点一定存在。
解法:快慢指针保持 k 步间距
核心思路
[!blue]
如果知道链表长度
n,倒数第k个节点就是从头开始的第n - k + 1个节点,也就是从头节点前进n - k步的位置。关键是不用提前遍历一遍计算n,仍然让一个指针恰好走这n - k步。让
fast、slow都从头节点出发,先让fast单独前进k步。此后两者每轮都前进一步,它们之间就始终保持k次后继跳转的距离;这里的间距按跳转次数计算,不是两指针之间夹着的节点个数。从头节点走到尾节点后的空位置一共需要
n步。fast已经预先走了k步,剩下恰好还要走n - k步,所以同步阶段结束时,slow也只前进了n - k步,正好停在目标节点。这套写法让
fast最终停在空位置,预走步数就必须是k。当k = n时,预走后fast已经为空,slow不再移动,直接返回头节点;当k = 1时,slow会停在尾节点。
解题步骤
- 将
fast、slow都初始化为头节点。- 让
fast单独前进k步。题目保证k有效,前进过程中不会提前访问空节点的后继。- 只要
fast还不为空,就让两个指针各前进一步,保持原来的间距。fast到达空位置时停止,返回slow指向的节点。
代码实现
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
// 从头节点建立 k 步间距,同步走到快指针为空时慢指针即为目标。
for (int i = 0; i < k; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
func getKthFromEnd(head *ListNode, k int) *ListNode {
fast, slow := head, head
// 从头节点建立 k 步间距,同步走到快指针为空时慢指针即为目标。
for i := 0; i < k; i++ {
fast = fast.Next
}
for fast != nil {
fast = fast.Next
slow = slow.Next
}
return slow
}
复杂度分析
- 时间复杂度:$O(n)$,快指针总共前进
n步,慢指针前进n - k步。- 空间复杂度:$O(1)$,只使用两个指针,不保存节点集合或递归栈。
关键点总结
[!green]
- 倒数位置可以转成从头前进
n - k步,快指针负责感知何时已经走满n步。- 预先领先
k步,再同步移动,就让慢指针少走恰好k步。- “预走
k步”与“快指针停在空位置”是一组对应条件,不能单独修改其中一项。
易错点总结
[!yellow]
- 在本写法中只预走
k - 1步,会让慢指针多走一步,答案向后偏移;若采用另一种间距定义,终止位置也必须相应改变。- 同步循环不能写成
fast.next != null:这会提前停在尾节点,而且k = n时fast本身已经为空。- 两个指针必须同步移动,只更新快指针会破坏“慢指针走
n - k步”的推导。- 返回的是原链表中的节点,不能只返回它的值,也不能为了显示单个节点而断开后继。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 19. 删除链表的倒数第 N 个结点 | 中等 | 固定间隔指针可复用,原题删除节点需要前驱,本题直接返回倒数第k个节点。 |
| 面试题 02.02. 返回倒数第 k 个节点 | 简单 | 定位相同,原题只返回值,本题返回节点并保留它后面的链接。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!