一道非常經典的題目,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];
}
如果有其他解法或者建議,歡迎探討~