zoukankan      html  css  js  c++  java
  • 所有排序总结(内排序)(续)——线性时间排序

     

    一、前言

          这里稍微证明一下基于比较的排序算法的下界,采用决策树模型。下图是一个含三个元素的输入序列,采用插入排序算法的决策树。每一个结点里面包含(i,j)。左子树代表i<=j的情况,右子树代表i>j的情况。

          各位,如果有跟我一样,半天看不懂的话,我就稍微解释一下。拿(1,2,3)这个序列的得出,稍作说明。在根节点处,1和2比较(输入位置为1的元素与输入位置为2的元素比较,不是1和2比较,下同)。 1<2,进入到左子树。  2和3比较,  2<3,还是到左子树   得出排序后的序列(1,2,3)。

          有一点要说明的是:任何一个正确的排序算法,都能产生全部n!个序列。这n!个序列对应决策树中的所有叶节点。

          从这个模型明显可以得出结论:从根节点到任意一个叶节点之间最长路径的长度,也即决策树的高度(每一本书所说的高度概念不一致,这里取图中的高度为3),表示排序算法在最坏情况下的比较次数。

          利用决策树模型对基于比较的排序算法时间下界的证明:

          考虑一个高为 h,  叶节点个数为l的  决策树。  输入序列为n。  显然  n!<=l。(说明:如果不考虑n个元素有相等的情况,n!应该等于l).
    
          又,一个高度为h的完全二叉树的叶节点的数目为2h。   l <= 2h 
    
          即      n!<=l<=2h.
    
          取对数可得到   h>=lg(n!)。
    
          又, lg(n!) = Θ(nlgn);
          得到   h>=Ω(nlgn);

          说明一下这个:lg(n!) = Θ(nlgn);

         (注:好长时间没看这些,应该是从来没有认真看过==...  都没有搞清楚  Ο  Θ   Ω  这个几个符号的意思。翻了导论前面几章才算明白。   这个的证明要用到斯特灵公式。    我感觉已经超出我的能力范围之外了,不想花时间再去研究这玩意儿了 。)

          斯特林公式参看这里 http://zh.wikipedia.org/wiki/%E6%96%AF%E7%89%B9%E9%9D%88%E5%85%AC%E5%BC%8F

          简单说就是:

          f(n) = Θ(g(n)) 当且仅当f(n) =  Ω(nlgn)和f(n) = Ο(nlgn)。  这里的“=”并不是等号的意思。  f(n) = Θ(g(n)) 是 f(n) 属于 Θ(g(n)) 的意思,Θ(g(n)) 是一个集合。

          lg(n!) = Θ(nlgn);  就是说存在  c1和c2  所有的n>=n0,有 0<=c1(nlgn) <= lg(n!) <= c2(nlgn) 。

    二、三种线性时间排序

     

    1、计数排序

         计数排序就是根据元素的值统计元素的个数,然后直接把元素放到合适的位置,以达到Θ(n)的目的。思路也很简单,废话不多说,直接上代码

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 void countingSort(int *A,int n,int *B,int *C,int k)
     4 {
     5     //A为待排序的数组;
     6     //n为A数组的长度;
     7     //B为保存的结果;
     8     //C为临时存储区  长度为k
     9     //k为n个数据输入的范围; 0~~k-1
    10     for(int i=0;i<k;i++)
    11        C[i] = 0;
    12     for(int j=0;j<n;j++)
    13        C[A[j]] += 1;      //统计每个位置的个数
    14     for(int i=1;i<k;i++)
    15        C[i] += C[i-1];   //c[i]表示小于等于i的个数
    16     for(int j = n-1;j>=0;j--)//放到合适的位置,因为是从0开始  要减1
    17     {
    18         B[C[A[j]]-1] = A[j]; //c[i]表示小于等于i的个数  B[]从0开始  c[i]-1 表示在B中的位置
    19         C[A[j]] -= 1;
    20     }
    21 }
    22 int main()
    23 {
    24     int n=11;//待排序的长度
    25     int k=11;//数字范围 为 0~~k-1
    26     int a[n] ;
    27     int b[n] ;
    28     int c[k] ;
    29     for(int i=0;i<n;i++)
    30        a[i] = rand()%k ;
    31     for(int i=0;i<n;i++)
    32        printf("%d ",a[i]);
    33     printf("\n");
    34     countingSort(a,n,b,c,k);
    35     for(int i=0;i<n;i++)
    36      printf("%d ",b[i]);
    37     return 0;
    38 }

           写代码的时候,习惯从0开始了。导致第四个for循环的时候忘记减1了,纠结了好久。。。。显然可以看出上面的计数排序的Θ(n+k)。好吧,我一开始也不知道这个是怎么来的。一共四个循环,Θ(2n+2k),去掉常数就是Θ(n+k),n表示待排序数组,k表示要排序的的值的范围。这样,计数排序就很有局限了,k的值注定了这种排序的适用范围很小。

    2、桶排序

         也许看完上面的计数排序,你会发现既然C数组已经统计完了每个值的个数,那么直接扫描一遍C数组,依次序输出也就OK了。这个就算是桶排序了,这里的每个桶只能存储一个值,算作是特殊的桶排序吧。将上面的代码稍作改变就可以得到:

     1 void BucketSort(int *A,int n,int *B,int *C,int k)
     2 {
     3     //A为待排序的数组;
     4     //n为A数组的长度;
     5     //B为保存的结果;
     6     //C为临时存储区  长度为k
     7     //k为n个数据输入的范围; 0~~k-1
     8     for(int i=0;i<k;i++)
     9        C[i] = 0;
    10     for(int j=0;j<n;j++)
    11        C[A[j]] += 1; //统计每个位置的个数
    12     int j=0;
    13     for(int i=0;i<k;i++)
    14     {
    15         while(C[i]!=0)
    16         {
    17             B[j++] = i;
    18             C[i]--;
    19         }
    20     }
    21 }

          一般桶排序的操作步骤就是:

          1、确定要排序数组的取值范围,并划分成M个区间;

          2、给这个M区间划分M个桶,并对每个桶内进行排序;

          3、再依次序把这M个桶的元素串接起来。

          这里上一幅导论里面的图,它讲100划分成10个区间,设计10个桶。然后将数组里面的元素放到相应的桶里面去。

         附上我写的代码(实际是参考别人写的==!  一个链表操作写了好长时间,还被别人鄙视了无数遍。。。。。。。。。)

    #include <stdio.h>
    #include <stdlib.h>
    struct Node
    {
        int value;
        Node *next;
    };
    void BucketSort(int *array,int n,int low,int high,int interval)
    {//low和high分别代表  元素的最小值 和最大值
     //n为待排序数组长度,interval间隔
        int BucketNum = (high-low)/interval;//有多少个桶
        Node *BucketArray = new Node[BucketNum];
        Node *pre,*cur;
        int count=0;
        for(int i=0;i<10;i++)
        {
            BucketArray[i].value = 0;
            BucketArray[i].next = NULL;  //初始化。。。。
        }
        for(int i=0;i<n;i++)
        {
            Node *insert = new Node();
            insert->value = array[i];    //待插入的节点
            int num = array[i]/interval;
            if(BucketArray[num].next == NULL)
            {
                BucketArray[num].next = insert;   //每个桶的第一个节点保持空
            }
            else
            {
                pre = &BucketArray[num];
                cur = BucketArray[num].next;
                while(cur!=NULL && cur->value<=array[i])//找到合适的位置。
                {
                    cur = cur->next;
                    pre = pre->next;
                }
                insert->next = cur;
                pre->next = insert;
    
            }
        }
        for(int i=0;i<BucketNum;i++)
        {
            cur = BucketArray[i].next;
            if(cur == NULL)
                continue;
            while(cur!=NULL)
            {
                array[count++] = cur->value;
                cur = cur->next;
            }
        }
    }
    int main()
    {
        int a[10];
        for(int i=0;i<10;i++)
        {
            a[i] = rand()%100;//随机数
        }
        BucketSort(a,10,0,100,10);
        for(int i=0;i<10;i++)
        {
            printf("%d ",a[i]);
        }
        return 0;
    } 

         这段代码里面,设元素有N个,M个桶,平均每个桶里面有N/M个元素。在桶里面的,我没有采用其他的排序算法,简单的插入。因此桶里面为Θ(N/M)。总的时间复杂度为   Θ(N * N/M)=Θ(N2/M) ,可以看出来,当桶的数量越多。。。趋近于N时,时间复杂度就为Θ(N)了。

    3、基数排序

          基数排序是对多个关键字的排序。两种方式,一是从首要关键字到次要关键字,另外一种是从次要关键字到首要关键字。导论上面说的是第二种。如果是从首要关键字开始,就要对首要关键字相同的进行分堆,再根据次要关键字排序。增加开销,有点桶排序的味道了。

          这里我以技术排序为基础,从次要关键字开始,写了这个基数排序:

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <math.h>
     4 void countingSort(int *A,int n,int *C,int k,int d)
     5 {
     6     //A为待排序的数组;
     7     //n为A数组的长度;
     8     //B为保存的结果;
     9     //C为临时存储区  长度为k
    10     //k为n个数据输入的范围; 0~~k-1
    11     int B[n];
    12     int key=0;
    13     for(int i=0;i<k;i++)
    14        C[i] = 0;
    15     for(int j=0;j<n;j++)
    16     {
    17         key = (A[j]/(int)(pow(10,d)))%10;//分离位数用。。。。
    18         C[key] += 1;      //统计每个位置的个数
    19     }
    20     for(int i=1;i<k;i++)
    21        C[i] += C[i-1];   //c[i]表示小于等于i的个数
    22     for(int j = n-1;j>=0;j--)//放到合适的位置,因为是从0开始  要减1
    23     {
    24         key = (A[j]/(int)(pow(10,d)))%10;
    25         B[C[key]-1] = A[j]; //c[i]表示小于等于i的个数  B[]从0开始  c[i]-1 表示在B中的位置
    26         C[key] -= 1;
    27     }
    28     for(int i=0;i<n;i++)
    29     {
    30         A[i]=B[i];
    31     }
    32 }
    33 void radixSort(int A[],int n,int d)
    34 {
    35     int k=10;//数字范围 为 0~~k-1
    36     int C[k] ;
    37     for(int i=0;i<d;i++)
    38     {
    39         countingSort(A,n,C,k,i);
    40     }
    41 }
    42 int main()
    43 {
    44     int A[10];
    45     for(int i=0;i<10;i++)
    46        A[i] = rand()%999;
    47     for(int i=0;i<10;i++)
    48        printf("%d ",A[i]);
    49     printf("\n");
    50     radixSort(A,10,3);
    51     for(int i=0;i<10;i++)
    52      printf("%d ",A[i]);
    53     return 0;
    54 }

          有人跟我说,代码里面key对 位数的获取 ,可以用位操作,我想了想,需要BCD码,没有想到特别好的解决方案,还是这样易懂。我总觉得硬性搞成位操作反而有点麻烦了,没有多大意义。没有让人眼睛一亮的解决方案。上面的代码时间复杂度为Θ(d(n+k));

    三、总结

         显然,线性时间排序有很大的局限,对数据值范围有要求。

         另外,写这三个排序,原本以为半天多都可以搞定,结果弄了两三天。遭到了无数的鄙视。。最关键还是自己不够熟悉。。

         关于排序,暂时就到这里吧。开学了,下学期专业课压死人。每学期好多门课,但是都感觉没一门学得扎实。

         以后如果有新的认识,我会再继续写写的。我觉得还会有续的,就是不知道多久了。。。。

         持续关注数据结构算法,一来,它很重要,二来,还是有点兴趣。

    最后,推荐一篇文章   挺有启发意义的。

    http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/

      

  • 相关阅读:
    cscope
    C语言
    PMP-------框架
    shell--打开新的.sh文件,直接添加title
    工欲善其事,必先利其器
    同步异步阻塞非阻塞可中断的睡眠不可中断的睡眠
    内核--时间
    没有必要的事情,要学会适当忍让!
    嵌入式开发之CPU的那些事...
    互斥技术----原子变量和自旋锁
  • 原文地址:https://www.cnblogs.com/xibaohe/p/2648136.html
Copyright © 2011-2022 走看看