最近看到了两个题目,有一定的相似性,就一起总结下。一道题目是合并n个有序的数组/链表,另一道题目是寻找两个有序数组中所有数字的中位数。
第一个题目,有多种思路,一种是不断从n个数组中取出一个数组,进行合并两个数组的操作,基础操作是合并两个数组。
第二个题目,最简单的思路也是合并两个数组之后取中位数,不过还可以通过二分法的方式优化,最后再说。
合并K个升序列表 题目给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
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
解题思路 方法一:顺序合并我们可以想到一种最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。
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 class Solution {public : ListNode* mergeTwoLists (ListNode *a, ListNode *b) { if ((!a) || (!b)) return a ? a : b; ListNode head, *tail = &head, *aPtr = a, *bPtr = b; while (aPtr && bPtr) { if (aPtr->val < bPtr->val) { tail->next = aPtr; aPtr = aPtr->next; } else { tail->next = bPtr; bPtr = bPtr->next; } tail = tail->next; } tail->next = (aPtr ? aPtr : bPtr); return head.next; } ListNode* mergeKLists (vector<ListNode*>& lists) { ListNode *ans = nullptr ; for (size_t i = 0 ; i < lists.size (); ++i) { ans = mergeTwoLists (ans, lists[i]); } return ans; } };
方法二:分治合并考虑优化方法一,用分治的方法进行合并。
将 k 个链表配对并将同一对中的链表合并; 第一轮合并以后, k 个链表被合并成了k 2 \frac{k}{2} 2 k 个链表,平均长度为2 n k \frac{2n}{k} k 2 n ,然后是k 4 \frac{k}{4} 4 k 个链表,k 8 \frac{k}{8} 8 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 class Solution {public : ListNode* mergeTwoLists (ListNode *a, ListNode *b) { if ((!a) || (!b)) return a ? a : b; ListNode head, *tail = &head, *aPtr = a, *bPtr = b; while (aPtr && bPtr) { if (aPtr->val < bPtr->val) { tail->next = aPtr; aPtr = aPtr->next; } else { tail->next = bPtr; bPtr = bPtr->next; } tail = tail->next; } tail->next = (aPtr ? aPtr : bPtr); return head.next; } ListNode* merge (vector <ListNode*> &lists, int l, int r) { if (l == r) return lists[l]; if (l > r) return nullptr ; int mid = (l + r) >> 1 ; return mergeTwoLists (merge (lists, l, mid), merge (lists, mid + 1 , r)); } ListNode* mergeKLists (vector<ListNode*>& lists) { return merge (lists, 0 , lists.size () - 1 ); } };
方法三:使用优先队列合并这个方法和前两种方法的思路有所不同,我们需要维护当前每个链表没有被合并的元素的最前面一个,k 个链表就最多有 k 个满足这样条件的元素,每次在这些元素里面选取 val 属性最小的元素合并到答案中。在选取最小元素的时候,我们可以用优先队列来优化这个过程。
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 class Solution {public : struct Status { int val; ListNode *ptr; bool operator < (const Status &rhs) const { return val > rhs.val; } }; priority_queue <Status> q; ListNode* mergeKLists (vector<ListNode*>& lists) { for (auto node: lists) { if (node) q.push ({node->val, node}); } ListNode head, *tail = &head; while (!q.empty ()) { auto f = q.top (); q.pop (); tail->next = f.ptr; tail = tail->next; if (f.ptr->next) q.push ({f.ptr->next->val, f.ptr->next}); } return head.next; } };
方法四:每次取出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 35 36 37 var mergeKLists = function (lists ) { const pres = new Array (lists.length ).fill (0 ).map ((_, i ) => new ListNode (undefined , lists[i])); const curs = new Array (lists.length ).fill (0 ).map ((_, i ) => lists[i]); const dummy = new ListNode (); let tail = dummy; while (curs.some (c => c !== null )) { let min = 0 ; for (let i = 1 ; i < lists.length ; i++) { if (curs[min] === null && curs[i] !== null ) { min = i; } else if (curs[min] !== null && curs[i] !== null ) { min = curs[min].val < curs[i].val ? min : i; } } tail.next = curs[min]; tail = tail.next ; pres[min] = curs[min]; curs[min] = curs[min].next ; } return dummy.next ; };
寻找两个有序数组中的中位数 题目给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
1 2 3 输入:nums1 = [1,3], nums2 = [2] 输出:2.00000 解释:合并数组 = [1,2,3] ,中位数 2
解题思路 方法一:合并数组后取中位数随便选择上一个题目的一种方法进行合并,然后取中位数就好
方法二:指针法其实我们不需要另外开辟空间去存储合并后的数组,我们只需要知道中位数在那里就可以了
用 len 表示合并后数组的长度,如果是奇数,我们需要知道第 (len+1)/2 个数就可以了,如果遍历的话需要遍历 int(len/2 ) + 1 次。如果是偶数,我们需要知道第 len/2和 len/2+1 个数,也是需要遍历 len/2+1 次。所以遍历的话,奇数和偶数都是 len/2+1 次。
返回中位数的话,奇数需要最后一次遍历的结果就可以了,偶数需要最后一次和上一次遍历的结果。所以我们用两个变量 left 和 right,right 保存当前循环的结果,在每次循环前将 right 的值赋给 left。这样在最后一次循环的时候,left 将得到 right 的值,也就是上一次循环的结果,接下来 right 更新为最后一次的结果。
循环中该怎么写,什么时候 A 数组后移,什么时候 B 数组后移。用 aStart 和 bStart 分别表示当前指向 A 数组和 B 数组的位置。如果 aStart 还没有到最后并且此时 A 位置的数字小于 B 位置的数组,那么就可以后移了。也就是aStart<m&&A[aStart]< B[bStart]。
但如果 B 数组此刻已经没有数字了,继续取数字 B[ bStart ],则会越界,所以判断下 bStart 是否大于数组长度了,这样 || 后边的就不会执行了,也就不会导致错误了,所以增加为 aStart<m&&(bStart) >= n||A[aStart]<B[bStart]) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public double findMedianSortedArrays (int [] A, int [] B) { int m = A.length; int n = B.length; int len = m + n; int left = -1 , right = -1 ; int aStart = 0 , bStart = 0 ; for (int i = 0 ; i <= len / 2 ; i++) { left = right; if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) { right = A[aStart++]; } else { right = B[bStart++]; } } if ((len & 1 ) == 0 ) return (left + right) / 2.0 ; else return right; }
方法三上边的两种思路,时间复杂度都达不到题目的要求 O(log(m+n)O(log(m+n)O(log(m+n)。看到 log,很明显,我们只有用到二分的方法才能达到。我们不妨用另一种思路,题目是求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法。
解法二中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个一个排除。由于数列是有序的,其实我们完全可以一半儿一半儿的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数。看下边一个例子。
假设我们的两个数组分别是:
[1,3,4,9] [1,2,3,4,5,6,7,8,9,10]
我们比较两个数组的第 k/2 个数字,如果 k 是奇数,向下取整。也就是比较第 3 个数字,上边数组中的 4 和下边数组中的 3,如果哪个小,就表明该数组的前 k/2 个数字都不是第 k 小数字,所以可以排除。也就是 1,2,3 这三个数字不可能是第 7 小的数字,我们可以把它排除掉。将 1349 和 45678910 两个数组作为新的数组进行比较。
A[1] ,A[2] ,A[3],A[k/2] … ,B[1],B[2],B[3],B[k/2] … ,如果 A[k/2]<B[k/2] ,那么A[1],A[2],A[3],A[k/2] 都不可能是第 k 小的数字。
A 数组中比 A[k/2] 小的数有 k/2-1 个,B 数组中,B[k/2] 比 A[k/2] 小,假设 B[k/2] 前边的数字都比 A[k/2] 小,也只有 k/2-1 个,所以比 A[k/2] 小的数字最多有 k/1-1+k/2-1=k-2个,所以 A[k/2] 最多是第 k-1 小的数。而比 A[k/2] 小的数更不可能是第 k 小的数了,所以可以把它们排除。
由于我们已经排除掉了 3 个数字,就是这 3 个数字一定在最前边,所以在两个新数组中,我们只需要找第 7 - 3 = 4 小的数字就可以了,也就是 k = 4。此时两个数组,比较第 2 个数字,3 < 5,所以我们可以把小的那个数组中的 1 ,3 排除掉了。
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 public double findMedianSortedArrays (int [] nums1, int [] nums2) { int n = nums1.length; int m = nums2.length; int left = (n + m + 1 ) / 2 ; int right = (n + m + 2 ) / 2 ; return (getKth(nums1, 0 , n - 1 , nums2, 0 , m - 1 , left) + getKth(nums1, 0 , n - 1 , nums2, 0 , m - 1 , right)) * 0.5 ; } private int getKth (int [] nums1, int start1, int end1, int [] nums2, int start2, int end2, int k) { int len1 = end1 - start1 + 1 ; int len2 = end2 - start2 + 1 ; if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k); if (len1 == 0 ) return nums2[start2 + k - 1 ]; if (k == 1 ) return Math.min(nums1[start1], nums2[start2]); int i = start1 + Math.min(len1, k / 2 ) - 1 ; int j = start2 + Math.min(len2, k / 2 ) - 1 ; if (nums1[i] > nums2[j]) { return getKth(nums1, start1, end1, nums2, j + 1 , end2, k - (j - start2 + 1 )); } else { return getKth(nums1, i + 1 , end1, nums2, start2, end2, k - (i - start1 + 1 )); } }