https://leetcode.com/problems/median-of-two-sorted-arrays/
鉴于这道题本事有明确的时间复杂度要求,并且题面原始的输入结构又是两个List,最好不要贪图省事直接合并两个List为一个List,重新sort再找中位数这种方法。不客气的说,这种答案对于学习实践数据结构算法几乎没有意义,遗憾的是评论区都是这样的答案。 https://leetcode.com/problems/median-of-two-sorted-arrays/discuss/?currentPage=1&orderBy=hot&query=
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
nums = nums1 + nums2
numsSorted = sorted(nums)
return self.median(numsSorted)
def median(self, numbers: list) -> float:
length = len(numbers)
if length%2==0:
return (numbers[int(length/2)]+numbers[int(length/2-1)])/2
else: return float(numbers[int((length-1)/2)])
鉴于此,我觉得读官方解题是非常重要。因为能想出出题人都没想到的point固然很好但是,有些看似高效的解,实则应用了原生函数,且忽略题面复杂度条件就不好了。官网上的解答,虽然对于代码细节有些时候也没有讲得很清楚,但是思路清晰。鉴于此,这篇会主要Dig官网的内容。
审题
https://leetcode.com/problems/median-of-two-sorted-arrays/solution/
中位数 median的数学意义是什么?
Dividing a set into two equal length subsets, that one subset is always greater than the other.
翻译过来就是把一个集合平分成两个子集,一个子集里的所有数比另一个子集里面的所有数值大。这里一共两个条件。数学表达是我乱写的,欢迎质疑指正。
- l e n ( L e f t ) = l e n ( R i g h t ) len(Left) = len(Right) len(Left)=len(Right)
- ∀ L e f t < ∀ R i g h t \forall Left<\forall Right ∀Left<∀Right
再来分析我们的题目。
已有A,B两个排序好的数组。如果我们先对A进行拆分。找到A合适的划分间隔(时间复杂度
O
(
m
−
1
)
O(m-1)
O(m−1)),再用同样的方法找到集合B的划分间隔(时间复杂度
O
(
n
−
1
)
O(n-1)
O(n−1))。现在内存空间中共有 A-left,A-right,B-left,B-right四个组。但是注意把A-left和B-left放Left把A-right,B-right放进Right 只能保证条件2。
正式因为没办法直接满足条件我们需要把上面提到的条件break down到两个数组A和B数组组成完整数组的情况,也就是根据A和B对于中位数重新定义上面两个条件。i代表A的索引,j代表B的索引。A长度m,B长度n。
- i + j = m − i + n − j ( o r : m − i + n − j + 1 ) i+j=m−i+n−j (or: m - i + n - j + 1) i+j=m−i+n−j(or:m−i+n−j+1)
- B [ j − 1 ] ≤ A [ i ] a n d A [ i − 1 ] ≤ B [ j ] B[j−1]≤A[i] \quad and \quad \text{A}[i-1] \leq \text{B}[j] B[j−1]≤A[i]andA[i−1]≤B[j]
注意公式1就是:
2
(
i
+
j
)
=
m
+
n
2(i+j) = m+n
2(i+j)=m+n。再转换下更明显:
m
+
n
2
=
i
+
j
\frac{m+n}{2} = i+j
2m+n=i+j其实还是在定义条件1。A数组的间隔索引i加上B数组的间隔索引j需要刚好等于总数据量的一半。一会儿补个图更清晰。另外由于i和j实际上都必须大于0。原作者补充了一句。
i
f
n
≥
m
:
i
=
0
∼
m
,
j
=
m
+
n
+
1
2
−
i
if \quad n \geq m : i = 0 \sim m, j = \frac{m + n + 1}{2} - i
ifn≥m:i=0∼m,j=2m+n+1−i
条件2类推也是对于原来公式2的发展。左边最后一个数组一定比右边第一个数字大。
注意这里原作者说了他只是写了个简单的表达忽略了i,j =0,和i=m,j=n的情况。
推导到此结束,实际运算会用的运算逻辑就是
从区间[0,m]中选择i,i需要符合下面两个大条件
- B [ j − 1 ] ≤ A [ i ] a n d A [ i − 1 ] ≤ B [ j ] \text{B}[j-1] \leq \text{A}[i] \quad and \quad \text{A}[i-1] \leq \text{B}[j] B[j−1]≤A[i]andA[i−1]≤B[j]
-
j
=
m
+
n
+
1
2
−
i
j = \frac{m + n + 1}{2} - i
j=2m+n+1−i
好好利用数据已经排好序的这个特点,可以用两分法更快的找到 i i i和其对应的 j j j。这个和正常的二分查找差不多。
二分查找
- 设置i的查找区间 i m i n = 0 , i m a x = m imin = 0, imax = m imin=0,imax=m
- 初始化A和B的查找分隔点 i = i m i n + i m a x 2 i = \frac{imin+imax}{2} i=2imin+imax j = m + n + 1 2 − i \quad j= \frac{m + n + 1}{2} - i j=2m+n+1−i
- 1和2 的初始化设置可以满足条件2使得我们目标的Left和Right等长但是并不满足条件1,所以还需要根据判断结果进行操作。
- 直接满足 B [ j − 1 ] ≤ A [ i ] a n d A [ i − 1 ] ≤ B [ j ] \text{B}[j-1] \leq \text{A}[i] \quad and \quad \text{A}[i-1] \leq \text{B}[j] B[j−1]≤A[i]andA[i−1]≤B[j]结束游戏
- B [ j − 1 ] > A [ i ] B[j−1]>A[i] B[j−1]>A[i] 我们需要大一点的i,imin+1再次执行二分查找
- A [ i − 1 ] > B [ j ] : A[i−1]>B[j]: A[i−1]>B[j]: 我们需要小一点的i,imax-1再次执行二分查找
- j, i 确定后有两种情况
- m+n为odd, m e d i a n = m a x ( A [ i − 1 ] , B [ j − 1 ] ) median = max(A[i−1],B[j−1]) median=max(A[i−1],B[j−1])
- m+n为even, m e d i a n = m a x ( A [ i − 1 ] , B [ j − 1 ] ) + m i n ( A [ i ] , B [ j ] ) 2 median = \frac{max(A[i−1],B[j−1])+min(A[i],B[j])}{2} median=2max(A[i−1],B[j−1])+min(A[i],B[j])
代码
看完上面之后代码应该已经非常清楚了。当然你和官网下面的comments一样觉得不清楚的化,我在browse的过程中发现大家都推荐
https://medium.com/@hazemu/finding-the-median-of-2-sorted-arrays-in-logarithmic-time-1d3f2ecbeb46/
都是在说同一个事儿,这个有图可能清楚很多。因为限制了算法复杂度,只有按着这个思路来的才是正确答案。
class Solution {
public double findMedianSortedArrays(int[] A, int[] B) {
int m = A.length;
int n = B.length;
if (m > n) { // to ensure m<=n
int[] temp = A; A = B; B = temp;
int tmp = m; m = n; n = tmp;
}
int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;
while (iMin <= iMax) {
int i = (iMin + iMax) / 2;
int j = halfLen - i;
if (i < iMax && B[j-1] > A[i]){
iMin = i + 1; // i is too small
}
else if (i > iMin && A[i-1] > B[j]) {
iMax = i - 1; // i is too big
}
else { // i is perfect
int maxLeft = 0;
if (i == 0) { maxLeft = B[j-1]; }
else if (j == 0) { maxLeft = A[i-1]; }
else { maxLeft = Math.max(A[i-1], B[j-1]); }
if ( (m + n) % 2 == 1 ) { return maxLeft; }
int minRight = 0;
if (i == m) { minRight = B[j]; }
else if (j == n) { minRight = A[i]; }
else { minRight = Math.min(B[j], A[i]); }
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
}
def median(A, B):
m, n = len(A), len(B)
if m > n:
A, B, m, n = B, A, n, m
if n == 0:
raise ValueError
imin, imax, half_len = 0, m, (m + n + 1) / 2
while imin <= imax:
i = (imin + imax) / 2
j = half_len - i
if i < m and B[j-1] > A[i]:
# i is too small, must increase it
imin = i + 1
elif i > 0 and A[i-1] > B[j]:
# i is too big, must decrease it
imax = i - 1
else:
# i is perfect
if i == 0: max_of_left = B[j-1]
elif j == 0: max_of_left = A[i-1]
else: max_of_left = max(A[i-1], B[j-1])
if (m + n) % 2 == 1:
return max_of_left
if i == m: min_of_right = B[j]
elif j == n: min_of_right = A[i]
else: min_of_right = min(A[i], B[j])
return (max_of_left + min_of_right) / 2.0
算法复杂度解释
时间复杂度
- 由于集合A间隔i取值有m种情况所以为 O ( m ) O(m) O(m)
- 由于二分查找的使用每次查找后搜寻空间少一半所以有 O ( l o g ( m ) ) O(log(m)) O(log(m))
- 又由于其实m和n的值相互制约的,实际上m和n之间最少值确定了取值范围
O
(
l
o
g
(
m
i
n
(
m
,
n
)
)
)
O(log(min(m,n)))
O(log(min(m,n)))
空间复杂度
由于一直是在算索引,只需要几个记录的常数,和ij变量中间都不再存数组相关值共9个[A,B,m,n,i,j,imin,imax,half_len]。
空间复杂度
O ( 1 ) O(1) O(1)