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

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

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

 

1:决策树模型

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

算法导论笔记:08线性排序_第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中相同元素放在同一位置上

图示:

算法导论笔记:08线性排序_第2张图片

 

       时间复杂度:该算法总的时间复杂度为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

图示:

算法导论笔记:08线性排序_第3张图片

       时间复杂度:对于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

图示:

算法导论笔记:08线性排序_第4张图片

       时间复杂度:桶排序的时间复杂度也是Θ(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:选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。归基帽)

 

 

转载于:https://www.cnblogs.com/gqtcgq/p/7247239.html

你可能感兴趣的:(算法导论笔记:08线性排序)