正确的 O(n) 解决方案相当复杂,需要大量的文本、代码和技巧来解释和证明。更准确地说,它需要 3 页才能令人信服地做到这一点,详情请参见 http://www.cse.yorku.ca/~andy/pubs/X+Y.pdf(在 cmets 中由 simonzack 找到)。
它基本上是一种聪明的分治算法,除其他外,它利用了这样一个事实,即在排序的 n×n 矩阵中,可以在O(n) 中找到较小的元素数量/大于给定数字k。它递归地将矩阵分解为更小的子矩阵(通过仅采用奇数行和列,从而产生具有n/2 列和n/2 行的子矩阵)结合上述步骤,结果复杂度为O(n) + O(n/2) + O(n/4)... = O(2*n) = O(n)。太疯狂了!
我无法比论文更好地解释它,这就是为什么我将解释一个更简单的O(n logn) 解决方案:)。
O(n * logn) 解:
这是一次采访!您无法及时得到O(n) 的解决方案。所以,嘿,为什么不提供一个解决方案,虽然不是最佳的,但表明你可以比其他明显的O(n²) 候选人做得更好?
我将利用上面提到的O(n) 算法,在排序的n-by-n 矩阵中找到小于/大于给定数字k 的数字数量。请记住,我们不需要实际的矩阵!正如 OP 所描述的,两个大小为n 的数组的笛卡尔和产生一个排序的n-by-n 矩阵,我们可以通过考虑数组的元素来模拟如下:
a[3] = {1, 5, 9};
b[3] = {4, 6, 8};
//a + b:
{1+4, 1+6, 1+8,
5+4, 5+6, 5+8,
9+4, 9+6, 9+8}
因此,每一行都包含非递减的数字,每一列也是如此。现在,假装给你一个号码k。我们想在O(n) 中找出这个矩阵中有多少个数字小于k,有多少个更大。显然,如果两个值都小于(n²+1)/2,则意味着k 是我们的中位数!
算法很简单:
int smaller_than_k(int k){
int x = 0, j = n-1;
for(int i = 0; i < n; ++i){
while(j >= 0 && k <= a[i]+b[j]){
--j;
}
x += j+1;
}
return x;
}
这基本上计算了每行有多少元素符合条件。由于行和列已经如上所示排序,这将提供正确的结果。由于i 和j 都最多迭代n 次,因此算法为O(n) [注意j 不会在for 循环中重置]。 greater_than_k 算法类似。
现在,我们如何选择k?那是logn 部分。 二分搜索! 正如其他答案/cmets 中提到的,中位数必须是此数组中包含的值:
candidates[n] = {a[0]+b[n-1], a[1]+b[n-2],... a[n-1]+b[0]};.
简单地排序这个数组[也是O(n*logn)],然后对其进行二分搜索。由于数组现在处于非递减顺序,因此可以直接注意到小于每个 candidate[i] 的数字数量也是非递减值(单调函数),这使其适用于二分搜索。结果smaller_than_k(k)返回小于(n²+1)/2的最大数k = candidate[i]就是答案,在log(n)迭代中获得:
int b_search(){
int lo = 0, hi = n, mid, n2 = (n²+1)/2;
while(hi-lo > 1){
mid = (hi+lo)/2;
if(smaller_than_k(candidate[mid]) < n2)
lo = mid;
else
hi = mid;
}
return candidate[lo]; // the median
}