寻找第K大的数

 Published On February 09, 2015

一、概述

朴素的寻找第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]
  • 如果k1,我们只需要返回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个数,又该怎么做呢?

  1. 如果不要求前k个数有序,那么复杂度等同于找到第k个数,因为在找到第k个数的时候,前面的书都比第k个数小。
  2. 如果要求前k个数有序,那么在找到之后进行一次排序即可。复杂度O(n*log(k))。

参考

  1. Median of Two Sorted Arrays
  2. leetcode之 median of two sorted arrays
  3. Leetcode 4 Median of Two Sorted Arrays
  4. Time Bounds for Selection

Tags: Algorithm



Comments:

comments powered by Disqus