分而治之算法应用.docx

上传人:b****7 文档编号:23349799 上传时间:2023-05-16 格式:DOCX 页数:27 大小:33.09KB
下载 相关 举报
分而治之算法应用.docx_第1页
第1页 / 共27页
分而治之算法应用.docx_第2页
第2页 / 共27页
分而治之算法应用.docx_第3页
第3页 / 共27页
分而治之算法应用.docx_第4页
第4页 / 共27页
分而治之算法应用.docx_第5页
第5页 / 共27页
点击查看更多>>
下载资源
资源描述

分而治之算法应用.docx

《分而治之算法应用.docx》由会员分享,可在线阅读,更多相关《分而治之算法应用.docx(27页珍藏版)》请在冰豆网上搜索。

分而治之算法应用.docx

分而治之算法应用

 

2.2应用

2.2.1残缺棋盘

残缺棋盘(defectivechessboard)是一个有2k×2k个方格的棋盘,其中恰有一个方格残缺。

图2-3给出k≤2时各种可能的残缺棋盘,其中残缺的方格用阴影表示。

注意当k=0时,仅存在一种可能的残缺棋盘(如图14-3a所示)。

事实上,对于任意k,恰好存在22k种不同的残缺棋盘。

残缺棋盘的问题要求用三格板(triominoes)覆盖残缺棋盘(如图14-4所示)。

在此覆盖中,两个三格板不能重叠,三格板不能覆盖残缺方格,但必须覆盖其他所有的方格。

在这种限制条件下,所需要的三格板总数为(22k-1)/3。

可以验证(22k-1)/3是一个整数。

k为0的残缺棋盘很容易被覆盖,因为它没有非残缺的方格,用于覆盖的三格板的数目为0。

当k=1时,正好存在3个非残缺的方格,并且这三个方格可用图14-4中的某一方向的三格板来覆盖。

用分而治之方法可以很好地解决残缺棋盘问题。

这一方法可将覆盖2k×2k残缺棋盘的问题转化为覆盖较小残缺棋盘的问题。

2k×2k棋盘一个很自然的划分方法就是将它划分为如图14-5a所示的4个2k-1×2k-1棋盘。

注意到当完成这种划分后,4个小棋盘中仅仅有一个棋盘存在残缺方格(因为原来的2k×2k棋盘仅仅有一个残缺方格)。

首先覆盖其中包含残缺方格的2k-1×2k-1残缺棋盘,然后把剩下的3个小棋盘转变为残缺棋盘,为此将一个三格板放在由这3个小棋盘形成的角上,如图14-5b所示,其中原2k×2k棋盘中的残缺方格落入左上角的2k-1×2k-1棋盘。

可以采用这种分割技术递归地覆盖2k×2k残缺棋盘。

当棋盘的大小减为1×1时,递归过程终止。

此时1×1的棋盘中仅仅包含一个方格且此方格残缺,所以无需放置三格板。

可以将上述分而治之算法编写成一个递归的C++函数TileBoard(见程序14-2)。

该函数定义了一个全局的二维整数数组变量Board来表示棋盘。

Board[0][0]表示棋盘中左上角的方格。

该函数还定义了一个全局整数变量tile,其初始值为0。

函数的输入参数如下:

?

tr棋盘中左上角方格所在行。

?

tc棋盘中左上角方格所在列。

?

dr残缺方块所在行。

?

dl残缺方块所在列。

?

size棋盘的行数或列数。

TileBoard函数的调用格式为TileBoard(0,0,dr,dc,size),其中size=2k。

覆盖残缺棋盘所需要的三格板数目为(size2-1)/3。

函数TileBoard用整数1到(size2-1)/3来表示这些三格板,并用三格板的标号来标记被该三格板覆盖的非残缺方格。

令t(k)为函数TileBoard覆盖一个2k×2k残缺棋盘所需要的时间。

当k=0时,size等于1,覆盖它将花费常数时间d。

当k>0时,将进行4次递归的函数调用,这些调用需花费的时间为4t(k-1)。

除了这些时间外,if条件测试和覆盖3个非残缺方格也需要时间,假设用常数c表示这些额外时间。

可以得到以下递归表达式:

程序14-2覆盖残缺棋盘

voidTileBoard(inttr,inttc,intdr,intdc,intsize)

{//覆盖残缺棋盘

if(size==1)return;

intt=tile++,//所使用的三格板的数目

s=size/2;//象限大小

//覆盖左上象限

if(dr

//残缺方格位于本象限

TileBoard(tr,tc,dr,dc,s);

else{//本象限中没有残缺方格

//把三格板t放在右下角

Board[tr+s-1][tc+s-1]=t;

//覆盖其余部分

TileBoard(tr,tc,tr+s-1,tc+s-1,s);}

//覆盖右上象限

if(dr=tc+s)

//残缺方格位于本象限

TileBoard(tr,tc+s,dr,dc,s);

else{//本象限中没有残缺方格

//把三格板t放在左下角

Board[tr+s-1][tc+s]=t;

//覆盖其余部分

TileBoard(tr,tc+s,tr+s-1,tc+s,s);}

//覆盖左下象限

if(dr>=tr+s&&dc

//残缺方格位于本象限

TileBoard(tr+s,tc,dr,dc,s);

else{//把三格板t放在右上角

Board[tr+s][tc+s-1]=t;

//覆盖其余部分

TileBoard(tr+s,tc,tr+s,tc+s-1,s);}

//覆盖右下象限

if(dr>=tr+s&&dc>=tc+s)

//残缺方格位于本象限

TileBoard(tr+s,tc+s,dr,dc,s);

else{//把三格板t放在左上角

Board[tr+s][tc+s]=t;

//覆盖其余部分

TileBoard(tr+s,tc+s,tr+s,tc+s,s);}

}

voidOutputBoard(intsize)

{

for(inti=0;i

for(intj=0;j

cout<

cout<

}

}

可以用迭代的方法来计算这个表达式(见例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(left

inti=(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(s

MergePass(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+s

elsefor(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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > PPT模板 > 节日庆典

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1