• 算法导论笔记:08线性排序


           之前的排序算法都是比较排序:在排序的最终结果中,各元素的次序依赖于他们之间的比较任何比较排序在最坏情况下,都要经历Ω(n lgn)次比较,所以,归并排序和堆排序都是渐进最优的

           除了比较排序之外,还有其他的排序方法,但是都必须满足一定的前提条件,这些排序算法的下界不再是Ω(n lgn),而可以达到线性的下界。

     

    1:决策树模型

           比较排序可以抽象成一颗决策树, 他表示在给定的输入规模的情况下,某一特定排序算法对所有元素的比较操作。其中,控制,数据移动等其他操作都已被忽略了。比如下图就是针对三个元素插入排序算法所对应的决策树。

           在决策树中,每个内部结点都以i:j进行标记,其中1  i, j  n。i,j都代表数组元素的下标,n是输入数组的大小。每个叶节点都是一个序列,也就是排序的结果。

           排序算法的执行对应于一条从根节点到叶子结点的路径。每个内部结点表示了  和 的比较。左子树表示 ,右子树表示 。对于任意的输入,正确的排序算法都可以生成正确的序列,所以,对于n个元素来说,有n!中可能的结果,也就是说,决策树的叶子节点有n!

           在决策树中,从根节点到叶节点的路径长度表示了算法的执行的比较操作的次数。所以一个排序算法的最坏情况比较次数就等于决策树的高度,所以就有这样的结论:在最坏情况下,任何比较排序算法都需要Ω(n lgn)次比较。

           证明:假设树的高度为h,决策树叶子节点数为l,对于n个元素的输入规模,l 。同时,完全二叉树叶子结点数为 所以有:n! 。所以,h  lg(n!) =Ω(n lgn)。

     

    2:计数排序

           条件:n个输入元素,每个元素都是在[0,k]区间之内的整数,其中,k为整数。

           基本思想:对于输入元素x,确定小于x的元素个数,利用该信息,就可以直接把x放到输出数组的正确位置上了,比如有17个元素小于x,则x的位置应该是18了。当输入元素有相同时,需要略作修改。

           伪代码(在该算法中,输入数组为A,输出数组为B,数组C表示了小于x的元素个数):

           COUNTING-SORT(A,B, k)

                  letC[0…k] be a new array

                  for I = 0 to k

                         C[i]= 0                     //初始化C中元素为0

                  forj =1 to A.len

                         C[A[j]]= C[A[j]] + 1   // C[i]中的元素表示,在数组A中,等于i的元素个数。

                  forI = 1 to k

                         C[i]= C[i] + C[i-1]              //C[i] 中的元素表示,在数组A中,小于等于的元素个数。

                  

                  for j = A.len downto 1     //为了维持稳定性。

                         B[C[A[j]]]= A[j]         //将A[j]放在数组B中某个位置上,该位置为C[A[j]],也就是小于等于A[j]元素个数

                         C[A[j]]= C[A[j]] -1      //为了防止A中相同元素放在同一位置上

    图示:

     

           时间复杂度:该算法总的时间复杂度为O(n+k)在实际工作中,如果k = O(n),则计数排序的时间复杂度为Θ(n)。

           备注:计数排序具有稳定性的特点,也就是说,原数组中,具有相同值的元素的相对位置,在输出数组中,相对位置不变一般情况下,稳定性对于需要排序的数据中还附带卫星数据的情况至关重要。比如计数排序通常作为基数排序的子过程,计数排序的稳定性是基数排序的关键所在。

    void countsort(int *set, int *res, int len, intboundary)

    {

           int i;

           int csize= (boundary+1) * sizeof(int);

           int *count= malloc(csize);

           memset(count,0, csize);

           for(i = 0;i <= boundary; i++)

           {

                  count[i]= 0;

           }

     

           for(i = 0;i < len; i++)

           {

                  count[set[i]]= count[set[i]] + 1;

           }

           for(i = 1;i <= boundary; i++)

           {

                  count[i]= count[i] + count[i-1];

           }

           for(i =len-1; i>=0; i--)

           {

                  res[count[set[i]]- 1] = set[i];

                  count[set[i]]--;

           }

           free(count);

    }

     

    3:基数排序

           基本思想:n个输入元素,每个元素最多是d位数,对这n个元素进行排序时,直观上可能会先比较高位,然后对相同高位元素在比较次高位。这种算法会需要保存很多临时数据。基数排序的基本思想是,从最低位开始比较,首先根据第0位对数组A进行排序,结果为 ,然后根据第1位对数组 进行排序,得到结果为 依次重复下去直到最高位。为了保证基数排序的正确性,每一位排序的算法必须具有稳定性(比如123145两个数排序,最低位是35,所以按照最低位排序的次序是123.145;第二位是24,所以第二位排序后的结果是123.145;第三位是11,如果没有稳定性,那么有可能最终的次序是145.123)

           伪代码:

                  RADIX-SORT(A,d)

                         forI = 1 to d

                                usea stable sort to sort array A on digit i

    图示:

           时间复杂度:对于nd位数,其中每一位的可能取值在[0,k]内,如果每一位的排序算法为O(n+k),则基数排序的时间复杂度为 (d(n+k))。如果d为常数,且k =O(n),则基数排序的时间复杂度为 O(n)

     

           备注:在更一般的情况下,可以将给定的元素分成若干位,比如对于b位数,可以将其当做d位数,其中d=b/r,每一位其中还有r位。对于“每一位”来说,使用计数排序的时间为O(n+ )。所以时间复杂度为 ((b/r)( n+ ))。

           这样,对于给定的n和b,如何选择r值使得最小化表达式(b/r)(n+2r)。如果b< lgn,对于任何r<=b的值,都有(n+ )=Θ(n),于是选择r=b,使基数排序的时间为Θ((b/b)(n+2b)) = Θ(n)。 如果b>lgn,则选择r=lgn,可以给出在某一常数因子内的最佳时间:当r=lgn使,算法复杂度为Θ(bn/lgn),当r增大到lgn以上时,分子 增大比分母r快,于是运行时间复杂度为Ω(bn/lgn);反之当r减小到lgn以下的时候,b/r增大,而n+ 仍然是Θ(n)。

           对于基数排序和快速排序哪个算法更好,需要取决于实际情况,虽然基数排序的时间Θ(n)看上去要比快速排序要好,但是常数项因子不同,而且基数排序每一步耗费时间都要比快速排序要高。而且基数排序不是原址排序。完整代码如下:

    void radixsort(int *set, int len, int bitlen)
    {
           int i, j;
           int size = len * sizeof(int);
           int *res = malloc(size);
           memset(res, 0, size);

           for(i = 0; i < bitlen; i++)
           {
                  countsort_bit(set, res, len, i);
                  memcpy(set, res, size);
                  memset(res, 0, size);
    }
           free(res);
    }


    void countsort_bit(int *set, int *res, int len ,int bit)
    {
           int boundary = 9;
           int size = (boundary+1) * sizeof(int);
           int *count = malloc(size);
           int i;
           int bitnum = -1;
           int temp = (int)pow(10, bit);

           memset(count, 0, size);

           for(i = 0; i < len; i++)
           {
                  bitnum = (set[i] / temp) % 10;//get the bit num of set[i]
                  count[bitnum] = count[bitnum] + 1;
           }

           for(i = 1; i <= boundary; i++)
           {
                  count[i] = count[i] + count[i-1];
           }

           for(i = len-1; i >= 0; i--)
           {
                  bitnum = (set[i] / temp) % 10;
                  res[count[bitnum] - 1] = set[i];
                  count[bitnum]--;
    }

           free(count);
    }

     

    4:桶排序

           条件:输入是由一个随机过程产生的,将n个元素均匀,独立的分布在在[0,1)区间上。

           基本思想:将n个数均匀的放在n个桶中,因为输入是均匀的,所以一般不会出现很多数落在一个桶中的情况,为了得到输出结果,先对每个桶中的数进行排序,然后遍历每个桶即可,桶一般用链表来实现。

           伪代码:

           BUCKET-SORT(A)

                  n =A.len

                  letB[0..n-1] be a new array

                  forI = 0 to n-1

                         makeB[i] an empty list

                  forI = 1 to n

                         insert A[i] into list B[n A[i]]

                  forI = 0 to n-1

                         sortlist B[i] with insertion sort

                  concatenatethe lists B[0],B[1],…,B[n-1] together in order

    图示:

           时间复杂度:桶排序的时间复杂度也是Θ(n)。完整代码如下:

    typedef struct Node
    {
           double num;
           struct Node *next;
    }node;


    void bucketsort(double *set, int len)
    {
           node ** bset = NULL;
           node *p = NULL, *q = NULL;

           int size = len * sizeof(node *);
           int i, j;

           node *e = NULL;

           bset = malloc(size);
           memset(bset, 0, size);

           for(i = 0; i < len; i++)
           {
                  e = malloc(sizeof(node));
                  e->num = set[i];
                  e->next = NULL;

                  p = bset[(int)(set[i] * len)];
                  if(p == NULL)
                  {
                         bset[(int)(set[i] * len)] = e;
                  }
                  else
                  {
                         q = p;
                         while(p != NULL)
                         {
                                if(p->num > e->num)
                                {
                                       break;
                                }
                                q = p;
                                p = p->next;
                         }

                         if(p == bset[(int)(set[i] * len)])
                         {
                                bset[(int)(set[i] * len)] = e;
                                e->next = p;
                         }
                         else
                         {
                                q->next = e;
                                e->next = p;
                         }
                  }
           }

           i = 0;
           j = 0;
           while(i < len)
           {
                  p = bset[i];
                  while(p != NULL)
                  {
                         set[j++] = p->num;
                         p = p->next;
                         free(p);
                  }
                  i++;
           }

           free(bset);
    }

     

           5:选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。归基帽)

     

  • 相关阅读:
    Shiro框架:Failed to deserialize java.lang.Exception: Failed to deserialize问题解决
    Linux系统:CentOS防火墙的各种命令使用
    Linux系统:CentOS下vsftpd的安装配置
    Vue项目启动报错:UnhandledPromiseRejectionWarning: TypeError: loaderContext.getResolve is not a function问题解决
    Minio存储桶:部署存储服务以及设置永久下载链接
    Redis之Bitmaps
    Redis事务与Lua
    Redis Pipeline
    Redis Shell详解
    Redis慢查询
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247239.html
Copyright © 2020-2023  润新知