diff --git a/notes/Leetcode 题解 - 链表.md b/notes/Leetcode 题解 - 链表.md index 66eebffb..d8b44edf 100644 --- a/notes/Leetcode 题解 - 链表.md +++ b/notes/Leetcode 题解 - 链表.md @@ -15,6 +15,7 @@ * [12. 重排链表](#12-重排链表) * [13. 合并 K 个有序链表](#13-合并-k-个有序链表) * [14. 链表排序](#14-链表排序) + * [参考资料](#参考资料) @@ -50,11 +51,13 @@ B: b1 → b2 → b3 e1 → e2 要求时间复杂度为 O(N),空间复杂度为 O(1)。如果不存在交点则返回 null。 -设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 +两个指针同时出发,难点在于 A、B 交点前的长度不同,指针不会同时到达交点。解决方法是让每个指针走完自己的链表后,接着从另一个链表的头部走: -当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 +设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度。指针 l1 走完 A 再走 B 的前 b 个节点,指针 l2 走完 B 再走 A 的前 a 个节点,两者走的步数都是 a + c + b,因此会同时到达交点。以上面的例子 a = 2、b = 3、c = 3 为例,两个指针都在第 9 步到达 c1(null 也算一步): -如果不存在交点,那么 a + b = b + a,以下实现代码中 l1 和 l2 会同时为 null,从而退出循环。 +

+ +如果不存在交点,两个指针都走完 A、B 两个链表后同时为 null,从而退出循环。 ```java public ListNode getIntersectionNode(ListNode headA, ListNode headB) { @@ -67,7 +70,7 @@ public ListNode getIntersectionNode(ListNode headA, ListNode headB) { } ``` -如果只是判断是否存在交点,那么就是另一个问题,即 编程之美 3.6 的问题。有两种解法: +如果只是判断是否存在交点,那么就是另一个问题,即《编程之美》3.6 的问题。有两种解法: - 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; - 或者直接比较两个链表的最后一个节点是否相同。 @@ -78,7 +81,7 @@ public ListNode getIntersectionNode(ListNode headA, ListNode headB) { [Leetcode](https://leetcode.com/problems/reverse-linked-list/description/) / [力扣](https://leetcode-cn.com/problems/reverse-linked-list/description/) -递归 +递归:先把 head.next 之后的部分反转,返回新的头节点;此时 next 是反转后那一段的尾节点,把 head 接在 next 后面,再断开 head 原来的 next 指针。例如 1→2→3,反转 2→3 得到 3→2 后,令 2.next = 1、1.next = null,得到 3→2→1。 ```java public ListNode reverseList(ListNode head) { @@ -93,7 +96,7 @@ public ListNode reverseList(ListNode head) { } ``` -头插法 +头插法(迭代):从头到尾取下每个节点,插到虚拟头节点 newHead 的后面,先取下的节点会被后取下的节点挤到后面,因此顺序反转。newHead.next 相当于常见写法中指向已反转部分的 pre 指针。每一步要先用 next 记住 head.next 再修改 head.next,否则后面的链表就丢了。迭代只需要 O(1) 的额外空间,递归需要 O(N) 的栈空间,以 1 -\> 2 -\> 3 为例的逐步过程见[剑指 Offer 24](24.%20反转链表.md#迭代)。 ```java public ListNode reverseList(ListNode head) { @@ -114,6 +117,8 @@ public ListNode reverseList(ListNode head) { [Leetcode](https://leetcode.com/problems/merge-two-sorted-lists/description/) / [力扣](https://leetcode-cn.com/problems/merge-two-sorted-lists/description/) +比较两个链表的头节点,较小的那个作为结果的头节点,它的 next 指向剩余部分归并的结果。递归最坏需要 O(m + n) 的栈空间(m、n 为两个链表的长度),链表很长时可以改用迭代:用虚拟头节点 dummy 和尾指针 tail,每次把较小的节点接到 tail 后面,代码见[链表排序](#14-链表排序)中的 merge()。 + ```java public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) return l2; @@ -139,6 +144,8 @@ Given 1->1->2, return 1->2. Given 1->1->2->3->3, return 1->2->3. ``` +先递归处理 head.next 之后的部分,返回时如果 head 与后继节点的值相同,就丢掉 head、返回后继节点。 + ```java public ListNode deleteDuplicates(ListNode head) { if (head == null || head.next == null) return head; @@ -158,6 +165,8 @@ Given linked list: 1->2->3->4->5, and n = 2. After removing the second node from the end, the linked list becomes 1->2->3->5. ``` +fast 先走 n 步,然后 fast 和 slow 一起走,fast 到达最后一个节点时,slow 正好停在倒数第 n 个节点的前一个节点上。例如 n = 2 时 fast 停在 5,slow 停在 3,令 3.next = 5 即可。如果 fast 先走 n 步后就为 null,说明要删除的是头节点。 + ```java public ListNode removeNthFromEnd(ListNode head, int n) { ListNode fast = head; @@ -185,7 +194,9 @@ public ListNode removeNthFromEnd(ListNode head, int n) { Given 1->2->3->4, you should return the list as 2->1->4->3. ``` -题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 +题目要求:不能修改节点的 val 值,只能改变节点之间的指针。 + +pre 指向待交换的两个节点 l1、l2 的前一个节点,依次令 l1 指向 l2 的后继、l2 指向 l1、pre 指向 l2,交换后 l1 在后面,作为下一对的 pre。 ```java public ListNode swapPairs(ListNode head) { @@ -216,7 +227,9 @@ Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) Output: 7 -> 8 -> 0 -> 7 ``` -题目要求:不能修改原始链表。 +进阶要求:不能反转输入的链表。 + +链表的高位在前,而加法要从低位开始算。把两个链表的值分别压入栈中,出栈的顺序就是从低位到高位;每一位的结果用头插法插到结果链表的前面。 ```java public ListNode addTwoNumbers(ListNode l1, ListNode l2) { @@ -252,9 +265,9 @@ private Stack buildStack(ListNode l) { [Leetcode](https://leetcode.com/problems/palindrome-linked-list/description/) / [力扣](https://leetcode-cn.com/problems/palindrome-linked-list/description/) -题目要求:以 O(1) 的空间复杂度来求解。 +进阶要求:以 O(1) 的空间复杂度来求解。 -切成两半,把后半段反转,然后比较两半是否相等。 +用快慢指针找到中点后切成两半,把后半段反转,然后比较两半是否相等。节点数为奇数时,中间的节点分到后半段,比较到前半段结束为止。这种做法会断开并改动输入的链表,需要保持原状时,比较后把后半段再反转一次,接回前半段的末尾。 ```java public boolean isPalindrome(ListNode head) { @@ -299,13 +312,13 @@ private boolean isEqual(ListNode l1, ListNode l2) { ## 9. 分隔链表 -725\. Split Linked List in Parts(Medium) +725\. Split Linked List in Parts (Medium) [Leetcode](https://leetcode.com/problems/split-linked-list-in-parts/description/) / [力扣](https://leetcode-cn.com/problems/split-linked-list-in-parts/description/) ```text Input: -root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 +head = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] Explanation: The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. @@ -313,10 +326,12 @@ The input has been split into consecutive parts with size difference at most 1, 题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 +设链表长度为 N,每部分至少 N / k 个节点,前 N % k 部分各多 1 个。例如 N = 10、k = 3 时,三部分的长度为 4、3、3。 + ```java -public ListNode[] splitListToParts(ListNode root, int k) { +public ListNode[] splitListToParts(ListNode head, int k) { int N = 0; - ListNode cur = root; + ListNode cur = head; while (cur != null) { N++; cur = cur.next; @@ -324,7 +339,7 @@ public ListNode[] splitListToParts(ListNode root, int k) { int mod = N % k; int size = N / k; ListNode[] ret = new ListNode[k]; - cur = root; + cur = head; for (int i = 0; cur != null && i < k; i++) { ret[i] = cur; int curSize = size + (mod-- > 0 ? 1 : 0); @@ -351,6 +366,8 @@ Given 1->2->3->4->5->NULL, return 1->3->5->2->4->NULL. ``` +odd 和 even 分别串起奇数位置和偶数位置的节点,交替向后跳一个节点,最后把偶数链表接在奇数链表的后面。 + ```java public ListNode oddEvenList(ListNode head) { if (head == null) { @@ -379,9 +396,9 @@ Input: 1->2->3->4->5, k = 2 Output: 2->1->4->3->5 ``` -题目描述:每 k 个节点一组进行翻转,最后不足 k 个的节点保持原有顺序。 +题目描述:每 k 个节点一组进行翻转,最后不足 k 个的节点保持原有顺序。如果要求不足 k 个也翻转,走不到 k 步时把剩下的部分同样翻转即可。 -使用一个虚拟头节点 dummy,pre 指向下一组的前一个节点。先从 pre 向后走 k 步找到这一组的尾节点 tail,走不到说明剩余节点不足 k 个。然后把 pre.next 到 tail 这一段原地翻转,再把它接回原链表,最后 pre 移到这一组翻转后的末尾。 +使用一个虚拟头节点 dummy,pre 指向下一组的前一个节点。先从 pre 向后走 k 步找到这一组的尾节点 tail,走不到说明剩余节点不足 k 个。然后把 pre.next 到 tail 这一段原地翻转,再把它接回原链表,最后 pre 移到这一组翻转后的末尾。翻转时 prev 的初始值是下一组的头节点 next 而不是 null,这样翻转完之后这一组原来的头节点 first 已经接在下一组前面,只需要再令 pre.next = tail。

@@ -470,7 +487,7 @@ private ListNode reverse(ListNode head) { 用小顶堆保存每个链表当前的头节点,每次取出值最小的节点接到结果链表后面,再把它的下一个节点放入堆中。设一共有 N 个节点,每个节点进出堆一次,时间复杂度为 O(NlogK)。 -也可以像归并排序那样两两合并,时间复杂度同样是 O(NlogK)。 +也可以像归并排序那样两两合并:第一轮把 K 个链表合并成 K / 2 个,第二轮合并成 K / 4 个,一共 logK 轮,每轮处理 N 个节点,时间复杂度同样是 O(NlogK)。需要注意的是,如果依次把每个链表合并到同一个结果链表上,前面的节点会被反复扫描,时间复杂度为 O(NK)。 ```java public ListNode mergeKLists(ListNode[] lists) { @@ -501,7 +518,7 @@ public ListNode mergeKLists(ListNode[] lists) { 题目描述:在 O(NlogN) 时间复杂度内对链表进行排序。 -链表不能随机访问,适合用归并排序:用快慢指针找到中点断开,两半分别排序后,用第 3 题的方法归并。递归需要 O(logN) 的栈空间。 +链表不能随机访问,适合用归并排序:用快慢指针找到中点断开,两半分别排序后,用[归并两个有序的链表](#3-归并两个有序的链表)的方法归并。递归需要 O(logN) 的栈空间。 ```java public ListNode sortList(ListNode head) { @@ -534,3 +551,8 @@ private ListNode merge(ListNode l1, ListNode l2) { return dummy.next; } ``` + +## 参考资料 + +- [LeetCode 题库](https://leetcode.com/problemset/) +- 《编程之美》小组. 编程之美 [M]. 电子工业出版社, 2008. diff --git a/notes/pics/reverse-k-group.png b/notes/pics/reverse-k-group.png index 7041a694..4e8a8aa9 100644 Binary files a/notes/pics/reverse-k-group.png and b/notes/pics/reverse-k-group.png differ