七道链表题 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
1 2 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
1 2 输入:l1 = [], l2 = [] 输出:[]
示例 3:
1 2 输入:l1 = [], l2 = [0] 输出:[0]
提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1
和 l2
均按 非递减顺序 排列
代码中用到一个链表的算法题中是很常见的「虚拟头结点」技巧
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 class Solution {public : ListNode* mergeTwoLists (ListNode* list1, ListNode* list2) { ListNode* dummy = new ListNode (-1 ); ListNode* p = dummy; ListNode* p1 = list1, *p2 = list2; while (p1 != NULL && p2 != NULL ) { if (p1 -> val < p2 -> val) { p -> next = p1; p1 = p1 -> next; } else { p -> next = p2; p2 = p2 -> next; } p = p -> next; } if (p1 != NULL ) { p -> next = p1; } else { p -> next = p2; } return dummy -> next; } };
单链表的分解
给你一个链表的头节点 head
和一个特定值 x
,请你对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
1 2 输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5]
示例 2:
1 2 输入:head = [2,1], x = 2 输出:[1,2]
提示:
链表中节点的数目在范围 [0, 200]
内
-100 <= Node.val <= 100
-200 <= x <= 200
整体逻辑和合并有序链表非常相似,细节直接看代码吧,注意虚拟头结点的运用:
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 class Solution {public : ListNode* partition (ListNode* head, int x) { ListNode* dummy1 = new ListNode (-1 ); ListNode* dummy2 = new ListNode (-1 ); ListNode* p = head; ListNode* l1 = dummy1, *l2 = dummy2; while (p != NULL ) { if (p->val < x) { l1->next = p; l1 = l1->next; } else { l2->next = p; l2 = l2->next; } ListNode* temp = p->next; p->next = NULL ; p = temp; } l1->next = dummy2->next; return dummy1->next; } };
细节: 如果你不断开原链表中的每个节点的 next
指针,那么就会出错,因为结果链表中会包含一个环
总的来说,如果我们需要把原链表的节点接到新链表上,而不是 new 新节点来组成新链表的话,那么断开节点和原链表之间的链接可能是必要的。那其实我们可以养成一个好习惯,但凡遇到这种情况,就把原链表的节点断开,这样就不会出错了。
合并 k 个有序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
1 2 3 4 5 6 7 8 9 10 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6
示例 2:
示例 3:
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i]
按 升序 排列
lists[i].length
的总和不超过 10^4
难点: 如何快速得到 k
个节点中的最小节点,接到结果链表上?
用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k
个节点中的最小节点:
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 class Solution {public : ListNode* mergeKLists (vector<ListNode*>& lists) { ListNode* dummy = new ListNode (-1 ); ListNode* p = dummy; priority_queue<ListNode*,vector<ListNode*>,function<bool (ListNode*, ListNode*)>> pq ([] (ListNode* l1, ListNode* l2) {return l1 -> val > l2 -> val; }); for (auto head : lists) { if (head != NULL ) { pq.push (head); } } while (!pq.empty ()) { ListNode* node = pq.top (); pq.pop (); p -> next = node; if (node -> next != NULL ) { pq.push (node -> next); } p = p -> next; } return dummy -> next; } };
单链表的倒数第 k 个节点 (深圳软牛笔试编程题)滑动窗口思想:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ListNode* findFromEnd (ListNode* head, int k) { ListNode* p1 = head; for (int i = 0 ; i < k; i++) { p1 = p1 -> next; } ListNode* p2 = head; while (p1 != nullptr ) { p2 = p2 -> next; p1 = p1 -> next; } return p2; }
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
1 2 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
示例 2:
1 2 输入:head = [1], n = 1 输出:[]
示例 3:
1 2 输入:head = [1,2], n = 1 输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶: 你能尝试使用一趟扫描实现吗?
原理同上,找倒数第 k+1
个节点
单链表的中点 依然快慢指针,一个走两步,一个走一步
1 2 3 4 5 6 7 8 9 10 11 12 13 ListNode* middleNode (ListNode* head) { ListNode* slow = head; ListNode* fast = head; while (fast != nullptr && fast->next != nullptr ) { slow = slow->next; fast = fast->next->next; } return slow; }
判断链表是否包含环 快慢指针,如果相遇则有环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool hasCycle (ListNode* head) { ListNode* slow = head; ListNode* fast = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; if (slow == fast) { return true ; } } return false ; }
当然,这个问题还有进阶版,也是力扣第 142 题「环形链表 IIopen in new window 」:如果链表中含有环,如何计算这个环的起点?
证明来自labuladong:
我们假设快慢指针相遇时,慢指针 slow
走了 k
步,那么快指针 fast
一定走了 2k
步:
fast
一定比 slow
多走了 k
步,这多走的 k
步其实就是 fast
指针在环里转圈圈,所以 k
的值就是环长度的「整数倍」。
假设相遇点距环的起点的距离为 m
,那么结合上图的 slow
指针,环的起点距头结点 head
的距离为 k - m
,也就是说如果从 head
前进 k - m
步就能到达环起点。
巧的是,如果从相遇点继续前进 k - m
步,也恰好到达环起点。因为结合上图的 fast
指针,从相遇点开始走k步可以转回到相遇点,那走 k - m
步肯定就走到环起点了:
所以,只要我们把快慢指针中的任一个重新指向 head
,然后两个指针同速前进,k - m
步后一定会相遇,相遇之处就是环的起点了。
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 class Solution {public : ListNode *detectCycle (ListNode *head) { ListNode* fast = head; ListNode* slow = head; while (fast && fast -> next) { slow = slow -> next; fast = fast -> next -> next; if (slow == fast) { break ; } } if (fast == NULL || fast -> next == NULL ) { return NULL ; } slow = head; while (slow != fast) { slow = slow -> next; fast = fast -> next; } return slow; } };
两个链表是否相交 **解决这个问题的关键是,通过某些方式,让 p1
和 p2
能够同时到达相交节点 c1
**。
所以,我们可以让 p1
遍历完链表 A
之后开始遍历链表 B
,让 p2
遍历完链表 B
之后开始遍历链表 A
,这样相当于「逻辑上」两条链表接在了一起。
如果这样进行拼接,就可以让 p1
和 p2
同时进入公共部分,也就是同时到达相交节点 c1
:
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 class Solution {public : ListNode *getIntersectionNode (ListNode *headA, ListNode *headB) { ListNode* p1 = headA, *p2 = headB; while (p1 != p2) { if (p1 == NULL ) { p1 = headB; } else { p1 = p1 -> next; } if (p2 == NULL ) { p2 = headA; } else { p2 = p2 -> next; } } return p1; } };
递归反转单链表 递归反转整个单链表 Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : ListNode* reverseList (ListNode* head) { if (head == NULL || head -> next == NULL ) { return head; } ListNode* last = reverseList (head -> next); head -> next -> next = head; head -> next = NULL ; return last; } };
反转单链表前N个节点 具体思路差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : ListNode* successor = nullptr ; ListNode* reverseN (ListNode* head, int n) { if (n == 1 ) { successor = head -> next; return head; } ListNode* last = reverseList (head -> next, n - 1 ); head -> next -> next = head; head -> next = successor; return last; } };
具体的区别:
1、base case 变为 n == 1
,反转一个元素,就是它本身,同时要记录后驱节点 。
2、刚才我们直接把 head.next
设置为 null,因为整个链表反转后原来的 head
变成了整个链表的最后一个节点。但现在 head
节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor
(第 n + 1
个节点),反转之后将 head
连接上。
反转链表的一部分 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 class Solution {public : ListNode* successor = NULL ; ListNode* reverseN (ListNode* head, int n) { if (n == 1 ) { successor = head -> next; return head; } ListNode* last = reverseN (head -> next, n - 1 ); head -> next -> next = head; head -> next = successor; return last; } ListNode* reverseBetween (ListNode* head, int left, int right) { if (left == 1 ) { return reverseN (head, right); } head -> next = reverseBetween (head -> next, left - 1 , right - 1 ); return head; } };
如何 K 个一组反转链表
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
1 2 输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5]
示例 2:
1 2 输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
提示:
链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
进阶: 你可以设计一个只用 O(1)
额外内存空间的算法解决此问题吗?
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 class Solution {public : ListNode* reverse (ListNode* a, ListNode* b) { ListNode* pre = NULL , *curr = a, *nxt = a; while (curr != b) { nxt = curr -> next; curr -> next = pre; pre = curr; curr = nxt; } return pre; } ListNode* reverseKGroup (ListNode* head, int k) { if (head == NULL ) { return NULL ; } ListNode *a, *b; a = b = head; for (int i = 0 ; i < k; i++) { if (b == NULL ) { return head; } b = b -> next; } ListNode* newHead = reverse (a, b); a -> next = reverseKGroup (b, k); return newHead; } };
如何判断回文链表 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 class Solution {public : bool isPalindrome (ListNode* head) { if (head == nullptr ) { return true ; } ListNode *firstHalfEnd = endOfFirstHalf (head); ListNode *secondHalfStart = reverseList (firstHalfEnd -> next); ListNode *p1 = head; ListNode *p2 = secondHalfStart; bool result = true ; while (result && p2 != nullptr ) { if (p1 -> val != p2 -> val) { result = false ; } p1 = p1 -> next; p2 = p2 -> next; } firstHalfEnd -> next = reverseList (secondHalfStart); return result; } ListNode *reverseList (ListNode *head) { ListNode *prev = nullptr ; ListNode *curr = head; while (curr != nullptr ) { ListNode *next = curr -> next; curr -> next = prev; prev = curr; curr = next; } return prev; } ListNode *endOfFirstHalf (ListNode *head) { ListNode *fast = head; ListNode *slow = head; while (fast -> next != nullptr && fast -> next -> next != nullptr ) { fast = fast -> next -> next; slow = slow -> next; } return slow; } };