【算法之美】求解兩個有序數組的中位數 — leetcode 4. Median of Two Sorted Arrays


一道非常經典的題目,Median of Two Sorted Arrays。(PS:leetcode 我已經做了 190 道,歡迎圍觀全部題解 https://github.com/hanzichi/leetcode

題意非常簡單,給定兩個有序的數組,求中位數,難度系數給的是 Hard,希望的復雜度是 log 級別。回顧下中位數,對於一個有序數組,如果數組長度是奇數,那么中位數就是中間那個值,如果長度是偶數,就是中間兩個數的平均數。

O(nlogn)

最容易想到的解法是 O(nlogn) 的解法,將兩個數組合並成一個,然后排序,排序用 JavaScript 數組內置的 sort 函數,復雜度 nlogn,最后根據數組長度選擇中位數,非常容易理解。

var findMedianSortedArrays = function(nums1, nums2) {
  // 合並數組
  var s = nums1.concat(nums2);

  // 排序
  s.sort(function(a, b) {
    return a - b;
  });

  var len = s.length;

  // 根據數組長度求中位數
  if (len & 1) return s[~~(len / 2)];
  else return (s[len / 2 - 1] + s[len / 2]) / 2;
};

O(n)

將兩個有序的數組合並成一個有序的數組,想到了什么?沒錯,這正是歸並排序的關鍵一步。

關於歸並排序,請看樓主以前寫的這篇 【前端也要學點算法】 歸並排序的JavaScript實現。這正是寫博客的好處之一,可以將知識體系串聯起來,比如這里我就不用介紹歸並排序了,因為那篇文章我已經寫的非常非常清楚了,而且就算現在我忘了,稍微看一遍也就能記起來了。

我們把歸並排序中的 merge 函數拉出來,就 ok 了,一次線性的循環,復雜度降到了 O(n)。(其實應該是 O(n+m),方案一也一樣,就不多加區別了)

var findMedianSortedArrays = function(nums1, nums2) {
  // 合並數組,返回有序數組
  var s = merge(nums1, nums2);

  var len = s.length;

  // 根據數組長度求中位數
  if (len & 1) return s[~~(len / 2)];
  else return (s[len / 2 - 1] + s[len / 2]) / 2;
};

function merge(left, right) {
  var tmp = [];

  while (left.length && right.length) {
    if (left[0] < right[0])
      tmp.push(left.shift());
    else
      tmp.push(right.shift());
  }

  return tmp.concat(left, right);
}

PS:其實對於此題,排序到一半就 ok 了,絕對的復雜度可以降到一半,不過也沒什么必要。

O(logn)

以上兩種解法,個人覺得難度系數對應的分別是 Easy 和 Medium,而 Hard 的解法應該把復雜度降到 log 級別。

換個方式思考,給出兩個有序數組,假設兩個數組的長度和是 len,如果 len 為奇數,那么我們求的就是兩個數組合並后的第 (len >> 1) + 1 大的數,如果 len 為偶數,就是第 (len >> 1) 和 (len >> 1) + 1 兩個數的平均數。

可以進一步擴展,給定兩個有序數組,求第 k 大數。有序 + log 級別的復雜度,想到了什么?二分查找。

假設兩個有序數組 a 和 b,長度分別是 m 和 n,求第 k 大數。假設在 a 中取 x 個,那么 b 數組中取的個數也就確定了,為 k - x 個,據此我們可以將兩個數組分別一分為二,根據兩邊的邊界值判斷此次划分是否合理。而對於 x 的值,我們可以用二分查找。二分查找可以用迭代或者遞歸,這里我參考了 leetcode之 median of two sorted arrays 的遞歸寫法,美中不足的是頻繁調用了 slice 方法,可能導致性能下降。

var findMedianSortedArrays = function(nums1, nums2) {
  var m = nums1.length;
  var n = nums2.length;
  var total = m + n;
  var half = total >> 1;

  if (total & 1)
    return findKth(nums1, m, nums2, n, half + 1);
  else 
    return (findKth(nums1, m, nums2, n, half) + findKth(nums1, m, nums2, n, half + 1)) / 2;
};


function findKth(a, m, b, n, k) { 
  // always assume that m is equal or smaller than n  
  if (m > n)  
    return findKth(b, n, a, m, k);  
  if (m === 0)  
    return b[k - 1];  
  if (k === 1)  
    return Math.min(a[0], b[0]); 

  // divide k into two parts  
  var pa = Math.min(k >> 1, m)
    , pb = k - pa;  

  if (a[pa - 1] < b[pb - 1])  
    return findKth(a.slice(pa), m - pa, b, n, k - pa);  
  else if (a[pa - 1] > b[pb - 1])  
    return findKth(a, m, b.slice(pb), n - pb, k - pb);  
  else 
    return a[pa - 1];  
}  

如果有其他解法或者建議,歡迎探討~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM