}
}
可以用迭代的方法来计算这个表达式(见例2-20),可得t(k)=(4k)=(所需的三格板的数目)。
由于必须花费至少
(1)的时间来放置每一块三格表,因此不可能得到一个比分而治之算法更快的算法。
2.2.2归并排序
可以运用分而治之方法来解决排序问题,该问题是将n个元素排成非递减顺序。
分而治之方法通常用以下的步骤来进行排序算法:
若n为1,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。
假设仅将n个元素的集合分成两个子集合。
现在需要确定如何进行子集合的划分。
一种可能性就是把前面n-1个元素放到第一个子集中(称为A),最后一个元素放到第二个子集里(称为B)。
按照这种方式对A递归地进行排序。
由于B仅含一个元素,所以它已经排序完毕,在A排完序后,只需要用程序2-10中的函数insert将A和B合并起来。
把这种排序算法与InsertionSort(见程序2-15)进行比较,可以发现这种排序算法实际上就是插入排序的递归算法。
该算法的复杂性为O(n2)。
把n个元素划分成两个子集合的另一种方法是将含有最大值的元素放入B,剩下的放入A中。
然后A被递归排序。
为了合并排序后的A和B,只需要将B添加到A中即可。
假如用函数Max(见程序1-31)来找出最大元素,这种排序算法实际上就是SelectionSort(见程序2-7)的递归算法。
假如用冒泡过程(见程序2-8)来寻找最大元素并把它移到最右边的位置,这种排序算法就是BubbleSort(见程序2-9)的递归算法。
这两种递归排序算法的复杂性均为(n2)。
若一旦发现A已经被排好序就终止对A进行递归分割,则算法的复杂性为O(n2)(见例2-16和2-17)。
上述分割方案将n个元素分成两个极不平衡的集合A和B。
A有n-1个元素,而B仅含一个元素。
下面来看一看采用平衡分割法会发生什么情况:
A集合中含有n/k个元素,B中包含其余的元素。
递归地使用分而治之方法对A和B进行排序。
然后采用一个被称之为归并(merge)的过程,将已排好序的A和B合并成一个集合。
例2-5考虑8个元素,值分别为[10,4,6,3,8,2,5,7]。
如果选定k=2,则[10,4,6,3]和[8,2,5,7]将被分别独立地排序。
结果分别为[3,4,6,10]和[2,5,7,8]。
从两个序列的头部开始归并这两个已排序的序列。
元素2比3更小,被移到结果序列;3与5进行比较,3被移入结果序列;4与5比较,4被放入结果序列;5和6比较,.。
如果选择k=4,则序列[10,4]和[6,3,8,2,5,7]将被排序。
排序结果分别为[4,10]和[2,3,5,6,7,8]。
当这两个排好序的序列被归并后,即可得所需要的排序序列。
图2-6给出了分而治之排序算法的伪代码。
算法中子集合的数目为2,A中含有n/k个元素。
template
voidsort(TE,intn)
{//对E中的n个元素进行排序,k为全局变量
if(n>=k){
i=n/k;
j=n-i;
令A包含E中的前i个元素
令B包含E中余下的j个元素
sort(A,i);
sort(B,j);
merge(A,B,E,i,j,);//把A和B合并到E
}
else使用插入排序算法对E进行排序
}
图14-6分而治之排序算法的伪代码
从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O(n)。
设t(n)为分而治之排序算法(如图14-6所示)在最坏情况下所需花费的时间,则有以下递推公式:
其中c和d为常数。
当n/k≈n-n/k时,t(n)的值最小。
因此当k=2时,也就是说,当两个子集合所包含的元素个数近似相等时,t(n)最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。
可以用迭代方法来计算这一递推方式,结果为t(n)=(nlogn)。
虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n)是n的非递减函数。
t(n)=(nlogn)给出了归并排序的最好和最坏情况下的复杂性。
由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t(n)=(nlogn)。
图2-6中k=2的排序方法被称为归并排序(mergesort),或更精确地说是二路归并排序(two-waymergesort)。
下面根据图14-6中k=2的情况(归并排序)来编写对n个元素进行排序的C++函数。
一种最简单的方法就是将元素存储在链表中(即作为类chain的成员(程序3-8))。
在这种情况下,通过移到第n/2个节点并打断此链,可将E分成两个大致相等的链表。
归并过程应能将两个已排序的链表归并在一起。
如果希望把所得到C++程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。
为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a中返回排序后的元素序列。
为此按照下述过程来对图14-6的伪代码进行细化:
当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。
接下来对a中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a中。
图14-6的改进版见图14-7。
template
MergeSort(Ta[],intleft,intright)
{//对a[left:
right]中的元素进行排序
if(leftinti=(left+right)/2;//中心位置
MergeSort(a,left,i);
MergeSort(a,i+1,right);
Merge(a,b,left,i,right);//从a合并到b
Copy(b,a,left,right);//结果放回a
}
}
图14-7分而治之排序算法的改进
可以从很多方面来改进图14-7的性能,例如,可以容易地消除递归。
如果仔细地检查图14-7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。
当序列的长度变为1时即可进行归并操作,这个过程可以用n为2的幂来很好地描述。
长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n的序列。
图14-8给出n=8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。
初始序列[8][4][5][6][2][1][7][3]
归并到b[48][56][12][37]
复制到a[48][56][12][37]
归并到b[4568][1237]
复制到a[4568][1237]
归并到b[12345678]
复制到a[12345678]
图14-8归并排序的例子
另一种二路归并排序算法是这样的:
首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。
通过轮流地将元素从a归并到b并从b归并到a,可以虚拟地消除复制过程。
二路归并排序算法见程序14-3。
程序14-3二路归并排序
template
voidMergeSort(Ta[],intn)
{//使用归并排序算法对a[0:
n-1]进行排序
T*b=newT[n];
ints=1;//段的大小
while(sMergePass(a,b,s,n);//从a归并到b
s+=s;
MergePass(b,a,s,n);//从b归并到a
s+=s;
}
}
为了完成排序代码,首先需要完成函数MergePass。
函数MergePass(见程序14-4)仅用来确定欲归并子序列的左端和右端,实际的归并工作由函数Merge(见程序14-5)来完成。
函数Merge要求针对类型T定义一个操作符<=。
如果需要排序的数据类型是用户自定义类型,则必须重载操作符<=。
这种设计方法允许我们按元素的任一个域进行排序。
重载操作符<=的目的是用来比较需要排序的域。
程序14-4MergePass函数
template
voidMergePass(Tx[],Ty[],ints,intn)
{//归并大小为s的相邻段
inti=0;
while(i<=n-2*s){
//归并两个大小为s的相邻段
Merge(x,y,i,i+s-1,i+2*s-1);
i=i+2*s;
}
//剩下不足2个元素
if(i+selsefor(intj=i;j<=n-1;j++)
//把最后一段复制到y
y[j]=x[j];
}
程序14-5Merge函数
template
voidMerge(Tc[],Td[],intl,intm,intr)
{//把c[l:
m]]和c[m:
r]归并到d[l:
r].
inti=l,//第一段的游标
j=m+1,//第二段的游标
k=l;//结果的游标
//只要在段中存在i和j,则不断进行归并
while((i<=m)&&(j<=r))
if(c[i]<=c[j])d[k++]=c[i++];
elsed[k++]=c[j++];
//考虑余下的部分
if(i>m)for(intq=j;q<=r;q++)
d[k++]=c[q];
elsefor(intq=i;q<=m;q++)
d[k++]=c[q];
}
自然归并排序(naturalmergesort)是基本归并排序(见程序14-3)的一种变化。
它首先对输入序列中已经存在的有序子序列进行归并。
例如,元素序列[4,8,3,7,1,5,6,2]中包含有序的子序列[4,8],[3,7],[1,5,6]和[2],这些子序列是按从左至右的顺序对元素表进行扫描而产生的,若位置i的元素比位置i+1的元素大,则从位置i进行分割。
对于上面这个元素序列,可找到四个子序列,子序列1和子序列2归并可得[3,4,7,8],子序列3和子序列4归并可得[1,2,5,6],最后,归并这两个子序列得到[1,2,3,4,5,6,7,8]。
因此,对于上述元素序列,仅仅使用了两趟归并,而程序14-3从大小为1的子序列开始,需使用三趟归并。
作为一个极端的例子,假设输入的元素序列已经排好序并有n个元素,自然归并排序法将准确地识别该序列不必进行归并排序,但程序14-3仍需要进行[log2n]趟归并。
因此自然归并排序将在(n)的时间内完成排序。
而程序14-3将花费(nlogn)的时间。
2.2.3快速排序
分而治之方法还可以用于实现另一种完全不同的排序方法,这种排序法称为快速排序(quicksort)。
在这种方法中,n个元素被分成三段(组):
左段left,右段right和中段middle。
中段仅包含一个元素。
左段中各元素都小于等于中段元素,右段中各元素都大于等于中段元素。
因此left和right中的元素可以独立排序,并且不必对left和right的排序结果进行合并。
middle中的元素被称为支点(pivot)。
图14-9中给出了快速排序的伪代码。
//使用快速排序方法对a[0:
n-1]排序
从a[0:
n-1]中选择一个元素作为middle,该元素为支点
把余下的元素分割为两段left和right,使得left中的元素都小于等于支点,而right中的元素都大于等于支点
递归地使用快速排序方法对left进行排序
递归地使用快速排序方法对right进行排序
所得结果为left+middle+right
图14-9快速排序的伪代码
考察元素序列[4,8,3,7,1,5,6,2]。
假设选择元素6作为支点,则6位于middle;4,3,1,5,2位于left;8,7位于right。
当left排好序后,所得结果为1,2,3,4,5;当right排好序后,所得结果为7,8。
把right中的元素放在支点元素之后,left中的元素放在支点元素之前,即可得到最终的结果[1,2,3,4,5,6,7,8]。
把元素序列划分为left、middle和right可以就地进行(见程序14-6)。
在程序14-6中,支点总是取位置1中的元素。
也可以采用其他选择方式来提高排序性能,本章稍后部分将给出这样一种选择。
程序14-6快速排序
template
voidQuickSort(T*a,intn)
{//对a[0:
n-1]进行快速排序
{//要求a[n]必需有最大关键值
quickSort(a,0,n-1);
template
voidquickSort(Ta[],intl,intr)
{//排序a[l:
r],a[r+1]有大值
if(l>=r)return;
inti=l,//从左至右的游标
j=r+1;//从右到左的游标
Tpivot=a[l];
//把左侧>=pivot的元素与右侧<=pivot的元素进行交换
while(true){
do{//在左侧寻找>=pivot的元素
i=i+1;
}while(a[i]do{//在右侧寻找<=pivot的元素
j=j-1;
}while(a[j]>pivot);
if(i>=j)break;//未发现交换对象
Swap(a[i],a[j]);
}
//设置pivot
a[l]=a[j];
a[j]=pivot;
quickSort(a,l,j-1);//对左段排序
quickSort(a,j+1,r);//对右段排序
}
若把程序14-6中do-while条件内的<号和>号分别修改为<=和>=,程序14-6仍然正确。
实验结果表明使用程序14-6的快速排序代码可以得到比较好的平均性能。
为了消除程序中的递归,必须引入堆栈。
不过,消除最后一个递归调用不须使用堆栈。
消除递归调用的工作留作练习(练习13)。
程序14-6所需要的递归栈空间为O(n)。
若使用堆栈来模拟递归,则可以把这个空间减少为O(logn)。
在模拟过程中,首先对left和right中较小者进行排序,把较大者的边界放入堆栈中。
在最坏情况下left总是为空,快速排序所需的计算时间为(n2)。
在最好情况下,left和right中的元素数目大致相同,快速排序的复杂性为(nlogn)。
令人吃惊的是,快速排序的平均复杂性也是(nlogn)。
定理2-1快速排序的平均复杂性为(nlogn)。
证明用t(n)代表对含有n个元素的数组进行排序的平均时间。
当n≤1时,t(n)≤d,d为某一常数。
当n<1时,用s表示左段所含元素的个数。
由于在中段中有一个支点元素,因此右段中元素的个数为n-s-1。
所以左段和右段的平均排序时间分别为t(s),t(n-s-1)。
分割数组中元素所需要的时间用cn表示,其中c是一个常数。
因为s有同等机会取0~n-1中的任何一个值.
如对(2-8)式中的n使用归纳法,可得到t(n)≤knlogen,其中n>1且k=2(c+d),e~2.718为自然对数的基底。
在归纳开始时首先验证n=2时公式的正确性。
根据公式(14
展开阅读全文
相关搜索