题目描述
实现一种算法,找出单向链表中倒数第 k 个节点,返回该节点的值。
注意: 本题相对原题稍作改动
示例
输入:1->2->3->4->5 和 k = 2
输出:4
说明:
- 给定的 k 保证是有效的
解题思路
这道题要求找到链表中倒数第 k 个节点。最直观的解法是使用双指针技巧(快慢指针),这种方法只需要一次遍历即可解决问题。
双指针法原理
-
快指针先移动 k 步
-
慢指针从链表头开始
-
然后两个指针同时移动,当快指针到达链表末尾时,慢指针正好指向倒数第 k 个节点
为什么这样有效?
-
快指针领先慢指针 k 步
-
当快指针到达末尾(NULL)时,慢指针距离末尾正好 k 步
-
因此慢指针指向的就是倒数第 k 个节点
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int kthToLast(struct ListNode* head, int k) {
struct ListNode* fast = head; // 快指针
struct ListNode* cur = head; // 慢指针(当前指针)
// 快指针先移动 k 步
while(k--)
{
fast = fast->next;
}
// 两个指针同时移动,直到快指针到达末尾
while(fast)
{
cur = cur->next;
fast = fast->next;
}
return cur->val; // 返回倒数第 k 个节点的值
}
代码详解
核心逻辑
// 快指针先移动 k 步
while(k--)
{
fast = fast->next;
}
// 两个指针同时移动
while(fast)
{
cur = cur->next;
fast = fast->next;
}
执行过程可视化
以链表 1->2->3->4->5 和 k = 2 为例:
初始状态:
fast -> 1, cur -> 1
快指针先移动 2 步:
第1步:fast -> 2
第2步:fast -> 3
现在:
fast -> 3, cur -> 1
两个指针同时移动:
第1次:fast -> 4, cur -> 2
第2次:fast -> 5, cur -> 3
第3次:fast -> NULL, cur -> 4
返回 cur->val = 4
边界情况分析
k = 1(倒数第一个节点)
链表:1->2->3->4->5, k = 1
快指针先移动 1 步:fast -> 2
然后同时移动:
fast -> 3, cur -> 2
fast -> 4, cur -> 3
fast -> 5, cur -> 4
fast -> NULL, cur -> 5
返回 5(最后一个节点)
k = 链表长度(倒数第 n 个节点,即第一个节点)
链表:1->2->3->4->5, k = 5
快指针先移动 5 步:fast -> NULL
然后同时移动:不进入循环(因为 fast 已经是 NULL)
返回 cur->val = 1(第一个节点)
其他解法
方法二:两次遍历法
int kthToLast(struct ListNode* head, int k) {
// 第一次遍历:计算链表长度
int length = 0;
struct ListNode* current = head;
while (current != NULL) {
length++;
current = current->next;
}
// 第二次遍历:找到第 (length - k) 个节点
current = head;
for (int i = 0; i < length - k; i++) {
current = current->next;
}
return current->val;
}
优缺点:
-
优点:思路简单直观
-
缺点:需要遍历链表两次
方法三:递归法
int count = 0;
int result = 0;
int kthToLast(struct ListNode* head, int k) {
if (head == NULL) {
return 0;
}
kthToLast(head->next, k);
count++;
if (count == k) {
result = head->val;
}
return result;
}
优缺点:
-
优点:代码简洁
-
缺点:使用递归栈,空间复杂度 O(n)
复杂度分析
双指针法
-
时间复杂度:O(n),只需遍历链表一次
-
空间复杂度:O(1),只使用两个指针变量
比较其他方法
-
两次遍历法:时间复杂度 O(n),空间复杂度 O(1)
-
递归法:时间复杂度 O(n),空间复杂度 O(n)(递归栈)
扩展思考
如果要求返回节点而不是节点的值
struct ListNode* getKthFromEnd(struct ListNode* head, int k) {
struct ListNode* fast = head;
struct ListNode* slow = head;
// 快指针先走 k 步
while (k-- > 0) {
fast = fast->next;
}
// 同时移动直到快指针到达末尾
while (fast != NULL) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
如果 k 可能无效
int kthToLast(struct ListNode* head, int k) {
// 检查 k 的有效性
if (k <= 0) return -1; // 或者适当的错误处理
struct ListNode* fast = head;
struct ListNode* cur = head;
// 快指针先移动 k 步,同时检查是否超出链表长度
while (k-- > 0) {
if (fast == NULL) {
return -1; // k 大于链表长度
}
fast = fast->next;
}
// 两个指针同时移动
while (fast != NULL) {
cur = cur->next;
fast = fast->next;
}
return cur->val;
}
应用场景
这种快慢指针技巧在很多场景中都有应用:
-
找到链表的中间节点(快指针每次两步,慢指针每次一步)
-
判断链表是否有环
-
找到环的入口点
-
删除链表的倒数第 N 个节点
总结
这道题展示了双指针技巧的又一个经典应用:
-
核心思想:快指针领先慢指针 k 步,当快指针到达末尾时,慢指针正好在倒数第 k 个位置
-
效率优势:只需一次遍历,时间复杂度 O(n),空间复杂度 O(1)
-
边界处理:题目保证 k 有效,所以不需要额外检查
-
通用性:这种技巧可以推广到许多类似问题
掌握这种双指针技巧对于解决链表相关问题非常有帮助,是面试中常见的考点之一。