算法导论(八)--线性时间排序

来源:互联网 发布:苹果电脑软件如何更新 编辑:程序博客网 时间:2024/05/21 12:40

前面介绍过的排序算法都是通过比较各个元素来确定最后的排列顺序,这类都属于比较排序算法。比较排序可以被抽象地视为决策树。(书上说它是一棵满二叉树,但它明显不是)。比如含有对三个元素排序的决策树如下:

可以看到,根据不同的比较结果,每个叶结点都是n个元素的一个排列。设比较排序的决策树高为h,可达叶结点数为l。因为n个元素共有n!种排列,而树的叶子数至多为2^h,所以n! ≤ l ≤ 2^h
对上式取对数,则有h ≥ lg(n!) = Ω(n*lg(n)) (根据斯特林近式公式可以推出 lg(n!)=θ(n*lg(n)),理解不了就强记好了)。

综上所述,任意一个比较排序算法的运行时间为Ω(n*lg(n)),所以运行时间为O(n*lg(n))的堆排序和合并排序都是渐近最优的比较排序算法


计数排序

不同于比较排序,计数排序不需要通过比较来确定元素的位置。当n个输入的每一元素都为介于0~k的整数,此处k为整数,可以使用计数排序。

简单来说,计数排序记录在n个输入中,值为0, 1, ..., k分别有多少个,之后根据记录好的个数来决定各元素应该摆放的位置。
下面是伪代码,A为输入的数组,B为排好序的数组,k为输入数组中元素可能的最大值,C数组用来计数:


  1. COUNTING-SORT(A, B, k) {
  2. 1    for i = 0 to k
  3. 2       C[i] = 0;
  4. 3    for j = 1 to A.length
  5. 4       C[A[j]] += 1;
  6. // 此时C[i]为值等于i的元素的个数
  7. 5    for i = 1 to k
  8. 6       C[i] += C[i-1];
  9. // 此时C[i]为值小于等于i的元素的个数
  10. 7    for j = A.length downto 1 {
  11. 8       B[C[A[j]]] = A[j];
  12. 9       C[A[j]] -= 1;
  13. 10    }
  14. }

下面是某个具体的计数排序的过程:

代码第1~2行运行时间为θ(k),第3~4行运行时间为θ(n),第5~6行运行时间为θ(k),第7~10行运行时间为θ(n),所以计数排序总的运行时间为θ(n+k)。当k=O(n)时,它的运行时间为θ(n)。

计数排序是稳定的值相同的元素的相对次序在排序后保持不变。这归功于代码第7行中A数组中的元素从后开始复制到B中,否则相对次序就被打乱了。


基数排序

如果待排序的每个元素都为一个d位数,每个数位可以取k种可能的值,(比如十进制里的246是一个3位数,每个数位可以取0~9共10种可能的值。)第1位为最低位,而第d位为最高位,我们便可以依次对每一个位进行排序,当所有的位数都排好序时后,所有的元素也都排好序了。下面演示一个对三位十进制数的排序过程:

注意到每次排序的结果必须是稳定的,否则会出错。比如上例中,在排好第二位后,329在355的前面,之后在对最高位排序时,必须保持这个次序。利用不稳定的排序算法对最高位排序时,329可能会排到355的后面,因为最高位是相等的,都是3。

所以基数排序的伪代码为:


  1. RADIX_SORT(A, d) {
  2. 1    for i = 1 to d
  3. 2       use a stable sort to sort array A on digit i
  4. }

前面我们已经介绍过计数排序,它是稳定的,所以我们可以用它来实现基数排序。这样上面代码中每次迭代的代价为Ө(n+k),基数排序总的运行时间可达到Ө(d*(n+k)),其中d为元素的位素,k为每位可取的值。当d为常数,k为O(n)时,基数排序有线性时间。

接下来,我们针对计算机特例,把所有的数都视为二进制数。若待排序的每个元素都为b位(二进制)数,我们把它分成大小为r个二进制位的各个部份,这样共用b/r个部分,每部分的取值范围为2^r-1。(比如一个32位数,分成4部份,每部份都有8位,取值范围为0~2^8-1。)这样,我们便可以使用基数排序的方法,对每部份分别排序。前面分析基数排序的运行时间为Ө(d*(n+k)),这里d=b/r,k=2^r-1,所以总的运行时间为Ө((b/r)*(n+2^r))

如果b<floor(lg(n)),Ө(n+2^r)=Ө(n)。这时r取值为b时,总的运行时间渐近最优,为Ө(n)。
如果b≥floor(lg(n)),则r取值floor(lg(n))时可以得到总的运行时间Ө(b*n/lg(n)),这是该情况下的最佳运行时间。因为当r取比floor(lg(n))更大的值时,2^r会比分母中的r增长的快,总的运行时间为Ω(b*n/lg(n));而当r取比floor(lg(n))更小的值时,b/r这项就会超过b/lg(n),而(n+2^r)仍然保持Ө(n),所以总运行时间也为Ω(b*n/lg(n))。

基数排序是否比基于比较的排序(如快速排序)更好呢?看上去基数排序在b=O(lg(n)),r≈lg(n)时运行时间为Ө(n),渐近优于快速排序的平均情况Ө(n*lg(n)),但是基数排序的Ө(n)里隐含的常数因子很大,所以基数排序并不一定快于快速排序。而且基数排序所使用的计数排序不是原地排序,所以需要额外的内存


桶排序

A为待排序的数组,所有的元素都均匀而独立地落在[0,1)的区间里。一个辅助数组B,分成与A中元素个数相同的区间,每个区间都称为一个,它是一个链表。我们将A中的元素A[i]放入到B[floor(n*A[i])]里,比如A中共10个元素,值为0.1的元素就放到B[floor(10*0.1)],即B[1]里的桶里。可以看到,B中每个桶里的元素都要大于其前一个桶里的元素,比如B[6]里的元素都要比B[5]里的元素大。如果我们再对每个桶里的链表里的元素进行排序的话,我们便可以在B中得到一个有序数列。

下面是桶排序的伪代码:


  1. BUCKET_SORT(A) {
  2. 1    n = A.length;
  3. 2    for i = 1 to n
  4. 3       insert A[i] into list B[floor(n*A[i])];
  5. 4    for i = 0 to n-1;
  6. 5       sort list B[i] with insertion sort;
  7. 6    concatenate the lists B[0], B[1], ..., B[n-1] together in order
  8. }


从上面代码里可以看到第2~3行最坏情况下(所有的元素都放入同一个桶里)的运行时间为Ө(n),第6行的运行时间也为Ө(n)。设n_i为表示桶B[i]里元素个数的随机变量,而对链表进行插入排序的运行时间为Ө(n_i^2),总的运行时间为:
T(n) = Ө(n) + ∑<i=0→n-1>O(n_i^2)

对两边取期望,得:
E(Tn) = E[Ө(n) + ∑<i=0→n-1>O(n_i^2)]
    = Ө(n) + ∑<i=0→n-1>E[O(n_i^2)]
    = Ө(n) + ∑<i=0→n-1>O(E[n_i^2])

只要我们能求出E[n_i^2],上式便能得解。为此,我们定义指示器随机变量
X_i_j = I{A[j]落在桶i中}
于是有n_i = ∑<j=1→n>X_i_j,于是有:
E[n_i^2] = E[(∑<j=1→n>X_i_j)^2] = E[∑<j=1→n>∑<k=1→n>X_i_j * X_i_k]
= E[∑<j=1→n>X_i_j^2 + ∑<1≤j≤n>∑<1≤k≤n,k≠j>X_i_j * X_i_k] (将j=k和j≠k的情况分开)
= ∑<j=1→n>E(X_i_j^2) + ∑<1≤j≤n>∑<1≤k≤n,k≠j>E[X_i_j * X_i_k]

X_i_j为1的概率为1/n,于是有:
E[X_i_j^2] = 1^2 * 1/n = 1/n

当k≠j时,变量X_i_j和X_i_k是独立的,所以有:
E[X_i_j * X_i_k] = E[X_i_j] * E[X_i_k] = 1/n * 1/n = 1/n^2

把上面两式代入求n_i^2期望的式子里得:
E[n_i^2] = ∑<j=1→n>E(X_i_j^2) + ∑<1≤j≤n>∑<1≤k≤n,k≠j>E[X_i_j * X_i_k]
    = n * 1/n + n*(n-1)*1/n^2 = 1 + (n-1)/n = 2 - 1/n

所以对桶内的所有链表的排序在平均情况下运行时间为Ө(2 - 1/n),而桶排序的总运行时间为Ө(n) + Ө(2 - 1/n) = Ө(n)

<script>window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"0","bdSize":"16"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];</script>
阅读(843) | 评论(0) | 转发(0) |
0

上一篇:linux 多点触控协议

下一篇:u-boot启动完全分析

相关热门文章
  • MySQL新特性之mysql_config_ed...
  • 欢迎时间神偷在ChinaUnix博客...
  • cocos2d-x简述与HelloWorld程...
  • 欢迎时间使者在ChinaUnix博客...
  • 关于php的slow.log无法生效的...
  • A sample .exrc file for vi e...
  • IBM System p5 服务器 HACMP ...
  • 游标的特征
  • busybox的httpd使用CGI脚本(Bu...
  • Solaris PowerTOP 1.0 发布
给主人留下些什么吧!~~
原创粉丝点击