二分(一)寻找两个有序数组的中位数
问题描述
给定两个有序的数组 \(nums1\) 和 \(nums2\),现在要求得这两个数组元素组成的序列中的中位数
要求:算法的时间复杂度要在 \(log_2n\) 级别内
解决思路
只要遍历了数组,那么算法的时间复杂度就为 \(O(n)\),不满足要求
一般时间复杂度为 \(O(log_2n)\) 的算法实现都是基于分治策略来实现的
-
分治
将原问题等效为:从两个有序数组中找到第 \(k\) 小的数(\(k\) 为中位数的位置)
首先分情况进行讨论:
- 两个数组的总个数为偶数:找到 \(k\) 小的元素和 \(k+1\) 小的元素,结果为两个元素的平均值
- 两个数组总个数为奇数:结果为第 \(k\) 小的元素
具体的分治思路:
-
默认第一个数组的长度要比第二个数组的长度要短,如果不满足这个条件则调换这两个数组
-
第一个数组的有效长度从 \(i\) 开始,第二个数组的有效长度从 \(j\) 开始。其中 \([i, s_i - 1]\) 表示第一个数组的前 \(k/2\) 个有效元素,\([j, s_j - 1]\) 表示第二个数组的前 \(k - k/2\) 个有效长度(这么做的目的是为了确保 \(k\) 为奇数是的正确性)
-
当 \(nums1[s_i - 1] > nums[s_j - 1]\) 时,表示第 \(k\) 小的元素一定不在 \([j, s_j - 1]\) 区间内,可以舍弃这个区间的元素
-
当 \(nums1[s_i - 1] \leq nums[s_j - 1]\) 时,表示第 \(k\) 小的元素一定不在 \([i, s_i - 1]\) 区间内,可以舍弃这个区间的元素
-
不断递归,重复上面的步骤,最终当 \(k\) 不断缩小到 \(1\) 时,就找到了第 \(k\)
-
二分搜索
假设两个有序数组分别是 \(A\) 和 \(B\),现在问题转换为在 \(A\) 和 \(B\) 两个数组中找到第 \(k\) 个元素。
要找到第 \(k\) 个元素,可以比较 \(A[k/2 - 1]\) 和 \(B[k/2 - 1]\) 。
由于 \(A[k/2 - 1]\) 和 \(B[k/2 - 1]\) 前分别有 \(A[0……k/2 - 2]\) 和 \(B[0……k/2 - 2]\),即 \(k/2 - 1\) 个元素,对于 \(A[k/2-1]\) 和 \(B[k/2 - 1]\) 中的较小值,最多只会有 \((k/2- 1) + (k/2 -1) \leq k - 2\) 个元素比它小,因此它便不是第 \(k\) 小的数了
因此可以归纳为以下集中情况:
- 如果 \(A[k/2 -1] < B[k/2-1]\),那么比 \(A[k/2 -1]\) 小的数只有前 \(k/2 - 1\) 个和 \(B\) 的前 \(k/2- 1\) 个数,即比 \(A[k/2 - 1]\) 小的数最多只有 \(k-2\) 个,因此 \(A[k/2 - 1]\) 不可能为第 \(k\) 个数,\(A[0]\) 到 \(A[k/2-1]\) 也都不可能是第 \(k\) 个数,可以全部排除
- 如果 \(A[k/2 - 1] > B[k/2 - 1]\) ,则可以排除 \(B[0]\) 到 \(B[k/2 - 1]\)
- 如果 \(A[k/2 - 1] = B[k/2 - 1]\) 可以归纳到第一种情况进行处理
存在的特殊情况:
- 如果 \(A[k/2 - 1]\) 或者 \(B[k/2 - 1]\) 越界,那么可以选择对应数组中的最后一个元素,在这种情况下必须根据排除的元素的个数减少 \(k\) 的值,而不能直接将 \(k\) 减去 \(k/2\)
- 如果一个数组为空,说明这个数组中的所有的元素都已经被排除完毕了,此时可以直接返回另一个数组中第 \(k\) 小的元素
- 如果 \(k = 1\) ,那么只需要返回两个数组首元素的最小值即可
该实现思路与上文使用分治的思路是相对应的
实现
-
分治
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int total = nums1.length + nums2.length; // 元素个数为偶数的情况 if (total % 2 == 0) { int left = find(nums1, 0, nums2, 0, total / 2); int right = find(nums1, 0, nums2, 0, total / 2 + 1); return (left + right) / 2.0; } return find(nums1, 0, nums2, 0, total / 2 + 1); } int find(int[] nums1, int i, int[] nums2, int j, int k) { if (nums1.length - i > nums2.length - j) // 如果第一个数组的长度大于第二个数组的长度,则交换它们 return find(nums2, j, nums1, i, k); if (i >= nums1.length) // nums1 数组已经到达结尾,因此直接从 nums2 中获取对应的元素 return nums2[j + k - 1]; if (k == 1) // 终止条件 return Math.min(nums1[i], nums2[j]); int si = Math.min(i + k / 2, nums1.length), sj = j + k - k / 2; if (nums1[si - 1] > nums2[sj - 1]) return find(nums1, i, nums2, sj, k - sj + j); return find(nums1, si, nums2, j, k - si + i); } }
复杂度分析:
时间复杂度:\(O(log_2n)\)
空间复杂度:忽略由于递归调用带来的开销,空间复杂度为 \(O(1)\)
-
二分搜索
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int n1 = nums1.length, n2 = nums2.length; int total = n1 + n2; if (total % 2 == 0) return (getKthElement(nums1, nums2, total / 2) + getKthElement(nums1, nums2, total / 2 + 1) ) / 2.0; return getKthElement(nums1, nums2, total / 2 + 1); } int getKthElement(int[] nums1, int[] nums2, int k) { int n1 = nums1.length, n2 = nums2.length; int idx1 = 0, idx2 = 0; while (true) { if (idx1 == n1) return nums2[idx2 + k - 1]; if (idx2 == n2) return nums1[idx1 + k -1]; if (k == 1) return Math.min(nums1[idx1], nums2[idx2]); int half = k / 2; int newIdx1 = Math.min(idx1 + half, n1) - 1; int newIdx2 = Math.min(idx2 + half, n2) - 1; int pivot1 = nums1[newIdx1], pivot2 = nums2[newIdx2]; if (pivot1 <= pivot2) { k -= (newIdx1 - idx1 + 1); idx1 = newIdx1 + 1; } else { k -= (newIdx2 - idx2 + 1); idx2 = newIdx2 + 1; } } } }
复杂度分析:
时间复杂度:\(O(log_2n)\)
空间复杂度:\(O(1)\)
参考: