10. One-Pass Removal of k-th Node from End
Origin: One-Pass Removal of k-th Node from End
Given the head of a singly linked list and an integer k, remove the k-th node from the end in one traversal and return the new head. If k is invalid, return the original list.
Example
Input
head = [5, 6, 7, 8]
k = 3Output
[6, 7, 8]Explanation
The list has 4 nodes.
The k-th node from the end with k=3 is the 4th node from the end (value 5), which is the head. Removing it yields [6,7,8].Input Format
- The first line contains an integer n denoting the length of linked list.
- The next n lines contains elements of the linked list.
- The last line contains k.
Example
4
5
6
7
8
3here 4 is the length of the linked list, followed by the elements of the list and value of k.
Constraints
- 0 <= number of nodes in head <= 1000
- -10^9 <= value of each node <= 10
- 0 <= k <= 10
Output Format
- Return the head of the modified linked list after removal.
Sample Input 0
1
5
1Sample Output 0
5Sample Input 1
2
1
2
0Sample Output 1
1Resolution
我的解法(有 Bug)
function removeKthNodeFromEnd(head: SinglyLinkedListNode, k: number): SinglyLinkedListNode | {} {
// Write your code here
let cur: SinglyLinkedListNode | undefined = head
const store: SinglyLinkedListNode[] = []
while (cur) {
store.push(cur)
cur = cur.next
}
if (k <= store.length - 1) {
if (k <= store.length - 2) {
const preNode = store[store.length - 2 - k]
const nextNode = store[store.length - k]
preNode.next = nextNode
console.log('k <= store.length - 2', k)
return preNode
} else {
let toRemove = store[store.length - 1 - k]
console.log('k > store.length - 2', k)
return toRemove.next || {}
}
} else {
console.log('k > store.length - 1', k)
return head
}
}错误分析
错误一:返回值错了
删除中间节点时,代码 return preNode。但 preNode 是被删节点的前一个节点,不是链表头。应该返回 head(除非删的是头节点)。
举例:[5, 6, 7, 8],k=1(删倒数第 2 个,即 7)。
- 代码返回 preNode(值为 6 的节点),但正确返回应该是 head(值为 5 的节点)。
- 虽然 5→6→8 链表结构是对的,但返回了 6 而不是 5,输出变成 [6, 8] 而不是 [5, 6, 8]。
注意:k 的语义没有错。 这道题的 k 是从 0 开始的偏移量:k=0 删最后一个,k=1 删倒数第 2 个,k=n-1 删 head。验证:head=[5,6,7,8],k=3 删 5(倒数第 4 个),返回 [6,7,8],和题目示例一致。
错误二:不是 one-pass
题目要求 one traversal(一次遍历),代码把所有节点存进数组 store,虽然只遍历了一次链表,但用了 O(n) 额外空间。题目标题叫 “One-Pass”,面试官会追问”能不能不用数组”。
正确解法:快慢指针
两个指针,fast 先走 k+1 步,然后 fast 和 slow 一起走,fast 到 null 时 slow 恰好停在被删节点的前一个。删头节点时没有前驱,用 dummy 节点解决。
function removeKthNodeFromEnd(head: SinglyLinkedListNode, k: number): SinglyLinkedListNode | null {
if (!head || k <= 0) return head
const dummy: SinglyLinkedListNode = { data: 0, next: head }
let fast: SinglyLinkedListNode = dummy
let slow: SinglyLinkedListNode = dummy
// fast 先走 k+1 步,拉开间距
for (let i = 0; i <= k; i++) {
if (!fast) return head // k 超出链表长度,无效
fast = fast.next
}
// fast 和 slow 一起走,fast.next 到 null 时 slow 在被删节点前一个
while (fast.next) {
fast = fast.next
slow = slow.next
}
// 删除 slow.next
slow.next = slow.next.next
return dummy.next
}为什么是 k+1 步不是 k 步?
fast 先走 k+1 步,拉开 k+1 的间距。然后 fast 和 slow 一起走,while (fast.next) 保证 fast 停在最后一个节点(不是 null),此时 slow 恰好停在被删节点的前一个。
为什么用 while (fast.next) 而不是 while (fast)?
如果用 while (fast),fast 会走到 null 才停,slow 会多走一步,停在被删节点本身而不是前一个。用 while (fast.next) 让 fast 停在最后一个节点,slow 就正好停在被删节点的前一个。
验证(head=[5,6,7,8],k=3 删 head):
- fast 走 4 步到 8,
fast.next是 null,循环不执行 - slow 停在 dummy,
slow.next = head.next跳过 head ✓
为什么用 dummy?
删头节点时,head 没有前驱节点。如果 slow 直接从 head 开始,无法指向 head 的”前一个”。加一个 dummy 节点指向 head,slow 从 dummy 开始,删头节点时 slow 就是 dummy,dummy.next = head.next 直接跳过头节点。
复杂度:
- 时间:O(n),一次遍历。
- 空间:O(1),只用两个指针。
同类题:LeetCode 19. 删除链表的倒数第 N 个结点。经典面试题,高频考点。