程序员编程艺术学习笔记(三)寻找最小的k个数

来源:互联网 发布:mac浏览器自动跳转 编辑:程序博客网 时间:2024/04/29 08:19

程序员编程艺术原文出自大神:v_JULY_v  地址:http://blog.csdn.net/v_july_v/article/category/784066

c++实现.敬请膜拜。

仍旧:原文C++实现,现将自己实现的java版本记录如下,不定期更新,自我监督,自我监督。

此章涉及到一些排序内容,如排序理解不够深刻(如我),请回去看排序先。

此章包含作者很多纠结及讨论过程,包含书里的论证,排版及思路有些乱,如果没兴趣只记好的方式就行了,但是讨论本身很有意思。


程序员编程艺术:第三章、寻找最小的k个数

寻找最小的k个数
题目描述:5.查找最小的k个元素
题目:输入n个整数,输出其中最小的k个。
例如输入1,2,3,4,5,6,7和8这8个数字,则最小的4个数字为1,2,3和4。


注:下边的思路很多,可以选择性的看。

因为有一些讨论的成分,可以直接看结论。

运用类似快速排序的partition的快速选择SELECT算法寻找最小的k个元素能做到O(N)的复杂度。

以及,比较好的方法之一就是构建一个n元素的最小堆,取出堆顶元素,把最大的元素放在堆顶,然后下沉到k。

第二次取出第二个最小的堆顶元素,然后最大元素下沉至k-1,依次直到取出k个元素。

第一节、各种思路,各种选择

  • 0、   咱们先简单的理解,要求一个序列中最小的k个数,按照惯有的思维方式,很简单,先对这个序列从小到大排序,然后输出前面的最小的k个数即可。
  • 1、   至于选取什么的排序方法,我想你可能会第一时间想到快速排序,我们知道,快速排序平均所费时间为n*logn,然后再遍历序列中前k个元素输出,即可,总的时间复杂度为O(n*logn+k)=O(n*logn)
  • 2、   咱们再进一步想想,题目并没有要求要查找的k个数,甚至后n-k个数是有序的,既然如此,咱们又何必对所有的n个数都进行排序列?
 这时,咱们想到了用选择或交换排序,即遍历n个数,先把最先遍历到得k个数存入大小为k的数组之中,对这k个数,利用选择或交换排序,找到k个数中的最大数kmax(kmax设为k个元素的数组中最大元素),用时O(k)(你应该知道,插入或选择排序查找操作需要O(k)的时间),后再继续遍历后n-k个数,x与kmax比较:如果x<kmax,则x代替kmax,并再次重新找出k个元素的数组中最大元素kmax‘(多谢kk791159796 提醒修正);如果x>kmax,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),整趟下来,总的时间复杂度平均下来为:n*O(k)=O(n*k)

  • 3、   当然,更好的办法是维护k个元素的最大堆,原理与上述第2个方案一致,即用容量为k的最大堆存储最先遍历到的k个数,并假设它们即是最小的k个数,建堆费时O(k)后,有k1<k2<...<kmax(kmax设为大顶堆中最大元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,x<kmax,更新堆(用时logk),否则不更新堆。这样下来,总费时O(k+(n-k)*logk)=O(n*logk)。此方法得益于在堆中,查找等各项操作时间复杂度均为logk(不然,就如上述思路2所述:直接用数组也可以找出前k个小的元素,用时O(n*k))。
  • 4、 按编程之美第141页上解法二的所述,类似快速排序的划分方法,N个数存储在数组S中,再从数组中随机选取一个数X(随机选取枢纽元,可做到线性期望时间O(N)的复杂度,在第二节论述),把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素。像上述过程一样,这个运用类似快速排序的partition的快速选择SELECT算法寻找最小的k个元素,在最坏情况下亦能做到O(N)的复杂度。不过值得一提的是,这个快速选择SELECT算法是选取数组中“中位数的中位数”作为枢纽元,而非随机选取枢纽元。
  • 5、   RANDOMIZED-SELECT,每次都是随机选取数列中的一个元素作为主元,在0(n)的时间内找到第k小的元素,然后遍历输出前面的k个小的元素。 如果能的话,那么总的时间复杂度为线性期望时间:O(n+k)=O(n)(当k比较小时)

         Ok,稍后第二节中,我会具体给出RANDOMIZED-SELECT(A, p, r, i)的整体完整伪码。在此之前,要明确一个问题:我们通常所熟知的快速排序是以固定的第一个或最后一个元素作为主元,每次递归划分都是不均等的,最后的平均时间复杂度为:O(n*logn),但RANDOMIZED-SELECT与普通的快速排序不同的是,每次递归都是随机选择序列从第一个到最后一个元素中任一一个作为主元。

  • 6、   线性时间的排序,即计数排序,时间复杂度虽能达到O(n),但限制条件太多,不常用。
  • 7、   updated: huaye502在本文的评论下指出:“可以用最小堆初始化数组,然后取这个优先队列前k个值。复杂度O(n)+k*O(log n)”。huaye502的意思是针对整个数组序列建最小堆,建堆所用时间为O(n)(算法导论一书上第6章第6.3节已经论证,在线性时间内,能将一个无序的数组建成一个最小堆),然后取堆中的前k个数,总的时间复杂度即为:O(n+k*logn)

    关于上述第7点思路的继续阐述:至于思路7的O(n+k*logn)是否小于上述思路3的O(n*logk),即O(n+k*logn)?< O(n*logk)。粗略数学证明可参看如下第一幅图,我们可以这么解决:当k是常数,n趋向于无穷大时,求(n*logk)/(n+k*logn)的极限T,如果T>1,那么可得O(n*logk)>O(n+k*logn),也就是O(n+k*logn)< O(n*logk)。虽然这有违我们惯常的思维,然事实最终证明的确如此,这个极值T=logk>1,即采取建立n个元素的最小堆后取其前k个数的方法的复杂度小于采取常规的建立k个元素最大堆后通过比较寻找最小的k个数的方法的复杂度。但,最重要的是,如果建立n个元素的最小堆的话,那么其空间复杂度势必为O(N),而建立k个元素的最大堆的空间复杂度为O(k)。所以,综合考虑,我们一般还是选择用建立k个元素的最大堆的方法解决此类寻找最小的k个数的问题。

    思路3准确的时间复杂度表述为:O(k+(n-k)*logk),思路7准确的时间复杂度表述为:O(n+k*logn),也就是如gbb21所述粗略证明:要证思路3的时间复杂度大于思路7的时间复杂度,等价于要证原式k+n*logk-klogk-n-k*logn>0,即证(logk-1)n - k*logn + k - klogk > 0

事实上,是建立最大堆还是建立最小堆,其实际的程序运行时间相差并不大,运行时间都在一个数量级上。因为后续,我们还专门写了个程序进行测试,即针对1000w的数据寻找其中最小的k个数的问题,采取两种实现,一是采取常规的建立k个元素最大堆后通过比较寻找最小的k个数的方案,一是采取建立n个元素的最小堆,然后取其前k个数的方法,发现两相比较,运行时间实际上相差无几。结果可看下面的第二幅图。

  

  • 8、   @lingyun310:与上述思路7类似,不同的是在对元素数组原地建最小堆O(n)后,然后提取K次,但是每次提取时,换到顶部的元素只需要下移顶多k次就足够了,下移次数逐次减少(而上述思路7每次提取都需要logn,所以提取k次,思路7需要k*logn。而本思路8只需要K^2)。此种方法的复杂度为O(n+k^2)。@July:对于这个O(n+k^2)的复杂度,我相当怀疑。因为据我所知,n个元素的堆,堆中任何一项操作的复杂度皆为logn,所以按理说,lingyun310方法的复杂度应该跟下述思路8一样,也为O(n+k*logn),而非O(n+k*k)。ok,先放到这,待时间考证

  经过和几个朋友的讨论,已经证实,上述思路7lingyun310所述的思路应该是完全可以的。下面,我来具体解释下他的这种方法。

    我们知道,n个元素的最小堆中,可以先取出堆顶元素得到我们第1小的元素,然后把堆中最后一个元素(较大的元素)上移至堆顶,成为新的堆顶元素(取出堆顶元素之后,把堆中下面的最后一个元素送到堆顶的过程可以参考下面的第一幅图。至于为什么是怎么做,为什么是把最后一个元素送到堆顶成为堆顶元素,而不是把原来堆顶元素的儿子送到堆顶呢?具体原因可参考相关书籍)。

    此时,堆的性质已经被破坏了,所以此后要调整堆。怎么调整呢?就是一般人所说的针对新的堆顶元素shiftdown,逐步下移(因为新的堆顶元素由最后一个元素而来,比较大嘛,既然是最小堆,当然大的元素就要下沉到堆的下部了)。下沉多少步呢?即如lingyun310所说的,下沉k次就足够了。

    下移k次之后,此时的堆顶元素已经是我们要找的第2小的元素。然后,取出这个第2小的元素(堆顶元素),再次把堆中的最后一个元素送到堆顶,又经过k-1次下移之后(此后下移次数逐步减少,k-2,k-3,...k=0后算法中断)....,如此重复k-1趟操作,不断取出的堆顶元素即是我们要找的最小的k个数。虽然上述算法中断后整个堆已经不是最小堆了,但是求得的k个最小元素已经满足我们题目所要求的了,就是说已经找到了最小的k个数,那么其它的咱们不管了。

    我可以再举一个形象易懂的例子。你可以想象在一个水桶中,有很多的气泡,这些气泡从上到下,总体的趋势是逐渐增大的,但却不是严格的逐次大(正好这也符合最小堆的性质)。ok,现在我们取出第一个气泡,那这个气泡一定是水桶中所有气泡中最小的,把它取出来,然后把最下面的那个大气泡(但不一定是最大的气泡)移到最上面去,此时违反了气泡从上到下总体上逐步变大的趋势,所以,要把这个大气泡往下沉,下沉到哪个位置呢?就是下沉k次。下沉k次后,最上面的气泡已经肯定是最小的气泡了,把他再次取出。然后又将最下面最后的那个气泡移至最上面,移到最上面后,再次让它逐次下沉,下沉k-1次...,如此循环往复,最终取到最小的k个气泡。

    ok,所以,上面方法所述的过程,更进一步来说,其实是第一趟调整保持第0层到第k层是最小堆,第二趟调整保持第0层到第k-1层是最小堆...,依次类推。但这个思路只是下述思路8中正规的最小堆算法(因为它最终对全部元素都进行了调整,算法结束后,整个堆还是一个最小堆)的调优,时间复杂度O(n+k^2)没有量级的提高,空间复杂度为O(N)也不会减少。

原理理解透了,那么写代码,就不难了,完整粗略代码如下(有问题烦请批评指正):


“此处是还没实现的代码君”







    在算法导论第6章有下面这样一张图,因为开始时曾一直纠结过这个问题,“取出堆顶元素之后,把堆中下面的最后一个元素送到堆顶”。因为算法导论上下面这张图给了我一个假象,从a)->b)中,让我误以为是取出堆顶元素之后,是把原来堆顶元素的儿子送到堆顶。而事实上不是这样的。因为在下面的图中,16被删除后,堆中最后一个元素1代替16成为根结点,然后1下沉(注意下图所示的过程是最大堆的堆排序过程,不再是上面的最小堆了,所以小的元素当然要下移),14上移到堆顶。所以,图中小图图b)是已经在小图a)之和被调整过的最大堆了,只是调整了logn次,非上面所述的k次。


注:后续包含大量的讨论及论述,百家争鸣。看过以后大概觉着时间复杂度降低了,但是实用性并不高,截取了一部分,后续的就不截取了。有兴趣请移步原作者的原文。只贴出实用性较好的几种结论。

 ok,接下来,咱们再着重分析下上述思路4。或许,你不会相信上述思路4的观点,但我马上将用事实来论证我的观点。这几天,我一直在想,也一直在找资料查找类似快速排序的partition过程的分治算法(即上述在编程之美上提到的第4点思路),是否能做到O(N)的论述或证明,

     然找了三天,不但在算法导论上找到了RANDOMIZED-SELECT,在平均情况下为线性期望时间O(N)的论证(请参考本文第二节),还在mark allen weiss所著的数据结构与算法分析--c语言描述一书(还得多谢朋友sheguang提醒)中,第7章第7.7.6节(本文下面的第4节末,也有关此问题的阐述也找到了在最坏情况下,为线性时间O(N)(是的,不含期望,是最坏情况下为O(N))的快速选择算法(此算法,本文文末,也有阐述),请看下述文字(括号里的中文解释为本人添加):

    Quicksort can be modified to solve the selection problem, which we have seen in chapters 1 and 6. Recall that by using a priority queue, we can find the kth largest (or smallest) element in O(n + k log n)(即上述思路7). For the special case of finding the median, this gives an O(n log n) algorithm.

    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(快速选择,即上述编程之美一书上的,思路4,步骤如下):

    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.(选取一个枢纽元v属于S)
    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)。看到了没,就是平均复杂度为O(N)这句话). The analysis is similar to quicksort's and is left as an exercise.

    The implementation of quickselect is even simpler than the abstract description might imply. The code to do this shown in Figure 7.16. When the algorithm terminates, the kth smallest element is in position k. This destroys the original ordering; if this is not desirable, then a copy must be made. 

并给出了代码示例:


“此处也是没有实现的代码君”



结论:

  1. 与快速排序相比,快速选择只做了一次递归调用而不是两次。快速选择的最坏情况和快速排序的相同,也是O(N^2),最坏情况发生在枢纽元的选取不当,以致S1,或S2中有一个序列为空。
  2. 这就好比快速排序的运行时间与划分是否对称有关,划分的好或对称,那么快速排序可达最佳的运行时间O(n*logn),划分的不好或不对称,则会有最坏的运行时间为O(N^2)。而枢纽元的选取则完全决定快速排序的partition过程是否划分对称。
  3. 快速选择也是一样,如果枢纽元的选取不当,则依然会有最坏的运行时间为O(N^2)的情况发生。那么,怎么避免这个最坏情况的发生,或者说就算是最坏情况下,亦能保证快速选择的运行时间为O(N)列?对了,关键,还是看你的枢纽元怎么选取。
  4. 像上述程序使用三数中值作为枢纽元的方法可以使得最坏情况发生的概率几乎可以忽略不计。然而,稍后,在本文第四节末,及本文文末,您将看到:通过一种更好的方法,如“五分化中项的中项”,或“中位数的中位数”等方法选取枢纽元,我们将能彻底保证在最坏情况下依然是线性O(N)的复杂度。

     至于编程之美上所述:从数组中随机选取一个数X,把数组划分为Sa和Sb俩部分,那么这个问题就转到了下文第二节RANDOMIZED-SELECT,以线性期望时间做选择,无论如何,编程之美上的解法二的复杂度为O(n*logk)都是有待商榷的。至于最坏情况下一种全新的,为O(N)的快速选择算法,直接跳转到本文第四节末,或文末部分吧)。




第二节、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]中去查找。  

    写此文的目的,在于起一个抛砖引玉的作用。希望,能引起你的重视及好的思路,直到有个彻底明白的结果。

     updated:算法导论原英文版有关于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,我们定指示器Xk,为:

Xk = I{子数组A[p ..q]恰有k个元素} ,

我们假定元素的值不同,因此有

          E[Xk]=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),我们依赖于XkT(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从上方限界得到(这里,原中文版的翻译的确是有点含糊)。利用这个归纳假设,可以得到:

(此段原中文版翻译有点问题,上述文字已经修正过来,对应的此段原英文为:We solve the recurrence by substitution. Assume thatT(n)≤cn for some constant c that satisfies the initial conditions of the recurrence. We assume thatT(n) =O(1) forn less than some constant; we shall pick this constant later. 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 from above byan for alln> 0. Using this inductive hypothesis, we have)

        

4、为了完成证明,还需要证明对足够大的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)的复杂度,但此RANDOMIZED-SELECT算法在最坏情况下有O(N^2)的复杂度。所以,我们得找出一种在最坏情况下也为线性时间的算法。稍后,在本文的第四节末,及本文文末部分,你将看到一种在最坏情况下是线性时间O(N)的复杂度的快速选择SELECT算法。



第五节、堆结构实现,处理海量数据

 

    文章,可不能这么完了,咱们还得实现一种靠谱的方案,从整个文章来看,处理这个寻找最小的k个数,最好的方案是第一节中所提到的思路3:当然,更好的办法是维护k个元素的最大堆,原理与上述第2个方案一致,即用容量为k的最大堆存储最小的k个数,此时,k1<k2<...<kmax(kmax设为大顶堆中最大元素)。遍历一次数列,n,每次遍历一个元素x,与堆顶元素比较,x<kmax,更新堆(用时logk),否则不更新堆。这样下来,总费时O(n*logk)。

    为什么?道理很简单,如果要处理的序列n比较小时,思路2(选择排序)的n*k的复杂度还能说得过去,但当n很大的时候列?同时,别忘了,如果选择思路1(快速排序),还得在数组中存储n个数。当面对海量数据处理的时候列?n还能全部存放于电脑内存中么?(或许可以,或许很难)。

    ok,相信你已经明白了我的意思,下面,给出借助堆(思路3)这个数据结构,来寻找最小的k个数的完整代码,如下:


“没有实现的代码君”

k的堆和n的堆各自一版 个人比较喜欢n的最小堆



第七节、再探Selection_algorithm,类似partition方法O(n)再次求证

网友反馈:
    stupidcat:用类似快排的partition的方法,只求2边中的一边,在O(N)时间得到第k大的元素v; 
弄完之后,vector<int> &data的前k个元素,就是最小的k个元素了。 
时间复杂度是O(N),应该是最优的算法了。并给出了代码示例:


“代码君”




以及,从此后开始,作者开启了他的论证模式,我就不贴了。。



ok,综述全文,根据选取不同的元素作为主元(或枢纽)的情况,可简单总结如下:
1、RANDOMIZED-SELECT,以序列中随机选取一个元素作为主元,可达到线性期望时间O(N)的复杂度。
    这个在本文第一节有关编程之美第2.5节关于寻找最大的k个元素(但其n*logk的复杂度是严重错误的,待勘误,应以算法导论上的为准,随机选取主元,可达线性期望时间的复杂度),及本文第二节中涉及到的算法导论上第九章第9.2节中(以线性期望时间做选择),都是以随机选取数组中任一元素作为枢纽元的。

2、SELECT,快速选择算法,以序列中“五分化中项的中项”,或“中位数的中位数”作为主元(枢纽元),则不容置疑的可保证在最坏情况下亦为O(N)的复杂度。
    这个在本文第四节末,及本文第七节,本文文末中都有所阐述,具体涉及到了算法导论一书中第九章第9.3节的最快情况线性时间的选择,及Mark Allen Weiss所著的数据结构与算法分析--c语言描述一书的第10章第10.2.3节(选择问题)中,都有所阐述。

       本文结论:至此,可以毫无保留的确定此问题之结论:运用类似快速排序的partition的快速选择SELECT算法寻找最小的k个元素能做到O(N)的复杂度。RANDOMIZED-SELECT可能会有O(N^2)的最坏的时间复杂度,但上面的SELECT算法,采用如上所述的“中位数的中位数”的取元方法,则可保证此快速选择算法在最坏情况下是线性时间O(N)的复杂度




0 0
原创粉丝点击