题目链接: LeetCode|4.寻找两个正序数组的中位数
题面解释
找出两个有序数组合并后的中位数. 但要求时间复杂度\(\mathit{O}(log(m + n))\) .
解法一 二分
笔者看到复杂度\(\mathit{O}(log(m +
n))\) 第一想法便是二分, 但怎么二分? 两个数组只是内部有序,
两个数组并不有序, 常规对数组二分的思路肯定是不行的.
那该如何进行二分呢, 对答案进行二分. 令k为所寻找的k阶数, 初始为中位数,
则此时我们取两个数组的前\(k/2\) 部分出来, 比较\(nums1[k/2]\) 和\(nums2[k/2]\) , 则可以直接排除\(k/2\) 个数. 我们以下图例子来解释.
\(m = n = 4\) , 我们有 \(k = 4, k/2 = 2\) ,
于是我们取两个数组的前两位, 比较3和5, 不难看出, 1和3不可能是中位数.
为什么? 因为1和3的阶必定小于\(k = 4\) ,
即便第一个数组的3比第二个数组的5前面所有数都大, 3的阶也只能是3,
1就更不必说了. 而5前面的数可能大于3后面的数(例子中未能体现,
读者可以自行构造一个满足这种情况的简单例子). 因此通过这种操作,
我们可以排掉 \(k/2\)
个不可能是中位数的元素, 接着我们令\(k = k -
k/2\) , 将被排除的数去掉, 重新进行上述过程.
选取两个数组的前1位.比较2 和 4, 可以排除掉2. 此时过程进行到了边界处,
接下来\(k = 1\) 无法继续了.
而此时剩下的4和5正是我们需要的中位数.
Tips: 为什么第二个数组不是选择2, 5, 7?
因为我们在上面的步骤中无法确定2和5是中位数与否, 此时的有效信息不足.
而把2 5 7都放进来与4进行比较会出现bug, 以上例, 4会被错误的排除掉
k为奇数时情况稍有不同, 但大多是一些细节问题, 笔者在此不再赘述.
推荐读者自己构建一个例子去推算上面的过程, 相信不难理解其中要义.
每次排除\(k/2\) , 而 \(k = (m + n + 1) / 2\) , 故时间复杂度\(\mathit{O}(log(m + 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 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 class Solution {public : double findMedianSortedArrays (vector<int >& nums1, vector<int >& nums2) { int m = nums1. size (); int n = nums2. size (); int left1 = 0 ; int left2 = 0 ; int k = (m + n - 1 ) >> 1 ; while (k > 1 ) { if (left1 == m) { left2 += k; k = 0 ; break ; } if (left2 == n) { left1 += k; k = 0 ; break ; } int mid = k >> 1 ; int mid1 = left1 + mid - 1 ; int mid2 = left2 + mid - 1 ; if (left1 + mid > m) { mid1 = m - 1 ; } if (left2 + mid > n) { mid2 = n - 1 ; } if (nums1[mid1] >= nums2[mid2]) { k -= mid2 - left2 + 1 ; left2 = mid2 + 1 ; } else { k -= mid1 - left1 + 1 ; left1 = mid1 + 1 ; } } if (k == 1 ) { if (left1 == m) { left2++; } else if (left2 == n) { left1++; } else { if (nums1[left1] >= nums2[left2]) { left2++; } else { left1++; } } } if ((m + n) & 1 == 1 ) { if (left1 == m) { return nums2[left2]; } if (left2 == n) { return nums1[left1]; } return std::min (nums1[left1], nums2[left2]); } else { if (left1 == m) { return (nums2[left2] + nums2[left2 + 1 ]) * 1.0 / 2 ; } if (left2 == n) { return (nums1[left1] + nums1[left1 + 1 ]) * 1.0 / 2 ; } int num1; if (nums1[left1] >= nums2[left2]) { num1 = nums2[left2]; left2++; } else { num1 = nums1[left1]; left1++; } int num2; if (left1 == m) { num2 = nums2[left2]; } else if (left2 == n) { num2 = nums1[left1]; } else { num2 = min (nums1[left1], nums2[left2]); } return (num1 + num2) * 1.0 / 2 ; } return 0 ; } };
Tips: 本题思路并不难想出或者说并不难理解,
但非常考验代码功底, 对细节和边界情况的考察更像是此题的侧重点.
还望看到此处的读者能静下心来调试代码, 祝早日AC.
解法二 分割
笔者阅读题解时注意到的巧妙的解法, 原链接在此P4|windliang
更详细的数学证明, 思路和代码请阅读原文,
笔者在此只给出自己对其的理解.
本质来说, 两种方法殊途同归, 但分割的方法较为巧妙一些. 中位数,
或者说k阶数, 就是将数组分为两个部分, 其一是比k阶数小的部分,
其二是比k阶数大的部分.
对于有序的一个数组, 其性质本身就已经满足,
我们不妨将两部分简称为左半部分(小)和右半部分(大). 对于两个有序数组,
情况是一样的, 如果存在一种划分, 满足左边最大MaxLeft小于右边最小MinRight,
我们则认为这是关于k阶数的一种有效划分.
因此, 这道题就被转化为寻找数组中满足中位数的有效划分,
关于特定k阶数划分, 还需要满足左半部分和右半部分的数量关系,
若规定将k阶数划在左半部分, 则左半部分有k个元素, 右半部分有 n - k
个元素.
后面的事情就简单了, 固定数组1的划分位置,
根据数量关系找到数组2待判定划分位置,
根据MaxLeft和MinRight的大小关系决定数组1划分位置的左移或右移,
该过程可以通过二分完成.