最近看到了两个题目,有一定的相似性,就一起总结下。一道题目是合并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 ));     } }