寻找第K大的数
朴素的寻找第 $K$ 大元素可以排序,再找到第 $k$ 个,时间复杂度为 $O(n \times \log{n})$,但由于只需要第 $k$ 大,因此这样会造成很大的时间上浪费。 维护一个 $k$ 大的堆的时间复杂度为 $O(n \times \log{k})$,有所改进,但时间复杂度仍不够优,且实现复杂。而基于分治思想的算法可以在 $O(n)$的时间内找到 第 $k$ 大的数。
原理
利用分治法寻找第 $k$ 大的数的原理与快速排序类似,每次选取一个基准元素,利用与快速排序相同的操作,找到基准元素在最终的数列中的位置,如果等于 $k$,
就直接return
,如果大于,在第k大的元素一定在右半部分,否则一定在左半部分。接着递归进行上述过程。由此,便可以有效地避免不必要的比较和排序操作,
降低算法的时间复杂度。
具体实现
int k_find(int num[], int l, int r, int k)
{
int i = l, j = r, m = (l+r)>>1; // binary improved.
int x = num[m];
while (i != j) {
while (j > m && num[j] > x) {
j--;
}
num[m] = num[j]; m = j;
while (i < m && num[i] < x) {
i++;
}
num[m] = num[i]; m = i;
}
num[i] = x;
if (i < k) {
return k_find(i+1, r, k);
} else if (i > k) {
return k_find(l, i-1, k);
} else {
return num[i];
}
}
算法复杂度分析
基于分治的查找第 $K$ 大的数的算法期望时间度为 $O(n)$, 其中,$n$ 为元素的个数。由于无额外空间占用,因此,空间复杂度为 $O(n)$。
扩展
多次查询
对于查找序列中的第 $K$ 大数的问题,如果需要多次查询,那么先排序在查找或者划分树的算法更为合适。
多个序列
LeetCode OJ上有这样一道题目:
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})$.
题目链接:004. Median of Two Sorted Arrays
拿到这个题目,不难想到可以在O(m+n)的时间内归并这两个有序数组,然后二分在 $O(\log{m+n})$ 的时间复杂度内找出中位数,但这显然不符合题目的复杂度要求。 这便要求我们设计出更加高效的算法。
将寻找中位数扩展至寻找两个有序序列的第 $K$ 大,又该如何做呢?中位数是第 $\frac{m+n}{2}$ 小的数,因此,可以认为这两个问题等价。
首先假设数组 $A$ 和 $B$ 的元素个数都大于 $\frac{k}{2}$,我们比较 $A[\frac{k}{2}-1]$ 和 $B[\frac{k}{2}-1]$ 两个元素,这两个元素分别表示 $A$ 的第 $\frac{k}{2}$ 小的元素和B的第 $\frac{k}{2}$ 小的元素。这两个元素比较共有三种情况:$>$、$<$和$=$。如果 $A[\frac{k}{2}-1] < B[\frac{k}{2}-1]$,这表示 $A[0]$ 到 $A[\frac{k}{2}-1]$ 的元素都在 $A$ 和 $B$ 合并之后的前 $k$ 小的元素中。换句话说,$A[\frac{k}{2}-1]$ 不可能大于两数组合并之后的第 $k$ 小值,所以 可以将其舍弃。
证明也很简单,可以采用反证法。
-
假设 $A[\frac{k}{2}-1]$ 大于合并之后的第 $k$ 小值,我们不妨假定其为第 $(k+1)$ 小值。由于 $A[\frac{k}{2}-1]$ 小于 $B[\frac{k}{2}-1]$, 所以 $B[\frac{k}{2}-1]$ 至少是第 $(k+2)$ 小值。但实际上,在 $A$ 中至多存在 $\frac{k}{2}-1$ 个元素小于 $A[\frac{k}{2}-1]$, $B$ 中也至多存在 $\frac{k}{2}-1$ 个元素小于 $A[\frac{k}{2}-1]$,所以小于 $A[\frac{k}{2}-1]$ 的元素个数至多有 $\frac{k}{2}+\frac{k}{2}-2 = k-2$,小于 $k$, 这与 $A[\frac{k}{2}-1]$ 是第 $(k+1)$ 的数矛盾。
-
当 $A[\frac{k}{2}-1] > B[\frac{k}{2}-1]$ 时存在类似的结论。
-
当 $A[\frac{k}{2}-1] = B[\frac{k}{2}-1]$ 时,我们已经找到了第 $k$ 小的数,也即这个相等的元素,我们将其记为 $m$。由于在 $A$ 和 $B$ 中分别 有 $\frac{k}{2}-1$ 个元素小于 $m$,所以 $m$ 即是第 $k$ 小的数(需要注意的是,如果k为奇数,则m不是中位数。这里是进行了理想化考虑,在实际 代码中略有不同,是先求 $\frac{k}{2}$,然后利用 $k-\frac{k}{2}$ 获得另一个数)。
通过上面的分析,我们即可以采用递归的方式实现寻找第 $k$ 小的数。此外我们还需要考虑几个边界条件:
- 如果 $A$ 或者 $B$ 为空,则直接返回 $B[k-1]$ 或者 $A[k-1]$;
- 如果 $k$ 为 $1$,我们只需要返回 $A[0]$ 和 $B[0]$ 中的较小值;
- 如果 $A[\frac{k}{2}-1] = B[\frac{k}{2}-1]$,返回其中一个。
给出这一算法的Python实现:
def findMedianSortedArrays(nums1, nums2):
def findKth(a, m, b, n, k):
if m > n:
return findKth(b, n, a, m, k)
if m == 0:
return b[k-1]
if k == 1:
return min(a[0], b[0])
pa = min(k//2, m)
pb = k - pa
if a[pa-1] < b[pb-1]:
return findKth(a[pa:], m-pa, b, n, k-pa)
elif (a[pa-1] > b[pb-1]):
return findKth(a, m, b[pb:], n-pb, k-pb)
else:
return a[pa - 1];
m, n = len(nums1), len(nums2)
if (m+n) % 2 == 0:
return (findKth(nums1, m, nums2, n, (m+n)//2) +
findKth(nums1, m, nums2, n, (m+n)//2+1)) / 2.0
else:
return findKth(nums1, m, nums2, n, (m+n)//2+1)
寻找前 $k$ 个数
上面提到的算法仅仅要求找到第 $k$ 个数,那么,如果变换一下问题,要求找到前 $k$ 个数,又该怎么做呢?
- 如果不要求前 $k$ 个数有序,那么复杂度等同于找到第 $k$ 个数,因为在找到第 $k$ 个数的时候,前面的数都比第 $k$ 个数小。
- 如果要求前 $k$ 个数有序,那么在找到之后进行一次排序即可。复杂度 $O(n \times \log{k})$。