算法导论笔记(三)
第七章:快速排序
本章介绍了快速排序及其算法分析,快速排序采用的是分治算法思想,对包含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},以最后一个元素为主元素进行划分,划分过程如图所示:
书中给出了划分过程的伪代码:
PARTITION(A,p,r) x = A[r] //将最后一个元素作为主元素 i = p-1 for j=p to r-1 //从第一个元素开始到倒数第二个元素结束,比较确定主元的位置 do if A[j] <= x i = i+1 exchange A[i] <-> A[j] exchange A[i+1]<->A[r] //最终确定主元的位置 return i+1 //返回主元的位置
根据划分过程的为代码,书中又给出了快速排序的为代码:
QUICKSORT(A,p,r) if p<r q = PARTITION(A,p,r) //确定划分位置 QUICKSORT(A,p,q-1) //子数组A[p...q-1] QUICKSORT(Q,q+1,r) //子数组A[q+1...r]
采用C元素实现一个完成的快速排序程序,程序如下:
#include <stdio.h> #include <stdlib.h> size_t partition(int* datas,int beg,int last); void quick_sort(int* datas,int beg,int last); void swap(int *a,int *b); int main() { size_t i; int datas[10] = {78,13,9,23,45,14,35,65,56,79}; printf("After quick sort,the datas is:\n"); quick_sort(datas,0,9); for(i=0;i<10;++i) printf("%d ",datas[i]); exit(0); } void swap(int *a,int *b) { int temp = *a; *a = *b; *b = temp; } size_t partition(int* datas,int beg,int last) { int pivot = datas[last]; int i,j; i = beg -1; for(j=beg;j<last;j++) { if(datas[j] < pivot) { i = i+1; swap(datas+i,datas+j); } } swap(datas+i+1,datas+last); return (i+1); } void quick_sort(int* datas,int beg,int last) { int pivot; if(beg < last) { pivot = partition(datas,beg,last); quick_sort(datas,beg,pivot-1); quick_sort(datas,pivot+1,last); } }
程序测试结果如下:
可以将划分过程之间嵌入到快速排序过程中,C语言实现如下所示:
void quicksort(int *datas,int length) { int pivot ,i,j; if(length > 1) { pivot = datas[length-1]; for(i=0,j=0;j<length-1;j++) { if(datas[j] < pivot) { swap(datas+i,datas+j); i = i+1; } } swap(datas+i,datas+length-1); quicksort(datas,i); quicksort(datas+i,length-i); } }
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、快速排序的随机化版本
前面快速排序在划分的时候总是以最后一个元素作为主元进行划分的,此时可以改用随机获取一个主元素,获得较好的评价功能。可以调用随机函数获取随机的主元素,然后进行划分。书中给出了为代码如下:
RANDOMIZED-PARTITION(A,p,r) i = RANDOM(p,r) exchange A[r] <->A[j] 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>,计数排序过程如下:
数中给出了计数排序的伪代码:
COUNTING_SORT(A,B,k) for i=0 to k do C[i] = 0 for j=1 to length(A) do C[A[j]] = C[A[j]]+1 //C[i]中包含等于元素i的个数 for i=1 to k do C[i] = C[i] + C[i-1] //C[i]中包含小于等于元素i的个数 for j=length[A] downto 1 do B[C[A[j]]] = A[j] 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语言程序:
#include <stdio.h> #include <stdlib.h> #include <string.h> int max(int *A,size_t length); void count_sort(int *A,int *B,size_t length,int k); int main() { int datas[10] = {2,5,3,0,2,3,0,3,4,6}; int ret[10]; int i,k; k = max(datas,10); printf("max value k = %d\n",k); count_sort(datas,ret,10,k); printf("After counting sort,the result is:\n"); for(i=0;i<10;i++) printf("%d ",ret[i]); exit(0); } int max(int *A,size_t length) { int k = A[0]; int i; for(i=1;i<length;++i) if(A[i] > k) k = A[i]; return k; } void count_sort(int *A,int *B,size_t length,int k) { int i,j; //each element in A between 0 and k [0,k],total k+1 elements int *C = (int*)malloc(sizeof(int)*(k+1)); //init each element in C equal zero memset(C,0,(k+1)*sizeof(int)); //times of each element in A for(i=0;i<length;++i) C[A[i]] = C[A[i]] + 1; //nubmers of element which less and equal than sepcific position element in A for(j=1;j<=k;++j) C[j] = C[j] + C[j-1]; //Index from zero to length-1 in A and B for(i=length-1;i>=0;i--) { B[C[A[i]]-1] = A[i]; C[A[i]] = C[A[i]] - 1; } free(C); }
程序测试结果如下:
从计数排序的思想及过程可以看出,当输入数组A中的数较大的时候,就不适合。因为需要开辟最大元个辅助数组,统计每个元素的出现次数。通常计数排序用在基数排序中,作为一个子程序。计数排序最重要的性质就是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的次序相同。
3、基数排序
基数排序排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序,它的时间复杂度可达到线性阶:O(n)。对于十进制数来说,每一位的在[0,9]之中,d位的数,则有d列。基数排序首先按低位有效数字进行排序,然后逐次向上一位进行排序,直到最高位排序结束。举例说明基数排序过程,如下图所示:
基数排序算法很直观,假设长度为n的数组A中,每个元素都有d位数字,其中第1位是最低位,第d位是最高位。书中给出了伪代码如下所示:
RADIX_SORT(A,d) for i=1 to d do usage a stable sort to sort array A on digit i
引理:给定n个d位数,每一个数位可以取k种可能值。如果所用的稳定排序需要θ(n+k)的时间,基数排序算法性能以θ(d(n+k))的时间正确对这些数进行排序。
为了完整的理解基数排序,结合上面的计数排序,采用C语言实现一个程序,运用计数排序算法对一组3位数进行排序,程序如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> void radix_sort(int *datas,size_t length,size_t digits); int max(int *A,size_t length); size_t get_digit_number(int data); void count_sort(int *A,int *B,size_t length,int k); size_t get_digit(int data,size_t d); int main() { int i; int datas[10] = {432,578,256,782,691,206,942,387,696,374}; int k = max(datas,10); int d = get_digit_number(k); radix_sort(datas,10,d); printf("After radix sort the result is:\n"); for(i=0;i<10;i++) printf("%d ",datas[i]); exit(0); } void radix_sort(int *datas,size_t length,size_t digits) { int i,j,k; int *temps = (int*)malloc(sizeof(int)*10); int *tmpd = (int*)malloc(sizeof(int)*length); int *rets = (int*)malloc(sizeof(int)*length); for(i=0;i<digits;i++) { memset(temps,0,10*sizeof(int)); memset(tmpd,-1,10*sizeof(int)); memset(rets,-1,10*sizeof(int)); for(j = 0;j<length;j++) tmpd[j] = get_digit(datas[j],i); for(j=0;j<length;j++) temps[tmpd[j]] = temps[tmpd[j]] +1; for(k=1;k<10;k++) temps[k] = temps[k] + temps[k-1]; for(j=length-1;j>=0;j--) { rets[temps[tmpd[j]]-1] = datas[j]; temps[tmpd[j]] = temps[tmpd[j]] -1; } memcpy(datas,rets,sizeof(int)*length); } free(temps); free(tmpd); free(rets); } int max(int *datas,size_t length) { int k = datas[0]; int i; for(i=1;i<length;++i) if(datas[i] > k) k = datas[i]; return k; } size_t get_digit(int data,size_t d) { int tmp; tmp = data; while(d) { tmp /= 10; d--; } return (tmp%10); } size_t get_digit_number(int data) { int d = 0; while(data) { d = d+1; data = data / 10; } return d; }
程序测试结果如下所示:
4、桶排序
计数排序假设输入是由一个小范围内的整数构成,而桶排序则假设输入由一个随机过程产生的,该过程将元素均匀而独立地分布在区间[0,1)上。当桶排序的输入符合均匀分布时,即可以线性期望时间运行。桶排序的思想是:把区间[0,1)划分成n个相同大小的子区间,成为桶(bucket),然后将n个输入数分布到各个桶中去,对各个桶中的数进行排序,然后按照次序把各个桶中的元素列出来即可。
数中给出了桶排序的伪代码,假设输入是一个含有n个元素的数组A,且每个元素满足0≤A[i]<1,另外需要一个辅助数组B[0....n-1]来存放链表(桶)。伪代码如下所示:
BUCKET_SORT(A) n = length(A) for i= 1 to n do insert A[i] into list B for i=0 to n-1 do sort list B[i] with insertion sort 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进行操作,完整程序如下:
#include <iostream> #include <vector> #include <list> #include <cstdlib> using namespace std; void bucket_sort(float *datas,size_t length) { int i,j; int index; float fvalue; size_t lsize; list<float> *retlist = new list<float>[length]; list<float>::iterator iter; list<float>::iterator prioiter,enditer; for(i=0;i<length;++i) { index = static_cast<int>(datas[i]*10); //insert a new element retlist[index].push_back(datas[i]); lsize = retlist[index].size(); if(lsize > 1) { //get the last element in the list[index] iter = --retlist[index].end(); fvalue = *iter; enditer = --retlist[index].begin(); //insert the last element in right position while(iter != enditer) { //get the second last element in the list[index] prioiter = --iter; //back up iter to the last element in the list[index] iter++; //compare two float values if(*(prioiter) - *iter > 0.000001) { float temp = *(prioiter); *(prioiter) = *iter; *iter = temp; } iter--; } //the right inserted position *(++iter) = fvalue; } } //copy the result to datas j=0; for(int i=0;i<length;i++) { for(iter = retlist[i].begin();iter!=retlist[i].end();++iter) datas[j++] = *iter; } delete [] retlist; } int main() { float datas[10] = {0.78f,0.17f,0.39f,0.76f,0.23f,0.67f,0.48f,0.58f,0.92f,0.12f}; bucket_sort(datas,10); cout<<"After bucket_sort the result is:"<<endl; for(int i=0;i<10;i++) cout<<datas[i]<<" "; cout<<endl; exit(0); }
程序测试结果如下:
桶排序的期望运行时间为:θ(n)+n*O(2-1/n) = θ(n)。
第九章:中位数和顺序统计数
摘要:
本章所讨论的问题是在一个由n个不同数值构成的集合中选择第i个顺序统计量问题。主要讲的内容是如何在线性时间内O(n)时间内在集合S中选择第i小的元素,最基本的是选择集合的最大值和最小值。一般情况下选择的元素是随机的,最大值和最小值是特殊情况,书中重点介绍了如何采用分治算法来实现选择第i小的元素,并借助中位数进行优化处理,保证最坏保证运行时间是线性的O(n)。
1、基本概念
顺序统计量:在一个由n个元素组成的集合中,第i个顺序统计量是值该集合中第i小的元素。例如最小值是第1个顺序统计量,最大值是第n个顺序统计量。
中位数:一般来说,中位数是指它所在集合的“中间元素”,当n为奇数时,中位数是唯一的,出现位置为n/2;当n为偶数时候,存在两个中位数,位置分别为n/2(上中位数)和n/2+1(下中位数)。
2、选择问题描述
输入:一个包含n个(不同的)数的集合A和一个数i,1≤i≤n。
输出:元素x∈A,它恰大于A中其他的i-1个元素。
最直接的办法就是采用一种排序算法先对集合A进行排序,然后输出第i个元素即可。可以采用前面讲到的归并排序、堆排序和快速排序,运行时间为O(nlgn)。接下来书中由浅入深的讲如何在线性时间内解决这个问题。
3、最大值和最小值
要在集合中选择最大值和最小值,可以通过两两元素比较,并记录最大值和最小值,n元素的集合需要比较n-1次,这样运行时间为O(n)。举个例子来说明,现在要求和集合A={32,12,23,67,45,78}的最大值,开始假设第一个元素最大,即max=1,然后从第二个元素开始向后比较,记录最大值的位置。执行过程如下图所示:
书中给出的求最小值的伪代码如下:
MINMUN(A) min = A[1] for i=1 to length(A) do if min > A[i] then min >= A[i] return min
问题:
(1)同时找出集合的最大值和最小值
方法1:按照上面讲到的方法,分别独立的找出集合的最大值和最小值,各用n-1次比较,共有2n-2次比较。
方法2:可否将最大值和最小值结合在一起寻找呢?答案是可以的,在两两比较过程中同时记录最大值和最小值,这样最大需要3n/2次比较。现在的做法不是将每一个 输入元素与当前的最大值和最小值进行比较,而是成对的处理元素,先将一对输入元素进行比较,然后把较大者与当前最大值比较,较小者与当前最小者比较,因此每两个元素需要3次比较。初始设置最大值和最小值方法:如何n为奇数,就将最大值和最小值都设置为第一个元素的值,然后成对的处理后续的元素。如果n为偶数,那么先比较前面两个元素的值,较大的设置为最大值,较小的设置为最小值,然后成对处理后续的元素。这样做的目的保证能够成对的处理后续的元素。举个例子说明这个过程,假设现在要找出集合A={32,23,12,67,45,78,10,39,9,58}最大值和最小值,执行过程如下:
从图中可以看出方法2要比方法一要好,减少了元素之间的比较次数。现在用C语言实现方法2,程序如下:
#include <stdio.h> #include <stdlib.h> //return max and min value by pointer void get_max_min(int *datas,int length,int* ptrmax,int* ptrmin) { int i,maxtmp,mintmp; //judge length is even or odd if(length %2 == 0) { if(datas[0] > datas[1]) { *ptrmax = datas[0]; *ptrmin = datas[1]; } else { *ptrmax = datas[1]; *ptrmin = datas[0]; } } else { *ptrmax = datas[0]; *ptrmin = datas[0]; } for(i=2;i<length;i+=2) { if(datas[i] > datas[i+1]) { maxtmp = datas[i]; mintmp = datas[i+1]; } else { maxtmp = datas[i+1]; mintmp = datas[i]; } if(*ptrmax < maxtmp) *ptrmax = maxtmp; if(*ptrmin > mintmp) *ptrmin = mintmp; } } int main() { int max,min; int i; int datas[10] = {23,12,34,26,78,45,87,15,60,19}; get_max_min(datas,10,&max,&min); printf("All elements in set are:\n"); for(i=0;i<10;++i) printf("%d ",datas[i]); putchar('\n'); printf("max=%d\tmin=%d\n",max,min); exit(0); }
程序测试结果如下:
(2)如何找出找出n个元素中的第2小元素。
解答:类似与上面的同时找出最大值和最小值的方法2,变成同时找最小值和第2小元素值。先初始化最小值和第2小的值,然后成对比较后续的元素,找出较小的元素与当前最小值和第二小值进行比较,在三者中找出最小值和第二小值。
4、以期望线性时间做选择
一般的选择问题似乎要比选择最大值和最小值要难,但是这两种问题的运行时间是相同的,都是θ(n)。书中介绍了采用分治算法解决一般的选择问题,其过程与快速排序过程中划分类似。每次划分集合可以确定一个元素的最终位置,根据这个位置可以判断是否是我们要求的第i小的元素。如果不是,那么我们只关心划分产出两个子部分中的其中一个,根据i的值来判断是前一个还是后一个,然后接着对子数组进行划分,重复此过程,直到找到第i个小的元素。划分可以采用随机划分,这样能够保证期望时间是θ(n)(假设所有元素是不同的)。
给个例子说明此过程,假设现有集合A={32,23,12,67,45,78,10,39,9,58},要求其第5小的元素,假设在划分过程中以总是以最后一个元素为主元素进行划分。执行过程如下图所示:
书中给出了返回A[p...r]中的第i小元素的伪代码:
RANDOMIZED_SELECT(A,p,r,i) if p==r then return A[p] q = RANDOMIZED_PARTITION(A,p,r) k = q-p+1; if i==k then return A[q] else if i<k then return RANDOMIZED_SELECT(A,p,q-1,i) else return RANDOMIZED_SELECT(A,p,q-1,i-k)
RANDOMIZED_SELECT通过对输入数组的递归划分来找出所求元素,该算法要保证对数组的划分是个好划分才更加高效。RANDOMIZED_SELECT的最坏情况运行时间为θ(n^2),即使是选择最小元素也是如此。因为在每次划分过程中,导致划分后两边不对称,总好是按照剩下元素中最大的划分进行。为了更好的选择过程,我采用C语言实现求集合A={32,23,12,67,45,78,10,39,9,58}的第i小的元素,完成程序如下:
#include <stdio.h> #include <stdlib.h> #include <time.h> size_t randomized_partition(int* datas,size_t beg,size_t last); void swap(int* a,int *b); int randomized_select_one(int* datas,int beg,int last,int i); int randomized_select_two(int* datas,int length,int i); int main() { int datas[10]={32,23,12,67,45,78,10,39,9,58}; int i,ret; printf("The array is: \n"); for(i=0;i<10;++i) printf("%d ",datas[i]); printf("\n"); for(i=1;i<=10;++i) { //ret=randomized_select_one(datas,0,9,i); ret=randomized_select_two(datas,10,i); printf("The %dth least number is: %d \n",i,datas[i-1]); } exit(0); } /* 参数介绍:datas是待划分的数组,数组下标从0开始。 beg代表开始位置,last代表结束位置、封闭区间[beg,last] */ size_t randomized_partition(int* datas,size_t beg,size_t last) { int len,i,j,index; len = last-beg+1; //随机获取一个主元 srand(time(NULL)); index = beg + rand()%len; //将主元交换到末尾 swap(datas+index,datas+last); //从第一个元素开始向后查找主元的位置 i=beg; for(j=beg;j<last;j++) { if(datas[j] < datas[last]) { swap(datas+i,datas+j); i++; } } //最终确定主元的位置 swap(datas+i,datas+last); return i; } /* 参数介绍:datas是待查找的数组,数组下标从0开始。 beg代表开始位置,last代表结束位置、封闭区间[beg,last] i表示要要查找第i小元素,i从1开始 */ int randomized_select_one(int* datas,int beg,int last,int i) { int pivot,k; if(beg == last) return datas[beg]; pivot = randomized_partition(datas,beg,last); k = pivot-beg+1; if(k == i) return datas[pivot]; else if(k < i) randomized_select_one(datas,pivot+1,last,i-k); else randomized_select_one(datas,beg,pivot-1,i); } /* 参数介绍:datas是待查找的数组,数组下标从0开始。 length表示数组的长度,数组下标范围[0,length-1] i表示要要查找第i小元素,i从1开始 */ int randomized_select_two(int* datas,int length,int i) { int pivot,k,j; if(length == 1) return datas[length-1]; pivot = randomized_partition(datas,0,length-1); //确定是主元是第k小 k = pivot+1; if(k == i) return datas[pivot]; else if(k < i) randomized_select_two(datas+k,length-k,i-k); else randomized_select_two(datas,pivot,i); } void swap(int* a,int *b) { int temp = *a; *a = *b; *b = temp; }
程序测试结果如下所示:
程序中要注意的细节问题是:C语言中数组的小标是从0开始的,而要求第i小元素中的i是从1开始的,即第1小的元素对应与最终的主元位置0,依次类推。
5、最坏情况线性时间的选择
SELECT算法的思想是要保证对数组的划分是个好的划分,对PARTITION过程进行了修改。现在通过SELECT算法来确定n个元素的输入数组中的第i小的元素,具体操作步骤如下:
(1)将输入数组的n个元素划分为n/5(上取整)组,每组5个元素,且至多只有一个组有剩下的n%5个元素组成。(为何是5,而不是其他数,有点不明白。)
(2)寻找每个组织中中位数。首先对每组中的元素(至多为5个)进行插入排序,然后从排序后的序列中选择出中位数。
(3)对第2步中找出的n/5(上取整)个中位数,递归调用SELECT以找出其中位数x。(如果是偶数去下中位数)
(4)调用PARTITION过程,按照中位数x对输入数组进行划分。确定中位数x的位置k。
(5)如果i=k,则返回x。否则,如果i<k,则在地区间递归调用SELECT以找出第i小的元素,若干i>k,则在高区找第(i-k)个最小元素。
SELECT算法通过中位数进行划分,可以保证每次划分是对称的,这样就能保证最坏情况下运行时间为θ(n)。举个例子说明此过程,求集合A={32,23,12,67,45,78,10,39,9,58,125,84}的第5小的元素,操作过程如下图所示:
现在采用C语言实现上面的例子,完整程序如下所示:
#include <stdio.h> #include <stdlib.h> int partition(int* datas,int beg,int last,int mid); int select(int* datas,int length,int i); void swap(int* a,int *b); int main() { int datas[12]={32,23,12,67,45,78,10,39,9,58,125,84}; int i,ret; printf("The array is: \n"); for(i=0;i<12;++i) printf("%d ",datas[i]); printf("\n"); for(i=1;i<=12;++i) { ret=select(datas,12,i); printf("The %dth least number is: %d \n",i,datas[i-1]); } exit(0); } int partition(int* datas,int beg,int last,int mid) { int i,j; swap(datas+mid,datas+last); i=beg; for(j=beg;j<last;j++) { if(datas[j] < datas[last]) { swap(datas+i,datas+j); i++; } } swap(datas+i,datas+last); return i; } int select(int* datas,int length,int i) { int groups,pivot; int j,k,t,q,beg,glen; int mid; int temp,index; int *pmid; if(length == 1) return datas[length-1]; if(length % 5 == 0) groups = length/5; else groups = length/5 +1; pmid = (int*)malloc(sizeof(int)*groups); index = 0; for(j=0;j<groups;j++) { beg = j*5; glen = beg+5; for(t=beg+1;t<glen && t<length;t++) { temp = datas[t]; for(q=t-1;q>=beg && datas[q] > datas[q+1];q--) swap(datas+q,datas+q+1); swap(datas+q+1,&temp); } glen = glen < length ? glen : length; pmid[index++] = beg+(glen-beg)/2; } for(t=1;t<groups;t++) { temp = pmid[t]; for(q=t-1;q>=0 && datas[pmid[q]] > datas[pmid[q+1]];q--) swap(pmid+q,pmid+q+1); swap(pmid+q+1,&temp); } //printf("mid indx = %d,mid value=%d\n",pmid[groups/2],datas[pmid[groups/2]]); mid = pmid[groups/2]; pivot = partition(datas,0,length-1,mid); //printf("pivot=%d,value=%d\n",pivot,datas[pivot]); k = pivot+1; if(k == i) return datas[pivot]; else if(k < i) return select(datas+k,length-k,i-k); else return select(datas,pivot,i); } void swap(int* a,int *b) { int temp = *a; *a = *b; *b = temp; }
程序测试结果如下所示:
总结
本章中的选择算法之所以具有线性运行时间,是因为这些算法没有进行排序,线性时间的行为并不是因为对输入做假设所得到的结果。