Problem
LeetCode 4. Median of Two Sorted Arrays
Difficulty : Hard
There are two sorted arrays nums1 and nums2 of size m and n respectively.
Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).
You may assume nums1 and nums2 cannot be both empty.
Example 1:
1 | nums1 = [1, 3] |
Example 2:
1 | nums1 = [1, 2] |
Analysis
这道题需要在两个数组中找出中位数,看上去是并没有什么难度,最简单的办法就是将两个数组排序后直接取中位数位置上的值,但是这样做的话复杂度并不能达到O(log(m+n))
。因此,我们需要寻找复杂度较低的算法。
首先,我想到的是先确定中位数的位置。
根据两个数组的长度,可以得出:
- 当元素总个数为奇数时,下标为
(m + n - 1) / 2
的元素为中位数 - 当元素总个数为偶数时,下标为
(m + n) / 2 - 1
的元素与下标为(m + n) / 2
的元素的平均值为中位数
因此,算法的主要任务就是找出这一(两)个数
Approach 1 (伪)
一开始的思路是从一端开始遍历这两个数组,这里从开头开始遍历,使用两个变量i
, j
指向两个数组里面的元素。
每次遍历时:
当i
和j
都在数组范围之内时,将比较小的元素所在的数组的变量增加。
当i
或j
不在其数组范围时,将另一个变量增加
当算法执行k
次后,自然就找到数组中第k
大的元素,这样一来,就可以找到其中位数。
代码如下:
1 | func findMedianSortedArrays(nums1 []int, nums2 []int) float64 { |
但是这个算法的复杂度是O(m+n)
的,并不符合题目所要求的O(log(m+n))
, 因此还要继续寻找复杂度更低的算法。
Approach 2
上面的算法是从一端开始寻找这个中位数的,这里可以改进一下,从一开始就从中间去寻找这个中位数,再根据当前情况来将所寻找的位置前移或后移
首先,来看看一个数组里面的情况
当数组的长度为偶数的时候:
先把数组划分为两个部分
A[0], ..., A[i - 1] || A[i], ..., A[n - 1]
使得这两部分的元素的数量相同(i == n - i
),此时,(A[i] + A[i - 1]) / 2
就是中位数,
这两部分中,后者的最小值需要大于前者的最大值,因此可以得到以下两个结论:
len(left_part) == len(right_part)
max(left_part) < min(right_part)
然后,把这个规律扩展到两个数组的时候
同样把数组划分成两个部分
A[0], ..., A[i - 1] || A[i], ... , A[m - 1]
B[0], ..., B[j - 1] || B[j], ... , B[n - 1]
并且左右两个部分满足上面两个条件,即:
i + j == m - i + n - j
==j = (n + m) / 2 - i
(0 <= i <= m )
max(A[i - 1], B[j - 1]) < min (A[i], B[j])
这里的j
是通过i
计算出来的,因此需要确保j
是非负数,因此需要保证n > m
。
此时,中位数就是 [max(A[i - 1], B[j - 1]) + min (A[i], B[j])] / 2
在两个数组的元素数量总和为奇数的时候,自然就没办法将数组划分为相等的两部分,因此需要统一将多出来的一个元素放到左边或者右边,当统一左边部分比右边部分多一个的时候j
的表达式为j = (n + m + 1) / 2 - i
。
此时,中位数就是max(A[i - 1], B[j - 1])
这样一来,算法的核心就变成如何快速找到一个i
,满足上面的条件。
因为i
的取值范围是0 <= i <= m
, 因此比较好的方案就是取其取值范围的中间值,然后判断是否满足条件,调整取值范围然后再次找中间值。
因此可以根据以下的步骤快速找到i
的值:
确保
n > m
, 如果不满足则将两个数组交换过来确定搜索的范围,令
iMin = 0
,iMax = m
使得
i = (iMin + iMax) / 2
,j = (n + m + 1) / 2 - i
- 如果
i
的取值范围内的元素只剩下一个,那么就说明当前值是符合要求的,跳到下一步 - 如果
B[j - 1] <= A [i]
&&A[i - 1] <= B [j]
, 那么就可以判断左边部分的最大值小于右边部分的最小值,跳到下一步 - 如果
B[j - 1] > A [i]
, 那么说明满足要求的i
比当前值要大,因此可以将搜索范围的下界设置为i + 1
,即iMin = i + 1
, 然后跳转到第 3 步再次搜索 - 如果
A[i - 1] > B [j]
, 那么说明满足要求的i
比当前值要小,因此可以将搜索范围的上界设置为i - 1
,即iMax = i - 1
, 然后跳转到第 3 步再次搜索
- 如果
确定
i
的值为当前值
至此,根据得到的i
和j
, 已经成功将两个数组划分为两个部分。
A[0], ..., A[i - 1] || A[i], ... , A[m - 1]
B[0], ..., B[j - 1] || B[j], ... , B[n - 1]
最后,对于一些特殊情况,比如i
和j
在两个数组的边界之中,还需要额外处理一下,
当两个数据的元素个数和为奇数的时候:
在一般情况下中位数就是max(A[i - 1], B[j - 1])
因为需要保证分组满足上面两个条件,同时需要保证数组的下标不会越界,因此
- 当
i = 0
的时候,A[i - 1]
是不存在的,可以看作A
整个数组都在右边的部分,因此中位数取另一个数B[j - 1]
- 当
j = 0
的时候,同样的情况,中位数取A[i - 1]
当两个数据的元素个数和为偶数的时候:
当i = m
的时候:
在一般情况下中位数取[max(A[i - 1], B[j - 1]) + min (A[i], B[j])] / 2
max(A[i - 1], B[j - 1])
这个我们可以通过上面奇数情况上的计算的结果,同时需要考虑min (A[i], B[j])
越界的情况:
当
i = m
的时候,A[i]
是不存在的,因此可以看作A
整个数组都在左边的部分,因此右边的部分的值可以取B[j]
当
j = n
的时候,同理右边部分的值取A[i]
现在来分析一下具体的情况:
例:
A = [1, 5]
B = [2, 3, 4, 6]
因此
i
的搜索范围是[0, 2],取中间值为 1此时
j = (2 + 4 + 1)/ 2 - 1 = 2
因此可以划分为
Left Right 1 5 2, 3 4, 5 显然左边的最大值小于右边的最小值,已经条件,因此可以确定
i = 1
左边部分的数可以最大值 3,右边的数取最小值 4,因此中位数为
(3 + 4) / 2 = 3.5
代码如下:
1 | func findMedianSortedArrays(nums1 []int, nums2 []int) float64 { |
这种算法的搜索范围一开始是 0 到 m,然后一直对半地缩小搜索范围,有点类似二分法的思想,因此他的复杂度仅仅为O(log(min(m, n)))
, 显然已经达到了题目所要求的O(log (m+n))
.
Summary
这道题要找出答案并不难,难点在于如果在O(log (m+n))
的复杂度下找出答案,主要是运用了中位数的基本定义以及二分法的思想,将复杂度降低为O(log(min(m, n)))
。