19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例

示例 1:

  [plaintext]
1
2
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]

示例 2:

  [plaintext]
1
2
输入:head = [1], n = 1 输出:[]

示例 3:

  [plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
输入:head = [1,2], n = 1 输出:[1] ```  ## 提示: 链表中结点的数目为 sz 1 <= sz <= 30 0 <= Node.val <= 100 1 <= n <= sz   进阶:你能尝试使用一趟扫描实现吗? # V1-借助数组 ## 思路 链表是单向的,但是需要从倒数第 N 个删除,那么如何知道整体的长度呢? 所以至少需要一次遍历。 我想到的最直接的思路,就是首先遍历一遍,把 ListNode 的所有节点存放在数组中。 这样可以获取倒对应的整体长度; 想删除的时候,直接 O(1) 获取对应的下标节点。 ## java 实现 ```java /** * 移除倒数的元素 * * 个人认为这种接法是由于题目中的给出的2种解法的。 * * leetcode 的2个解法实际上都是:length + (length-n) 次移动,这是不合理的。 * 如果 length=100,想移除倒数第一个元素,那么是遍历2次。 * 个人借助数组,则只需要 length+O(1) 的查找。 * 思路2: * 构建双向链表,添加一个尾巴节点。 * * @param head 头结点 * @param n 位数 * @return 结果 * @since v1 */ public ListNode removeNthFromEnd(ListNode head, int n) { List<ListNode> list = buildNodeList(head); if(list.size() <= 1) { return null; } // 考虑头尾的问题 // 如果是移除头结点 if(n == list.size()) { // 第二个元素当做头结点 return list.get(1); } // 删除固定的节点 ListNode pre = list.get(list.size()-n-1); pre.next = n >= list.size() ? null : pre.next.next; return list.get(0); } /** * 构建一个完整的数组 * @param head 头结点 * @return 结果 * @since v1 */ private List<ListNode> buildNodeList(ListNode head) { List<ListNode> list = new ArrayList<>(); list.add(head); while (head.next != null) { ListNode next = head.next; list.add(next); head = head.next; } return list; }

效果

  [plaintext]
1
2
Runtime: 0 ms, faster than 100.00% of Java online submissions for Remove Nth Node From End of List. Memory Usage: 37.9 MB, less than 30.88% of Java online submissions for Remove Nth Node From End of List.

当时因为效果太好,所以直接跳过了。

本文主要查缺补漏,把官方的几个解法都记录一下。

V2-两次遍历

官方解法

第一次,遍历整个链表,计算出长度 len。

第二次遍历,找到删除节点,执行删除。返回对应的头结点。

当然,为了避免处理麻烦,我们可以加一个 dummy 节点。

为了方便删除操作,我们可以从哑节点开始遍历 len - N + 1 个节点。

当遍历到第 len - N + 1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。

java 实现

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/** * 移除倒数的元素 * * @param head 头结点 * @param n 位数 * @return 结果 */ public ListNode removeNthFromEnd(ListNode head, int n) { int len = getLen(head); // 创建 dummy 节点 ListNode dummy = new ListNode(0, head); ListNode cur = dummy; for (int i = 1; i < len - n + 1; ++i) { cur = cur.next; } // 删除 cur 节点 cur.next = cur.next.next; return dummy.next; } /** * 获取长度 * @param head 头结点 * @return 结果 */ private int getLen(ListNode head) { int i = 0; while (head != null) { head = head.next; i++; } return i; }

效果

  [plaintext]
1
2
Runtime: 0 ms, faster than 100.00% of Java online submissions for Remove Nth Node From End of List. Memory Usage: 40.3 MB, less than 74.3% of Java online submissions for Remove Nth Node From End of List.

当然,严格来说,这个方法最差的复杂度实际上是遍历两遍。

比如 1W 个元素,删除最后一个,这样性能其实一般,只是测试用例区分性不强而已。

V3-双指针

思考

假设链表的长度为 L,我们希望倒数第 n 个元素,有没有办法避免 V2 的两次遍历呢?

我们可以定义两个指针 i, j。

i 先走 n 步,然后 i j 同时往后走。当 i 走到结尾的时候,此时 j 刚好就是在倒数第 n 个位置。

当然,为了处理方便。我们可以引入 dummy 节点,让 j 从 dummy 开始,i 从 head 开始。

java 实现

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ListNode removeNthFromEnd(ListNode head, int n) { // 创建 dummy 节点 ListNode dummy = new ListNode(0, head); // i 先走 n 步 ListNode i = head; ListNode j = dummy; for(int k = 0; k < n; k++) { i = i.next; } // 二者同时开始走 while (i != null) { j = j.next; i = i.next; } // 删除当前节点 j.next = j.next.next; // 返回 return dummy.next; }

效果

  [plaintext]
1
2
Runtime: 0 ms, faster than 100.00% of Java online submissions for Remove Nth Node From End of List. Memory Usage: 40.4 MB, less than 64.16% of Java online submissions for Remove Nth Node From End of List.

小结

官方的解法 1 实际上比较容易想到,不过需要两次遍历。

想避免这种两次遍历,有 2 种方式:

1)通过数组存储 listNode,避免再次遍历

2)通过双指针,达到一次满足的效果。

开源地址

为了便于大家学习,所有实现均已开源。欢迎 fork + star~

https://github.com/houbb/leetcode

参考资料

https://leetcode.cn/problems/remove-nth-node-from-end-of-list/solution/shan-chu-lian-biao-de-dao-shu-di-nge-jie-dian-b-61/