本章介绍了快速排序及其算法分析,快速排序采用的是分治算法思想,对包含n个数的输入数组,最坏情况下运行时间为θ(n^2),但是平均性能相当好,期望的运行时间为θ(nlgn)。另外快速排序能够就地排序(我理解是不需要引入额外的辅助空间,每次划分能确定一个元素的具体位置),在虚拟环境中能很好的工作。
1、快速排序的描述
快速排序算法采用的分治算法,因此对一个子数组A[p…r]进行快速排序的三个步骤为:
(1)分解:数组A[p...r]被划分为两个(可能为空)子数组A[p...q-1]和A[q+1...r],给定一个枢轴,使得A[p...q-1]中的每个元素小于等于A[q],A[q+1...r]中的每个元素大于等于A[q],q下标是在划分过程中计算得出的。
(2)解决:通过递归调用快速排序,对子数组A[p...q-1]和A[q+1...r]进行排序。
(3)合并:因为两个子数组是就地排序,不需要合并操作,整个数组A[p…r]排序完成。
快速排序关键过程是对数组进行划分,划分过程需要选择一个主元素(pivot element)作为参照,围绕着这个主元素进划分子数组。举个列说明如何划分数组,现有子数组A={24,15,27,5,43,87,34},以最后一个元素为主元素进行划分,划分过程如图所示:
书中给出了划分过程的伪代码:
1 PARTITION(A,p,r)
2 x = A[r] //将最后一个元素作为主元素
3 i = p-1
4 for j=p to r-1 //从第一个元素开始到倒数第二个元素结束,比较确定主元的位置
5 do if A[j] <= x
6 i = i+1
7 exchange A[i] <-> A[j]
8 exchange A[i+1]<->A[r] //最终确定主元的位置
9 return i+1 //返回主元的位置
根据划分过程的为代码,书中又给出了快速排序的为代码:
1 QUICKSORT(A,p,r)
2 if p<r
3 q = PARTITION(A,p,r) //确定划分位置
4 QUICKSORT(A,p,q-1) //子数组A[p...q-1]
5 QUICKSORT(Q,q+1,r) //子数组A[q+1...r]
采用C元素实现一个完成的快速排序程序,程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 size_t partition(int* datas,int beg,int last);
5 void quick_sort(int* datas,int beg,int last);
6 void swap(int *a,int *b);
7
8 int main()
9 {
10 size_t i;
11 int datas[10] = {78,13,9,23,45,14,35,65,56,79};
12 printf("After quick sort,the datas is:\\n");
13 quick_sort(datas,0,9);
14 for(i=0;i<10;++i)
15 printf("%d ",datas[i]);
16 exit(0);
17 }
18
19 void swap(int *a,int *b)
20 {
21 int temp = *a;
22 *a = *b;
23 *b = temp;
24 }
25
26 size_t partition(int* datas,int beg,int last)
27 {
28 int pivot = datas[last];
29 int i,j;
30 i = beg -1;
31 for(j=beg;j<last;j++)
32 {
33 if(datas[j] < pivot)
34 {
35 i = i+1;
36 swap(datas+i,datas+j);
37 }
38 }
39 swap(datas+i+1,datas+last);
40 return (i+1);
41 }
42 void quick_sort(int* datas,int beg,int last)
43 {
44 int pivot;
45 if(beg < last)
46 {
47 pivot = partition(datas,beg,last);
48 quick_sort(datas,beg,pivot-1);
49 quick_sort(datas,pivot+1,last);
50 }
51
52 }
程序测试结果如下:
可以将划分过程之间嵌入到快速排序过程中,C语言实现如下所示:
1 void quicksort(int *datas,int length)
2 {
3 int pivot ,i,j;
4 if(length > 1)
5 {
6 pivot = datas[length-1];
7 for(i=0,j=0;j<length-1;j++)
8 {
9 if(datas[j] < pivot)
10 {
11 swap(datas+i,datas+j);
12 i = i+1;
13 }
14 }
15 swap(datas+i,datas+length-1);
16 quicksort(datas,i);
17 quicksort(datas+i,length-i);
18 }
19 }
2、快速算法排序的性能
最快情况划分:当划分过程中产生的两个区域分别包含n-1个元素和1个元素的时候(即将待排序的数是逆序的),这样第个调用过程中每次划分都是不对称的。算法时间递归的表示为:T(n)=T(n-1)+T(o)+θ(n)
= T(n-1)+θ(n) = θ(n^2)。例如下面的情况:
最佳情况划分:每次划分达到两个子问题的大小不可能都打大于n/2,当划分后一个两个子问题的大小分别为n/2(下取整)和n/2(上取整数)-1时候,快速排序时间最佳,这时候运行时间递归式为:T(n)<=2 T(n/2)+θ(n) = O(nlgn)。例如下面的情况:
3、快速排序的随机化版本
前面快速排序在划分的时候总是以最后一个元素作为主元进行划分的,此时可以改用随机获取一个主元素,获得较好的评价功能。可以调用随机函数获取随机的主元素,然后进行划分。书中给出了为代码如下:
1 RANDOMIZED-PARTITION(A,p,r)
2 i = RANDOM(p,r)
3 exchange A[r] <->A[j]
4 return PARTITION(A,p,r)
摘要:
本章先回顾了前面介绍的合并排序、堆排序和快速排序的特点及运行运行时间。合并排序和堆排序在最坏情况下达到O(nlgn),而快速排序最坏情况下达到O(n^2),平均情况下达到O(nlgn),因此合并排序和堆排序是渐进最优的。这些排序在执行过程中各元素的次序基于输入元素间的比较,称这种算法为比较排序。接下来介绍了用决策树的概念及如何用决策树确定排序算法时间的下界,最后讨论三种线性时间运行的算法:计数排序、基数排序和桶排序。这些算法在执行过程中不需要比较元素来确定排序的顺序,这些算法都是稳定的。
1、决策树模型
在比较排序算法中,用比较操作来确定输入序列<a1,a2,......,a3>的元素间次序。决策树是一棵完全二叉树,比较排序可以被抽象视为决策树,表示某排序算法作用域给定输入所做的比较。在决策树中,节点表示为i:j,其中1≤i,j≤n,n是待排序元素个数,叶子节点是排序的结果。节点的左子树满足ai≤aj,右子树满足ai>aj。排序算法正确工作的必要条件是:n个元素的n!中排列中的每一种都要作为决策树的一个叶子而出现。举例说明,先有序列A<3,2,1>,对其进行有小到达进行插入排序,排序的决策树如下图所示:
在决策树中,从跟到任意一个可达叶子节点之间最长路径的长度,表示对应的排序算法中最坏情况下的比较次数。
定理:对于一个比较排序算法在最坏情况下,都需要做Ω(nlgn)此比较。
推论:堆排序和合并排序都是渐进最优的比较排序算法。
2、计数排序
计数排序假设n个输入元素中的每一个都介于0和k之间的整数,k为n个数中最大的元素。当k=O(n)时,计数排序的运行时间为θ(n)。计数排序的基本思想是:对n个输入元素中每一个元素x,统计出小于等于x的元素个数,根据x的个数可以确定x在输出数组中的最终位置。此过程需要引入两个辅助存放空间,存放结果的B[1...n],用于确定每个元素个数的数组C[0...k]。算法的具体步骤如下:
(1)根据输入数组A中元素的值确定k的值,并初始化C[1....k]= 0;
(2)遍历输入数组A中的元素,确定每个元素的出现的次数,并将A中第i个元素出现的次数存放在C[A[i]]中,然后C[i]=C[i]+C[i-1],在C中确定A中每个元素前面有多个元素;
(3)逆序遍历数组A中的元素,在C中查找A中出现的次数,并结果数组B中确定其位置,然后将其在C中对应的次数减少1。
举个例子说明其过程,假设输入数组A=<2,5,3,0,2,3,0,3>,计数排序过程如下:
数中给出了计数排序的伪代码:
1 COUNTING_SORT(A,B,k)
2 for i=0 to k
3 do C[i] = 0
4 for j=1 to length(A)
5 do C[A[j]] = C[A[j]]+1 //C[i]中包含等于元素i的个数
6 for i=1 to k
7 do C[i] = C[i] + C[i-1] //C[i]中包含小于等于元素i的个数
8 for j=length[A] downto 1
9 do B[C[A[j]]] = A[j]
10 C[A[j]] = C[A[j]] -1
问题:在COUNTING_SORT过程中,第8行for循环为什么是 for j=length[A] downto 1,而不是 for j=1 to length[A]。
解答:虽然从改为 for j=1 to length[A]该算法仍然能够正常地工作,但是此时不能保证算法是稳定的。因为如果有两个元素相同,那么就导致排序后前面的出现在后面,后面的出现在前面,即相同值的元素在输出数组中的相对次序与它们在输入数组中的次序是不同的。而从for j=length[A] downto 1可以保证是稳定的算法。
为了更好的理解计数排序,我写了一个完整了C语言程序:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int max(int *A,size_t length);
6 void count_sort(int *A,int *B,size_t length,int k);
7
8 int main()
9 {
10 int datas[10] = {2,5,3,0,2,3,0,3,4,6};
11 int ret[10];
12 int i,k;
13 k = max(datas,10);
14 printf("max value k = %d\\n",k);
15 count_sort(datas,ret,10,k);
16 printf("After counting sort,the result is:\\n");
17 for(i=0;i<10;i++)
18 printf("%d ",ret[i]);
19 exit(0);
20 }
21
22 int max(int *A,size_t length)
23 {
24 int k = A[0];
25 int i;
26 for(i=1;i<length;++i)
27 if(A[i] > k)
28 k = A[i];
29 return k;
30 }
31 void count_sort(int *A,int *B,size_t length,int k)
32 {
33 int i,j;
34 //each element in A between 0 and k [0,k],total k+1 elements
35 int *C = (int*)malloc(sizeof(int)*(k+1));
36 //init each element in C equal zero
37 memset(C,0,(k+1)*sizeof(int));
38 //times of each element in A
39 for(i=0;i<length;++i)
40 C[A[i]] = C[A[i]] + 1;
41 //nubmers of element which less and equal than sepcific position element in A
42 for(j=1;j<=k;++j)
43 C[j] = C[j] + C[j-1];
44 //Index from zero to length-1 in A and B
45 for(i=length-1;i>=0;i--)
46 {
47 B[C[A[i]]-1] = A[i];
48 C[A[i]] = C[A[i]] - 1;
49 }
50 free(C);
51 }
程序测试结果如下:
从计数排序的思想及过程可以看出,当输入数组A中的数较大的时候,就不适合。因为需要开辟最大元个辅助数组,统计每个元素的出现次数。通常计数排序用在基数排序中,作为一个子程序。计数排序最重要的性质就是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的次序相同。
3、基数排序
基数排序排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序,它的时间复杂度可达到线性阶:O(n)。对于十进制数来说,每一位的在[0,9]之中,d位的数,则有d列。基数排序首先按低位有效数字进行排序,然后逐次向上一位进行排序,直到最高位排序结束。举例说明基数排序过程,如下图所示:
基数排序算法很直观,假设长度为n的数组A中,每个元素都有d位数字,其中第1位是最低位,第d位是最高位。书中给出了伪代码如下所示:
1 RADIX_SORT(A,d)
2 for i=1 to d
3 do usage a stable sort to sort array A on digit i
引理:给定n个d位数,每一个数位可以取k种可能值。如果所用的稳定排序需要θ(n+k)的时间,基数排序算法性能以θ(d(n+k))的时间正确对这些数进行排序。
为了完整的理解基数排序,结合上面的计数排序,采用C语言实现一个程序,运用计数排序算法对一组3位数进行排序,程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 void radix_sort(int *datas,size_t length,size_t digits);
6 int max(int *A,size_t length);
7 size_t get_digit_number(int data);
8 void count_sort(int *A,int *B,size_t length,int k);
9 size_t get_digit(int data,size_t d);
10
11 int main()
12 {
13 int i;
14 int datas[10] = {432,578,256,782,691,206,942,387,696,374};
15 int k = max(datas,10);
16 int d = get_digit_number(k);
17 radix_sort(datas,10,d);
18 printf("After radix sort the result is:\\n");
19 for(i=0;i<10;i++)
20 printf("%d ",datas[i]);
21 exit(0);
22 }
23
24 void radix_sort(int *datas,size_t length,size_t digits)
25 {
26 int i,j,k;
27
28 int *temps = (int*)malloc(sizeof(int)*10);
29 int *tmpd = (int*)malloc(sizeof(int)*length);
30 int *rets = (int*)malloc(sizeof(int)*length);
31 for(i=0;i<digits;i++)
32 {
33 memset(temps,0,10*sizeof(int));
34 memset(tmpd,-1,10*sizeof(int));
35 memset(rets,-1,10*sizeof(int));
36 for(j = 0;j<length;j++)
37 tmpd[j] = get_digit(datas[j],i);
38 for(j=0;j<length;j++)
39 temps[tmpd[j]] = temps[tmpd[j]] +1;
40 for(k=1;k<10;k++)
41 temps[k] = temps[k] + temps[k-1];
42 for(j=length-1;j>=0;j--)
43 {
44 rets[temps[tmpd[j]]-1] = datas[j];
45 temps[tmpd[j]] = temps[tmpd[j]] -1;
46 }
47 memcpy(datas,rets,sizeof(int)*length);
48 }
49 free(temps);
50 free(tmpd);
51 free(rets);
52 }
53
54 int max(int *datas,size_t length)
55 {
56 int k = datas[0];
57 int i;
58 for(i=1;i<length;++i)
59 if(datas[i] > k)
60 k = datas[i];
61 return k;
62 }
63
64 size_t get_digit(int data,size_t d)
65 {
66 int tmp;
67 tmp = data;
68 while(d)
69 {
70 tmp /= 10;
71 d--;
72 }
73 return (tmp%10);
74 }
75
76 size_t get_digit_number(int data)
77 {
78 int d = 0;
79 while(data)
80 {
81 d = d+1;
82 data = data / 10;
83 }
84 return d;
85 }
程序测试结果如下所示:
4、桶排序
计数排序假设输入是由一个小范围内的整数构成,而桶排序则假设输入由一个随机过程产生的,该过程将元素均匀而独立地分布在区间[0,1)上。当桶排序的输入符合均匀分布时,即可以线性期望时间运行。桶排序的思想是:把区间[0,1)划分成n个相同大小的子区间,成为桶(bucket),然后将n个输入数分布到各个桶中去,对各个桶中的数进行排序,然后按照次序把各个桶中的元素列出来即可。
数中给出了桶排序的伪代码,假设输入是一个含有n个元素的数组A,且每个元素满足0≤A[i]<1,另外需要一个辅助数组B[0....n-1]来存放链表(桶)。伪代码如下所示:
1 BUCKET_SORT(A)
2 n = length(A)
3 for i= 1 to n
4 do insert A[i] into list B
5 for i=0 to n-1
6 do sort list B[i] with insertion sort
7 concatenate the list B[0]、B[1],,,B[n-1] together in order
举个来说明桶排序的过程,假设现在有A={0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68},桶排序如下所示:
为了更好的理解桶排序,采用C++语言,借助STL中的list进行操作,完整程序如下:
1 #include <iostream>
2 #include <vector>
3 #include <list>
4 #include <cstdlib>
5 using namespace std;
6
7 void bucket_sort(float *datas,size_t length)
8 {
9 int i,j;
10 int index;
11 float fvalue;
12 size_t lsize;
13 list<float> *retlist = new list<float>[length];
14 list<float>::iterator iter;
15 list<float>::iterator prioiter,enditer;
16
17 for(i=0;i<length;++i)
18 {
19 index = static_cast<int>(datas[i]*10);
20 //insert a new element
21 retlist[index].push_back(datas[i]);
22 lsize = retlist[index].size();
23 if(lsize > 1)
24 {
25 //get the last element in the list[index]
26 iter = --retlist[index].end();
27 fvalue = *iter;
28 enditer = --retlist[index].begin();
29 //insert the last element in right position
30 while(iter != enditer)
31 {
32 //get the second last element in the list[index]
33 prioiter = --iter;
34 //back up iter to the last element in the list[index]
35 iter++;
36 //compare two float values
37 if(*(prioiter) - *iter > 0.000001)
38 {
39 float temp = *(prioiter);
40 *(prioiter) = *iter;
41 *iter = temp;
42 }
43 iter--;
44 }
45 //the right inserted position
46 *(++iter) = fvalue;
47 }
48 }
49 //copy the result to datas
50 j=0;
51 for(int i=0;i<length;i++)
52 {
53 for(iter = retlist[i].begin();iter!=retlist[i].end();++iter)
54 datas[j++] = *iter;
55 }
56 delete [] retlist;
57 }
58
59 int main()
60 {
61 float datas[10] = {0.78f,0.17f,0.39f,0.76f,0.23f,0.67f,0.48f,0.58f,0.92f,0.12f};
62 bucket_sort(datas,10);
63 cout<<"After bucket_sort the result is:"<<endl;
64 for(int i=0;i<10;i++)
65 cout<<datas[i]<<" ";
66 cout<<endl;
67 exit(0);
68 }
程序测试结果如下:
桶排序的期望运行时间为:θ(n)+n*O(2-1/n) = θ(n)。