LeetCode 面试题 02.02. 返回倒数第 k 个节点
题目描述

题意分析
给一条单向链表的头节点和一个正整数
k,要返回从末尾数起第k个节点所存的值。倒数第 1 个指的是最后一个节点,倒数第k个后面还跟着k - 1个节点。麻烦之处在于链表是单向的:只有
next指针,没有prev,也没有随机访问和长度字段。想知道「离末尾多远」,就必须先以某种方式感知到末尾在哪,而末尾只能靠向前走到空指针才能发现。题目明确保证
k是有效的,也就是链表长度至少为k,所以主逻辑不必为越界兜底;但如果面试官临时追加「k可能非法」的条件,就得补上提前判空的分支。另外注意返回类型是节点的值而不是节点本身,写成返回节点会直接编译不过。边界情形包括k恰好等于链表长度(答案就是头节点)和k等于 1(答案是尾节点)。
解法:快慢指针固定间距
核心思路
问题关键: 单链表不能从尾部向前数。两趟法先求长度再定位虽然正确,但“倒数第
k个”只需要相对距离,不需要知道总长度。为什么用快慢指针: 让
fast先走k步,再让fast、slow同步前进。同步阶段始终有:从slow出发走k次next正好到fast。正确性: 两个指针每轮各走一步,固定间距保持不变。当
fast到达空指针时,从slow到空指针恰好还有k步,所以slow后面有k - 1个节点,它正是倒数第k个节点。题目保证
k合法,因此主代码不增加越界分支;若面试官取消该保证,应在预走阶段检查fast == null,并同时约定空链表、k <= 0的返回方式。
解题步骤
fast、slow都从头节点出发。- 让
fast先走恰好k步,建立固定间距。- 当
fast非空时,两指针同步前进一步。fast为空时返回slow.val。口述示例:
1 -> 2 -> 3 -> 4 -> 5、k = 2。fast先到节点 3,随后两指针同步走;fast越过节点 5 时,slow停在节点 4。边界:
k = 1时slow最终停在尾节点;k等于链表长度时,fast预走后已经为空,slow保持在头节点。
代码实现
class Solution {
public int kthToLast(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
for (int i = 0; i < k; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow.val;
}
}
func kthToLast(head *ListNode, k int) int {
fast, slow := head, head
for i := 0; i < k; i++ {
fast = fast.Next
}
for fast != nil {
fast = fast.Next
slow = slow.Next
}
return slow.Val
}
复杂度分析
- 时间复杂度:$O(n)$;
fast总共走n步,每个节点至多访问常数次。- 空间复杂度:$O(1)$,只使用两个指针。
关键点总结
- 将“距末尾的位置”转化为两个指针的固定间距。
- 预走
k步、循环判断fast != null、最终返回slow必须配套。- 用空指针作为终点,可自然覆盖
k = 1和k = n。- 是否校验非法
k取决于题目契约,不要默默假设或随意返回魔法值。
易错点总结
fast只先走k - 1步:答案会向后偏一位。- 同步条件写成
fast.next != null:循环少走一步,答案向前偏一位。- 预走后只移动
fast:slow会一直停在头节点。- 在没有“
k合法”保证时直接访问fast.next:k大于链表长度会触发空指针异常。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 19. 删除链表的倒数第 N 个结点 | 中等 | 要删除而非读取,slow 必须停在前驱,需配合哑节点 |
| 876. 链表的中间结点 | 简单 | 间距不固定,改用一步与两步的速度差来定位中点 |
| 141. 环形链表 | 简单 | 快慢指针用于判断相遇而非定位,终止条件从判空变成判相等 |
| 61. 旋转链表 | 中等 | 同样按倒数位置切断,但要先对 k 取模再重新接环 |
| LCR 021. 删除链表的倒数第 N 个结点 | 中等 | 与 19 同源,适合对照一趟双指针与两趟计数两种写法 |
| 剑指 Offer 22. 链表中倒数第k个节点 | 简单 | 返回的是节点而非节点值,注意函数签名差异 |