19. 删除链表的倒数第 N 个结点
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
题目描述见链接内容。
解法1:两次遍历计算链表长度
可以通过两次遍历来实现,第一次遍历计算出链表长度,这样就知道倒数第n
个对应的正数是第几个了,然后在第二次遍历时,将对应的节点的『前驱节点』的next
指向next.next
问题是,当删除的是正数第一个节点时,由于第一个节点不存在『前驱节点』所以需要特殊处理
var removeNthFromEnd = function (head, n) {
let cur1 = head,
len = 0;
while (cur1) {
len += 1;
cur1 = cur1.next;
}
const targetIndex = len - n;
// 处理去除首节点的情况
if (targetIndex === 0) {
head = head.next;
}
let cur2 = head,
index = 0;
while (cur2) {
if (index + 1 === targetIndex) {
cur2.next = cur2.next ? cur2.next.next : null;
}
cur2 = cur2.next;
index += 1;
}
return head;
};
- 时间复杂度:
${O(N)}$
- 空间复杂度:
${O(1)}$
- 执行用时:64ms, 在所有JavaScript提交中击败了100%的用户,内存消耗:39.5MB,在所有JavaScript提交中击败了9%的用户
解法2:两次遍历+哑结点
因为头结点没有『前驱节点』,所以需要特殊处理。对链表进行操作时,一种常用的技巧是添加一个哑结点(Dummy Node),它的next
指针指向链表的头节点,这样就不需要对头节点进行特殊处理了,只考虑通用情况既可以。
要注意,最后返回的应该是哑结点的next
节点。
利用哑结点对上面的解法进行改造:
var removeNthFromEnd = function (head, n) {
let cur1 = head,
len = 0;
while (cur1) {
len += 1;
cur1 = cur1.next;
}
const targetIndex = len - n;
// 添加哑结点
const dummyNode = new ListNode(0, head);
let cur2 = dummyNode,
index = 0;
while (cur2) {
if (index === targetIndex) {
cur2.next = cur2.next ? cur2.next.next : null;
}
cur2 = cur2.next;
index += 1;
}
return dummyNode.next;
};
- 时间复杂度:
${O(N)}$
- 空间复杂度:
${O(1)}$
- 执行用时:80ms, 在所有JavaScript提交中击败了86%的用户,内存消耗:39.1MB,在所有JavaScript提交中击败了71%的用户
解法3:栈
首先沿用上面的小技巧,仍然声明一个哑结点,然后将所有节点依次入栈,完成后,然后再依次出栈,当出栈到第n
个后,就是我们要找到的节点,当前栈顶的节点就是要删除节点的前置节点了
var removeNthFromEnd = function (head, n) {
const dummyNode = new ListNode(0, head),
stack = [];
let cur1 = dummyNode;
// 入栈
while (cur1) {
stack.push(cur1);
cur1 = cur1.next;
}
let count = 0;
while (count < n) {
stack.pop();
count += 1;
}
// 前置节点
const target = stack.pop();
target.next = target.next ? target.next.next : null;
return dummyNode.next;
};
- 时间复杂度:
${O(N)}$
- 空间复杂度:
${O(1)}$
- 执行用时:68ms, 在所有JavaScript提交中击败了98%的用户,内存消耗:39MB,在所有JavaScript提交中击败了86%的用户
解法4:快慢指针
上面的解法都使用了两次遍历,如果要缩减为一次遍历的话,无非是利用空间换时间,或者增加指针数量。可以把上面的单指针变为双指针,一个慢指针,一个快指针,快指针先走n
步,慢指针再走,一起走到最后,这时慢指针就指向了倒数第n
个节点
我们需要找到倒数第n
个节点的前置节点,也就是倒数第n + 1
个节点,所以可以让快指针先走一个节点,即快指针指向head
,慢指针指向哑结点
var removeNthFromEnd = function (head, n) {
const dummyNode = new ListNode(0, head);
// 快指针先走一个
let fast = head,
slow = dummyNode,
count = 0;
// 快指针先走 n - 1 步
while (count < n) {
fast = fast.next;
count += 1;
}
// 一起走到最后
while (fast) {
fast = fast.next;
slow = slow.next;
}
// slow 就是倒数第 n 个节点的前置节点
slow.next = slow.next ? slow.next.next : null;
return dummyNode.next;
};
- 时间复杂度:
${O(N)}$
- 空间复杂度:
${O(1)}$
- 执行用时:64ms, 在所有JavaScript提交中击败了100%的用户,内存消耗:39.2MB,在所有JavaScript提交中击败了60%的用户