寻找最小的k个数

题目描述

输入n个整数,输出其中最小的k个。

分析与解法

解法一

要求一个序列中最小的k个数,按照惯有的思维方式,则是先对这个序列从小到大排序,然后输出前面的最小的k个数。

至于选取什么的排序方法,我想你可能会第一时间想到快速排序(我们知道,快速排序平均所费时间为  n*logn  ),然后再遍历序列中前k个元素输出即可。因此,总的时间复杂度:  O(n * log n)+O(k)=O(n * log n)  。

解法二

咱们再进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序,即:

1、遍历n个数,把最先遍历到的k个数存入到大小为k的数组中,假设它们即是最小的k个数; 
2、对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为  O(k)  ); 
3、继续遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与kmax比较:如果  x < kmax  ,用x替换kmax,并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果  x >= kmax  ,则继续遍历不更新数组。

每次遍历,更新或不更新数组的所用的时间为  O(k)  或  O(0)  。故整趟下来,时间复杂度为  n*O(k)=O(n*k)  。

解法三

更好的办法是维护容量为k的最大堆,原理跟解法二的方法相似:

  • 1、用容量为k的最大堆存储最先遍历到的k个数,同样假设它们即是最小的k个数;
  • 2、堆中元素是有序的,令k1<k2<...<kmax(kmax设为最大堆中的最大元素)
  • 3、遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与堆顶元素kmax比较:如果  x < kmax  ,用x替换kmax,然后更新堆(用时logk);否则不更新堆。

这样下来,总的时间复杂度:  O(k+(n-k)*logk)=O(n*logk)  。此方法得益于堆中进行查找和更新的时间复杂度均为:  O(logk) (若使用解法二:在数组中找出最大元素,时间复杂度:  O(k))  。

解法四

在《数据结构与算法分析--c语言描述》一书,第7章第7.7.6节中,阐述了一种在平均情况下,时间复杂度为  O(N)  的快速选择算法。如下述文字:

Since we can sort the file in O(nlog n) time, one might expect to obtain a better time bound for selection. The algorithm we present to find the kth smallest element in a set S is almost identical to quicksort. In fact, the first three steps are the same. We will call this algorithm quickselect(叫做快速选择). Let |Si| denote the number of elements in Si(令|Si|为Si中元素的个数). The steps of quickselect are( 步骤如下 ):

  1. If |S| = 1, then k = 1 and return the elements in S as the answer. If a cutoff for small files is being used and |S| <=CUTOFF, then sort S and return the kth smallest element.
  2. Pick a pivot element, v (- S.(选取S中一个元素作为枢纽元v)
  3. Partition S - {v} into S1 and S2, as was done with quicksort. (将集合S-{v}分割成S1和S2,就像快速排序那样)
  4. If k <= |S1|, then the kth smallest element must be in S1. In this case, return quickselect (S1, k). If k = 1 + |S1|, then the pivot is the kth smallest element and we can return it as the answer. Otherwise, the kth smallest element lies in S2, and it is the (k - |S1| - 1)st smallest element in S2. We make a recursive call and return quickselect (S2, k - |S1| - 1). (如果k<=|S1|,那么第k个最小元素必然在S1中。在这种情况下,返回quickselect(S1,k)。如果k=1+|S1|,那么枢纽元素就是第k个最小元素,即找到,直接返回它。否则,这第k个最小元素就在S2中,即S2中的第(k-|S1|-1)个最小元素,我们递归调用并返回quickselect(S2,k-|S1|-1))。

In contrast to quicksort, quickselect makes only one recursive call instead of two. The worst case of quickselect is identical to that of quicksort and is O(n2). Intuitively, this is because quicksort's worst case is when one of S1 and S2 is empty; thus, quickselect(快速选择) is not really saving a recursive call. The average running time, however, is O(n)( 不过,其平均运行时间为O(N) ). The analysis is similar to quicksort's and is left as an exercise.

示例代码:

//QuickSelect 将第k小的元素放在 a[k-1]
void QuickSelect( int a[], int k, int left, int right )
{
    int i, j;
    int pivot;

    if( left + cutoff <= right )
    {
        pivot = median3( a, left, right );
        //取三数中值作为枢纽元,可以很大程度上避免最坏情况
        i = left; j = right - 1;
        for( ; ; )
        {
            while( a[ ++i ] < pivot ){ }
            while( a[ --j ] > pivot ){ }
            if( i < j )
                swap( &a[ i ], &a[ j ] );
            else
                break;
        }
        //重置枢纽元
        swap( &a[ i ], &a[ right - 1 ] );  

        if( k <= i )
            QuickSelect( a, k, left, i - 1 );
        else if( k > i + 1 )
            QuickSelect( a, k, i + 1, right );
    }
    else
        InsertSort( a + left, right - left + 1 );
}

这个快速选择SELECT算法,类似快速排序的划分方法。N个数存储在数组S中,再从数组中选取“中位数的中位数”作为枢纽元X,把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素,这种解法在平均情况下能做到  O(N)  的复杂度。

解法五

《算法导论》介绍了一个随机选取主元的选择算法RANDOMIZED-SELECT。它每次都是随机选取数列中的一个元素作为主元,在  O(n) 的时间内找到第k小的元素,然后遍历输出前面的k个小的元素。平均时间复杂度:  O(n+k)=O(n)  (当k比较小时)。

我们知道,快速排序是以固定的第一个或最后一个元素作为主元,每次递归划分都是不均等的,最后的平均时间复杂度为:  O(n*logn) 。而RANDOMIZED-SELECT与普通的快速排序不同,它每次递归都是随机选择序列,从第一个到最后一个元素中任一一个作为主元。

下面是RANDOMIZED-SELECT(A, p, r)完整伪码:

PARTITION(A, p, r)         //partition过程 p为第一个数,r为最后一个数
1  x ← A[r]               //以最后一个元素作为主元
2  i ← p - 1
3  for j ← p to r - 1
4       do if A[j] ≤ x
5             then i ← i + 1
6                  exchange A[i] <-> A[j]
7  exchange A[i + 1] <-> A[r]
8  return i + 1

RANDOMIZED-PARTITION(A, p, r)      //随机快排的partition过程
1  i ← RANDOM(p, r)                                 //i  随机取p到r中个一个值
2  exchange A[r] <-> A[i]                         //以随机的 i作为主元
3  return PARTITION(A, p, r)            //调用上述原来的partition过程

RANDOMIZED-SELECT(A, p, r, i)       //以线性时间做选择,目的是返回数组A[p..r]中的第i 小的元素
1  if p = r          //p=r,序列中只有一个元素
2      then return A[p]
3  q ← RANDOMIZED-PARTITION(A, p, r)   //随机选取的元素q作为主元
4  k ← q - p + 1                     //k表示子数组 A[p…q]内的元素个数,处于划分低区的元素个数加上一个主元元素
5  if i == k                        //检查要查找的i 等于子数组中A[p....q]中的元素个数k
6      then return A[q]        //则直接返回A[q]
7  else if i < k
8      then return RANDOMIZED-SELECT(A, p, q - 1, i)
          //得到的k 大于要查找的i 的大小,则递归到低区间A[p,q-1]中去查找
9  else return RANDOMIZED-SELECT(A, q + 1, r, i - k)
          //得到的k 小于要查找的i 的大小,则递归到高区间A[q+1,r]中去查找。

下面则是《算法导论》原版关于RANDOMIZED-SELECT(A, p, r)为  O(n)  的证明,阐述如下:

此RANDOMIZED-SELECT最坏情况下时间复杂度为Θ(n2),即使是要选择最小元素也是如此,因为在划分时可能极不走运,总是按余下元素中的最大元素进行划分,而划分操作需要O(n)的时间。

然而此算法的平均情况性能极好,因为它是随机化的,故没有哪一种特别的输入会导致其最坏情况的发生。

算法导论上,针对此 RANDOMIZED-SELECT算法平均时间复杂度为O(n)的证明 ,引用如下,或许,能给你我多点的启示(本来想直接引用第二版中文版的翻译文字,但在中英文对照阅读的情况下,发现第二版中文版的翻译实在不怎么样,所以,得自己一个一个字的敲,最终敲完修正如下),分4步证明:

  1. 当RANDOMIZED-SELECT作用于一个含有n个元素的输入数组A[p ..r]上时,所需时间是一个随机变量,记为T(n),我们可以这样得到线性期望值E [T(n)]的下界:程序RANDOMIZED-PARTITION会以等同的可能性返回数组中任何一个元素为主元,因此,对于每一个k,(1 ≤k ≤n),子数组A[p ..q]有k个元素,它们全部小于或等于主元元素的概率为1/n.对k = 1, 2,...,n,我们定指示器X k ,为:
    X k = I{子数组A[p ..q]恰有k个元素} , 
    我们假定元素的值不同,因此有 
    E[X k ]=1/n 
    当调用RANDOMIZED-SELECT并且选择A[q]作为主元元素的时候,我们事先不知道是否会立即找到我们所想要的第i小的元素,因为,我们很有可能需要在子数组A[p ..q - 1], 或A[q + 1 ..r]上递归继续进行寻找.具体在哪一个子数组上递归寻找,视第i小的元素与A[q]的相对位置而定.
  2. 假设T(n)是单调递增的,我们可以将递归所需时间的界限限定在输入数组时可能输入的所需递归调用的最大时间(此句话,原中文版的翻译也是有问题的).换言之,我们断定,为得到一个上界,我们假定第i小的元素总是在划分的较大的一边,对一个给定的RANDOMIZED-SELECT,指示器Xk刚好在一个k值上取1,在其它的k值时,都是取0.当Xk =1时,可能要递归处理的俩个子数组的大小分别为k-1,和n-k,因此可得到递归式为

取期望值为:

为了能应用等式 (C.23) ,我们依赖于X k 和T(max(k - 1,n - k))是独立的随机变量(这个可以证明,证明此处略)。 
3. 下面,我们来考虑下表达式max(k - 1,n -k)的结果.我们有: 
 
如果n是偶数,从T(⌉)到T(n - 1)每个项在总和中刚好出现俩次,T(⌋)出现一次。因此,有 

我们可以用替换法来解上面的递归式。假设对满足这个递归式初始条件的某个常数c,有T(n) ≤cn。我们假设对于小于某个常数c(稍后再来说明如何选取这个常数)的n,有T(n) =O(1)。 同时,还要选择一个常数a,使得对于所有的n>0,由上式中O(n)项(用来描述这个算法的运行时间中非递归的部分)所描述的函数,可由an从上方限界得到(这里,原中文版的翻译的确是有点含糊)。利用这个归纳假设,可以得到:

  1. 为了完成证明,还需要证明对足够大的n,上面最后一个表达式最大为cn,即要证明:cn/4 -c/2 -an ≥ 0.如果在俩边加上c/2,并且提取因子n,就可以得到n(c/4 -a) ≥c/2.只要我们选择的常数c能满足c/4 -a > 0, i.e.,即c > 4a,我们就可以将俩边同时除以c/4 -a, 最终得到:

综上,如果假设对n < 2c/(c -4a),有T(n) =O(1),我们就能得到E[T(n)] =O(n)。所以,最终我们可以得出这样的结论,并确认无疑: 在平均情况下,任何顺序统计量(特别是中位数)都可以在线性时间内得到 

结论:RANDOMIZED-SELECT的时间复杂度为  O(N)  ,但它在最坏情况下时间的复杂度为  O(N^2)  。

解法五

《算法导论》第九章第9.3节介绍了一个最坏情况线性时间的选择算法,如下:

9.3 Selection in worst-case linear time( 最坏情况下线性时间的选择算法 

We now examine a selection algorithm whose running time isO(n) in the worst case(现在来看,一个最坏情况运行时间为O(N)的选择算法SELECT). Like RANDOMIZED-SELECT, the algorithm SELECT finds the desired element by recursively partitioning the input array. The idea behind the algorithm, however, is toguarantee a good split when the array is partitioned. SELECT uses the deterministic partitioning algorithm PARTITION from quicksort (seeSection 7.1), modified to take the element to partition around as an input parameter(像RANDOMIZED-SELECT一样,SELECTT通过输入数组的递归划分来找出所求元素,但是,该算法的基本思想是要保证对数组的划分是个好的划分。SECLECT采用了取自快速排序的确定性划分算法partition,并做了修改,把划分主元元素作为其参数).

The SELECT algorithm determines theith smallest of an input array ofn > 1 elements by executing the following steps. (Ifn = 1, then SELECT merely returns its only input value as theith smallest.)(算法SELECT通过执行下列步骤来确定一个有n>1个元素的输入数组中的第i小的元素。(如果n=1,则SELECT返回它的唯一输入数值作为第i个最小值。))

  1. Divide then elements of the input array into⌋ groups of 5 elements each and at most one group made up of the remainingn mod 5 elements.
  2. Find the median of each of the⌉ groups by first insertion sorting the elements of each group (of which there are at most 5) and then picking the median from the sorted list of group elements.
  3. Use SELECT recursively to find the medianx of the⌉ medians found in step 2. (If there are an even number of medians, then by our convention,x is the lower median.)
  4. Partition the input array around the median-of-mediansx using the modified version of PARTITION. Letk be one more than the number of elements on the low side of the partition, so thatx is thekth smallest element and there aren-k elements on the high side of the partition.(利用修改过的partition过程,按中位数的中位数x对输入数组进行划分,让k比划低去的元素数目多1,所以,x是第k小的元素,并且有n-k个元素在划分的高区)
  5. Ifi =k, then returnx. Otherwise, use SELECT recursively to find theith smallest element on the low side ifi k.( 如果要找的第i小的元素等于程序返回的k ,即i=k,则返回x。否则,如果ik,则在高区间找第(i-k)个最小元素)

(以上五个步骤,即本文上面的第四节末中所提到的所谓“五分化中项的中项”的方法。)

To analyze the running time of SELECT, we first determine a lower bound on the number of elements that are greater than the partitioning element x. (为了分析SELECT的运行时间,先来确定大于划分主元元素x的的元素数的一个下界)Figure 9.1 is helpful in visualizing this bookkeeping. At least half of the medians found in step 2 are greater than[1] the median-of-medians x. Thus, at least half of the ⌉ groupscontribute 3 elements that are greater than x, except for the one group that has fewer than 5 elements if 5 does not dividen exactly, and the one group containingx itself. Discounting these two groups, it follows that the number of elements greater thanx is at least:

(Figure 9.1: 对上图的解释或称对SELECT算法的分析:n个元素由小圆圈来表示,并且每一个组占一纵列。组的中位数用白色表示,而各中位数的中位数x也被标出。(当寻找偶数数目元素的中位数时,使用下中位数)。箭头从比较大的元素指向较小的元素,从中可以看出,在x的右边,每一个包含5个元素的组中都有3个元素大于x,在x的左边,每一个包含5个元素的组中有3个元素小于x。大于x的元素以阴影背景表示。 )

Similarly, the number of elements that are less thanx is at least 3n/10 - 6. Thus, in the worst case, SELECT is called recursively on at most 7n/10 + 6 elements in step 5.

We can now develop a recurrence for the worst-case running timeT(n) of the algorithm SELECT. Steps 1, 2, and 4 take O(n) time. (Step 2 consists ofO(n) calls of insertion sort on sets of sizeO(1).) Step 3 takes timeT(⌉), and step 5 takes time at mostT(7n/10+ 6), assuming thatT is monotonically increasing. We make the assumption, which seems unmotivated at first, that any input of 140 or fewer elements requiresO(1) time; the origin of the magic constant 140 will be clear shortly. We can therefore obtain the recurrence: 

We show that the running time is linear by substitution. More specifically, we will show thatT(n) ≤cn for some suitably large constant c and alln > 0. We begin by assuming thatT(n) ≤cn for some suitably large constantc and alln ≤ 140; this assumption holds ifc is large enough. We also pick a constanta such that the function described by theO(n) term above (which describes the non-recursive component of the running time of the algorithm) is bounded above byan for alln > 0. Substituting this inductive hypothesis into the right-hand side of the recurrence yields

T(n) ≤ c⌉ +c(7n/10 + 6) +an
     ≤ cn/5 +c + 7cn/10 + 6c +an
     = 9cn/10 + 7c +an
     = cn + (-cn/10 + 7c +an) ,

which is at mostcn if 

Inequality (9.2) is equivalent to the inequalityc ≥ 10a(n/(n - 70)) when n > 70. Because we assume thatn ≥ 140, we have n/(n - 70) ≤ 2, and so choosing c ≥ 20a will satisfyinequality (9.2). (Note that there is nothing special about the constant 140; we could replace it by any integer strictly greater than 70 and then choosec accordingly.) The worst-case running time of SELECT is therefore linear( 因此,此SELECT的最坏情况的运行时间是线性的 ).

As in a comparison sort (seeSection 8.1), SELECT and RANDOMIZED-SELECT determine information about the relative order of elements only by comparing elements. Recall fromChapter 8 that sorting requiresΩ(n lgn) time in the comparison model, even on average (see Problem 8-1). The linear-time sorting algorithms in Chapter 8 make assumptions about the input. In contrast, the linear-time selection algorithms in this chapter do not require any assumptions about the input. They are not subject to the Ω(n lgn) lower bound because they manage to solve the selection problem without sorting.

(与比较排序(算法导论8.1节)中的一样,SELECT和RANDOMIZED-SELECT仅通过元素间的比较来确定它们之间的相对次序。在算法导论第8章中,我们知道在比较模型中,即使在平均情况下,排序仍然要  O(n*logn)  的时间。第8章得线性时间排序算法在输入上做了假设。 相反地,本节提到的此类似partition过程的SELECT算法不需要关于输入的任何假设,它们不受下界 O(n*logn)  的约束,因为它们没有使用排序就解决了选择问题 (看到了没,道出了此算法的本质阿))

Thus, the running time is linear because these algorithms do not sort; the linear-time behavior is not a result of assumptions about the input, as was the case for the sorting algorithms inChapter 8. Sorting requiresΩ(n lgn) time in the comparison model, even on average (see Problem 8-1), and thus the method of sorting and indexing presented in the introduction to this chapter is asymptotically inefficient.(所以,本节中的选择算法之所以具有线性运行时间,是因为这些算法没有进行排序;线性时间的结论并不需要在输入上所任何假设,即可得到)。

举一反三

1、谷歌面试题:输入是两个整数数组,他们任意两个数的和又可以组成一个数组,求这个和中前k个数怎么做?

分析:

 “假设两个整数数组为A和B,各有N个元素,任意两个数的和组成的数组C有N^2个元素。
   那么可以把这些和看成N个有序数列:
          A[1]+B[1] <= A[1]+B[2] <= A[1]+B[3] <=…
          A[2]+B[1] <= A[2]+B[2] <= A[2]+B[3] <=…
          …
         A[N]+B[1] <= A[N]+B[2] <= A[N]+B[3] <=…
    问题转变成,在这N^2个有序数列里,找到前k小的元素”

2、有两个序列A和B,A=(a1,a2,...,ak),B=(b1,b2,...,bk),A和B都按升序排列。对于1<=i,j<=k,求k个最小的(ai+bj)。要求算法尽量高效。

3、给定一个数列a1,a2,a3,...,an和m个三元组表示的查询,对于每个查询(i,j,k),输出ai,ai+1,...,aj的升序排列中第k个数。

时间: 2024-10-22 18:21:10

寻找最小的k个数的相关文章

寻找最小的k个数(四种方法)

1 使用从大到小的优先队列保存最小的K个数,每次取出K个数之后的其余数和堆顶元素比较,如果比堆顶元素小,则将堆顶元素删除,将该元素插入 void topK(int arr[],int n,int k) { if(k>n) return; priority_queue<int> q; for(int i=0;i<n;++i) { if(q.size()<k) q.push(arr[i]); else { if(arr[i]<q.top()) { q.pop(); q.pu

O(N)的时间寻找最大的K个数

寻找N个数中最大的K个数,本质上就是寻找最大的K个数中最小的那个,也就是第K大的数. 可以使用二分搜索的策略来寻找N个数中的第K大的数.对于一个给定的数p,可以在O(N)的时间复杂度内找出所有不小于p的数. 寻找第k大的元素: #include <iostream> using namespace std; //快速排序的划分函数 int partition(int a[],int l,int r) { int i,j,x,temp; i = l; j = r+1; x = a[l]; //将

c语言-编程算法 - 最小的k个数 代码(C)

问题描述 编程算法 - 最小的k个数 代码(C) 请解释一下在c语言中怎样编写在输入的N个数中找到k个最小的数 解决方案 排序吧,再输出前k个数 解决方案二: 遍历,找出MAX,移除MAX,循环K遍 解决方案三: 我觉得你的问题是怎么将输入的数保存下来,你可以先定义一个vector. vector vec;int iNUm = 0;while(cin>>iNum)//需要结束的时候输入ctrl+z;{ vec.push_back(iNum);}//最后对整个vec进行排序,取得最小的值 解决方

c++容器-关于 multiset 容器用法的疑问?问题来源于“找最小的k个数”

问题描述 关于 multiset 容器用法的疑问?问题来源于"找最小的k个数" 以下为"找最小k个数"中的一段代码,它使用了 multiset 容器,基于红黑树实现, 而这段算法的思想是 最大堆的思想,下面算法中 leastNumbers.begin() 应是指向最大值,这是为什么? 红黑树应是一颗二叉搜索树,为什么能保证 leastNumbers.begin() 指向最大值? typedef multiset<int, greater<int>

编程之美之2.5 寻找最大的K个数

[题目] 有很多无序的数,从中找出最大的K个数.假定他们都不相等. [解法一] 如果数据不是很多,例如在几千个左右,我们可以排一下序,从中找出最大的K个数.排序可以选择快速排序或者堆排序 [cpp] view plaincopy #include<stdio.h>   #include<stdlib.h>   int cmp(const void *a,const void *b){       return *(int *)a - *(int *)b;   }   int mai

JAVA中寻找最大的K个数解法_java

这个题拿到之后首先会想到排序,排好序之后在选取选取最大的K个数.排序选择快速排序是个比较好的选择.好了,让我们来进行第一个解法:快速排序代码如下 复制代码 代码如下: public static void quickSort(int[] arr, int start, int end) {  if (start < end) {   int key = arr[start];   int right = start;   int left = end;   while (right < lef

寻找最大的K个数,Top K问题的堆实现

//生成随机的不重复的测试数据 #include <iostream> #include <time.h> #include <assert.h> using namespace std; //产生[l,u]区间的随机数 int randint(int l, int u) { return l+(RAND_MAX*rand()+rand())%(u-l+1); } //1000W的int,大约4M的数据,如果放在mian内,在我的机子上好像是栈溢出了,放在全局空间就没问

输入n个整数并找出其中的最小k个数

题目: 输入n个整数, 找出其中的最小k个数. 使用快速排序(Quick Sort)的方法求解, 把索引值(index)指向前k个数. 代码: /* * main.cpp * * Created on: 2014.6.12 * Author: Spike */ /*eclipse cdt, gcc 4.8.1*/ #include <stdio.h> #include <stdlib.h> int RandomInRange(int min, int max) { int rand

【29】求无序序列中最小k个数

题目:给定一个无序序列和一个数k,求最小k个数(不要求数字有序) 分析: 方案一:最朴素的方法是利用快速排序,然后直接输出最小k个数,时间复杂度为O(nlogn) 方案二:利用快速排序的partition函数的性质,根据基准的性质,每次可以把序列分成两个部分,左边序列全部小于等于基准的值,右边序列全部大于等于序列的值.               只要找到基准的位置为刚好为第k的位置,则序列前面即为最小k个数.和找第k大的数其实是一个性质的,时间复杂度都是O(n).