寻找第K大的数
一、概述
朴素的寻找第k大元可以排序,再找到第k个,时间复杂度为O(n * log(n)),但由于只需要第k大,因此这样会造成很大的时间上浪费。维护一个k大的堆的时间复杂度为O(n * 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大,又该如何做呢?中位数是第(m+n)/2小的数,因此,可以认为这两个问题等价。
首先假设数组A和B的元素个数都大于k/2,我们比较A[k/2-1]和B[k/2-1]两个元素,这两个元素分别表示A的第k/2小的元素和B的第k/2小的元素。这两个元素比较共有三种情况:>、<和=。如果A[k/2-1]<B[k/2-1],这表示A[0]到A[k/2-1]的元素都在A和B合并之后的前k小的元素中。换句话说,A[k/2-1]不可能大于两数组合并之后的第k小值,所以我们可以将其抛弃。
证明也很简单,可以采用反证法。
假设A[k/2-1]大于合并之后的第k小值,我们不妨假定其为第(k+1)小值。由于A[k/2-1]小于B[k/2-1],所以B[k/2-1]至少是第(k+2)小值。但实际上,在A中至多存在k/2-1个元素小于A[k/2-1],B中也至多存在k/2-1个元素小于A[k/2-1],所以小于A[k/2-1]的元素个数至多有k/2+k/2-2,小于k,这与A[k/2-1]是第(k+1)的数矛盾。
当A[k/2-1]>B[k/2-1]时存在类似的结论。
当A[k/2-1]=B[k/2-1]时,我们已经找到了第k小的数,也即这个相等的元素,我们将其记为m。由于在A和B中分别有k/2-1个元素小于m,所以m即是第k小的数。(这里可能有人会有疑问,如果k为奇数,则m不是中位数。这里是进行了理想化考虑,在实际代码中略有不同,是先求k/2,然后利用k-k/2获得另一个数。)
通过上面的分析,我们即可以采用递归的方式实现寻找第k小的数。此外我们还需要考虑几个边界条件:
- 如果
A或者B为空,则直接返回B[k-1]或者A[k-1]; - 如果
k为1,我们只需要返回A[0]和B[0]中的较小值; - 如果
A[k/2-1]=B[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*log(k))。
参考
- Median of Two Sorted Arrays
- leetcode之 median of two sorted arrays
- Leetcode 4 Median of Two Sorted Arrays
- Time Bounds for Selection
Tags: Algorithm