returnmin;
}
非递归算法分析的一般步骤:
1.决定用哪个(或哪些)参数作为算法问题规模的度量
2.找出算法中的基本语句
3.检查基本语句的执行次数是否只依赖于问题规模
4.建立基本语句执行次数的求和表达式
5.用渐进符号表示这个求和表达式
v关键:
建立一个代表算法运行时间的求和表达式,然后用渐进符号表示这个求和表达式。
1.2.4递归算法的分析
关键:
根据递归过程建立递推关系式,然后求解这个递推关系式。
1.猜测技术:
对递推关系式估计一个上限,然后(用数学归纳法)证明它正确。
2.扩展递归技术
3.通用分治递推式
大小为n的原问题分成若干个大小为n/b的子问题,其中a个子问题需要求解,而cnk是合并各个子问题的解需要的工作量。
1.2.5算法的后验分析
算法的后验分析(Posteriori)也称算法的实验分析,它是一种事后计算的方法,通常需要将算法转换为对应的程序并上机运行。
一般步骤:
1.明确实验目的
2.决定度量算法效率的方法,为实验准备算法的程序实现
3.决定输入样本,生成实验数据
4.对输入样本运行算法对应的程序,记录得到的实验数据
5.分析得到的实验数据
表格法记录实验数据
散点图记录实验数据
第4章分治法
4.1概述
4.1.1分治法的设计思想
将一个难以直接解决的大问题,划分成一些规模较小的子问题,以便各个击破,分而治之。
更一般地说,将要求解的原问题划分成k个较小规模的子问题,对这k个子问题分别求解。
如果子问题的规模仍然不够小,则再将每个子问题划分为k个规模更小的子问题,如此分解下去,直到问题规模足够小,很容易求出其解为止,再将子问题的解合并为一个更大规模的问题的解,自底向上逐步求出原问题的解。
启发式规则:
1.平衡子问题:
最好使子问题的规模大致相同。
也就是将一个问题划分成大小相等的k个子问题(通常k=2),这种使子问题规模大致相等的做法是出自一种平衡(Balancing)子问题的思想,它几乎总是比子问题规模不等的做法要好。
2.独立子问题:
各子问题之间相互独立,这涉及到分治法的效率,如果各子问题不是独立的,则分治法需要重复地解公共的子问题。
分治法的典型情况
4.1.2分治法的求解过程
一般来说,分治法的求解过程由以下三个阶段组成:
(1)划分:
既然是分治,当然需要把规模为n的原问题划分为k个规模较小的子问题,并尽量使这k个子问题的规模大致相同。
(2)求解子问题:
各子问题的解法与原问题的解法通常是相同的,可以用递归的方法求解各个子问题,有时递归处理也可以用循环来实现。
(3)合并:
把各个子问题的解合并起来,合并的代价因情况不同有很大差异,分治算法的有效性很大程度上依赖于合并的实现。
分治法的一般过程
DivideConquer(P)
{
if(P的规模足够小)直接求解P;
分解为k个子问题P1,P2,…Pk;
for(i=1;i<=k;i++)
yi=DivideConquer(Pi);
returnMerge(y1,…,yk);
}
例:
计算an,应用分治技术得到如下计算方法:
4.2递归
4.2.1递归的定义
递归就是子程序(或函数)直接调用自己或通过一系列调用语句间接调用自己,是一种描述问题和解决问题的基本方法。
递归有两个基本要素:
⑴边界条件:
确定递归到何时终止;
⑵递归模式:
大问题是如何分解为小问题的,确定递归体。
4.2.2递归函数的运行轨迹
在递归函数中,调用函数和被调用函数是同一个函数,需要注意的是递归函数的调用层次,如果把调用递归函数的主函数称为第0层,进入函数后,首次递归调用自身称为第1层调用;从第i层递归调用自身称为第i+1层。
反之,退出第i+1层调用应该返回第i层。
采用图示方法描述递归函数的运行轨迹,从中可较直观地了解到各调用层次及其执行情况。
4.2.3递归函数的内部执行过程
一个递归函数的调用过程类似于多个函数的嵌套调用,只不过调用函数和被调用函数是同一个函数。
为了保证递归函数的正确执行,系统需设立一个工作栈。
具体地说,递归调用的内部执行过程如下:
(1)运行开始时,首先为递归调用建立一个工作栈,其结构包括值参、局部变量和返回地址;
(2)每次执行递归调用之前,把递归函数的值参和局部变量的当前值以及调用后的返回地址压栈;
(3)每次递归调用结束后,将栈顶元素出栈,使相应的值参和局部变量恢复为调用前的值,然后转向返回地址指定的位置继续执行。
汉诺塔算法在执行过程中,工作栈的变化下图所示,其中栈元素的结构为(返回地址,n值,A值,B值,C值),返回地址对应算法中语句的行号。
递归算法结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此,它为设计算法和调试程序带来很大方便,是算法设计中的一种强有力的工具。
但是,因为递归算法是一种自身调用自身的算法,随着递归深度的增加,工作栈所需要的空间增大,递归调用时的辅助操作增多,因此,递归算法的运行效率较低。
4.3排序问题中的分治法
4.3.1归并排序
二路归并排序的分治策略是:
(1)划分:
将待排序序列r1,r2,…,rn划分为两个长度相等的子序列r1,…,rn/2和rn/2+1,…,rn;
(2)求解子问题:
分别对这两个子序列进行排序,得到两个有序子序列;
(3)合并:
将这两个有序子序列合并成一个有序序列。
算法4.3——归并排序
voidMergeSort(intr[],intr1[],ints,intt)
{
if(s==t)r1[s]=r[s];
else{
m=(s+t)/2;
Mergesort(r,r1,s,m);//归并排序前半个子序列
Mergesort(r,r1,m+1,t);//归并排序后半个子序列
Merge(r1,r,s,m,t);//合并两个已排序的子序列
}
}
算法4.4——合并有序子序列
voidMerge(intr[],intr1[],ints,intm,intt)
{
i=s;j=m+1;k=s;
while(i<=m&&j<=t)
{
if(r[i]<=r[j])r1[k++]=r[i++];//取r[i]和r[j]中较小者放入r1[k]
elser1[k++]=r[j++];
}
if(i<=m)while(i<=m)
//若第一个子序列没处理完,则进行收尾处理
r1[k++]=r[i++];
elsewhile(j<=t)
//若第二个子序列没处理完,则进行收尾处理
r1[k++]=r[j++];
}
二路归并排序的合并步的时间复杂性为O(n),所以,二路归并排序算法存在如下递推式:
根据1.2.4节的主定理,二路归并排序的时间代价是O(nlog2n)。
二路归并排序在合并过程中需要与原始记录序列同样数量的存储空间,因此其空间复杂性为O(n)。
4.3.2快速排序
快速排序的分治策略是:
(1)划分:
选定一个记录作为轴值,以轴值为基准将整个序列划分为两个子序列r1…ri-1和ri+1…rn,前一个子序列中记录的值均小于或等于轴值,后一个子序列中记录的值均大于或等于轴值;
(2)求解子问题:
分别对划分后的每一个子序列递归处理;
(3)合并:
由于对子序列r1…ri-1和ri+1…rn的排序是就地进行的,所以合并不需要执行任何操作。
v归并排序按照记录在序列中的位置对序列进行划分,
v快速排序按照记录的值对序列进行划分。
以第一个记录作为轴值,对待排序序列进行划分的过程为:
(1)初始化:
取第一个记录作为基准,设置两个参数i,j分别用来指示将要与基准记录进行比较的左侧记录位置和右侧记录位置,也就是本次划分的区间;
(2)右侧扫描过程:
将基准记录与j指向的记录进行比较,如果j指向记录的关键码大,则j前移一个记录位置。
重复右侧扫描过程,直到右侧的记录小(即反序),若i<j,则将基准记录与j指向的记录进行交换;
(3)左侧扫描过程:
将基准记录与i指向的记录进行比较,如果i指向记录的关键码小,则i后移一个记录位置。
重复左侧扫描过程,直到左侧的记录大(即反序),若i<j,则将基准记录与i指向的记录交换;
(4)重复
(2)(3)步,直到i与j指向同一位置,即基准记录最终的位置。
一次划分示例
算法4.5——一次划分
intPartition(intr[],intfirst,intend)
{
i=first;j=end;//初始化
while(i{
while(iif(ir[i]←→r[j];//将较小记录交换到前面
i++;
}
while(iif(ir[j]←→r[i];//将较大记录交换到后面
j--;
}
}
retutni;//i为轴值记录的最终位置
}
以轴值为基准将待排序序列划分为两个子序列后,对每一个子序列分别递归进行排序。
算法4.6——快速排序
voidQuickSort(intr[],intfirst,intend)
{
if(firstpivot=Partition(r,first,end);
//问题分解,pivot是轴值在序列中的位置
QuickSort(r,first,pivot-1);
//递归地对左侧子序列进行快速排序
QuickSort(r,pivot+1,end);
//递归地对右侧子序列进行快速排序
}
}
在最好情况下,每次划分对一个记录定位后,该记录的左侧子序列与右侧子序列的长度相同。
在具有n个记录的序列中,一次划分需要对整个待划分序列扫描一遍,则所需时间为O(n)。
设T(n)是对n个记录的序列进行排序的时间,每次划分后,正好把待划分区间划分为长度相等的两个子序列,则有:
T(n)≤2T(n/2)+n
≤2(2T(n/4)+n/2)+n=4T(n/4)+2n
≤4(2T(n/8)+n/4)+2n=8T(n/8)+3n
………
≤nT
(1)+nlog2n=O(nlog2n)
因此,时间复杂度为O(nlog2n)。
在最坏情况下,待排序记录序列正序或逆序,每次划分只得到一个比上一次划分少一个记录的子序列(另一个子序列为空)。
此时,必须经过n-1次递归调用才能把所有记录定位,而且第i趟划分需要经过n-i次关键码的比较才能找到第i个记录的基准位置,因此,总的比较次数为:
因此,时间复杂度为O(n2)。
在平均情况下,设基准记录的关键码第k小(1≤k≤n),则有:
这是快速排序的平均时间性能,可以用归纳法证明,其数量级也为O(nlog2n)。
4.4组合问题中的分治法
4.4.1最大子段和问题
给定由n个整数组成的序列(a1,a2,…,an),最大子段和问题要求该序列形如的最大值(1≤i≤j≤n),当序列中所有整数均为负整数时,其最大子段和为0。
例如,序列(-20,11,-4,13,-5,-2)的最大子段和为:
最大子段和问题的分治策略是:
(1)划分:
按照平衡子问题的原则,将序列(a1,a2,…,an)划分成长度相同的两个子序列(a1,…,a)和(a+1,…,an),则会出现以下三种情况:
①a1,…,an的最大子段和=a1,…,a的最大子段和;
②a1,…,an的最大子段和=a+1,…,an的最大子段和;
③a1,…,an的最大子段和=,且
(2)求解子问题:
对于划分阶段的情况①和②可递归求解,情况③需要分别计算
则s1+s2为情况③的最大子段和。
(3)合并:
比较在划分阶段的三种情况下的最大子段和,取三者之中的较大者为原问题的解。
算法4.7——最大子段和问题
intMaxSum(inta[],intleft,intright)
{
sum=0;
if(left==right){//如果序列长度为1,直接求解
if(a[left]>0)sum=a[left];
elsesum=0;
}
else{
center=(left+right)/2;//划分
leftsum=MaxSum(a,left,center);
//对应情况①,递归求解
rightsum=MaxSum(a,center+1,right);
//对应情况②,递归求解
s1=0;lefts=0;?
//以下对应情况③,先求解s1
for(i=center;i>=left;i--)
{
lefts+=a[i];
if(lefts>s1)s1=lefts;
}
s2=0;rights=0;//再求解s2
for(j=center+1;j<=right;j++)
{
rights+=a[j];
if(rights>s2)s2=rights;
}
sum=s1+s2;//计算情况③的最大子段和
if(sum//合并,在sum、leftsum和rightsum中取较大者
if(sum}
returnsum;
}
分析算法4.7的时间性能,对应划分得到的情况①和②,需要分别递归求解,对应情况③,两个并列for循环的时间复杂性是O(n),所以,存在如下递推式:
根据1.2.4节主定理,算法4.7的时间复杂性为O(nlog2n)。
4.4.2棋盘覆盖问题
在一个2k×2k(k≥0)个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为特殊方格。
棋盘覆盖问题要求用图4.11(b)所示的4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
分治法求解棋盘覆盖问题的技巧在于划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个特殊方格,从而将原问题分解为规模较小的棋盘覆盖问题。
k>0时,可将2k×2k的棋盘划分为4个2k-1×2k-1的子棋盘,这样划分后,由于原棋盘只有一个特殊方格,所以,这4个子棋盘中只有一个子棋盘包含该特殊方格,其余3个子棋盘中没有特殊方格。
为了将这3个没有特殊方格的子棋盘转化为特殊棋盘,以便采用递归方法求解,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。
递归地使用这种划分策略,直至将棋盘分割为1×1的子棋盘。
下面讨论棋盘覆盖问题中数据结构的设计。
(1)棋盘:
可以用一个二维数组board[size][size]表示一个棋盘,其中,size=2k。
为了在递归处理的过程中使用同一个棋盘,将数组board设为全局变量;
(2)子棋盘:
整个棋盘用二维数组board[size][size]表示,其中的子棋盘由棋盘左上角的下标tr、tc和棋盘大小s表示;
(3)特殊方格:
用board[dr][dc]表示特殊方格,dr和dc是该特殊方格在二维数组board中的下标;
(4)L型骨牌:
一个2k×2k的棋盘中有一个特殊方格,所以,用到L型骨牌的个数为(4k-1)/3,将所有L型骨牌从1开始连续编号,用一个全局变量t表示。
算法4.8——棋盘覆盖
voidChessBoard(inttr,inttc,intdr,intdc,intsize)
//tr和tc是棋盘左上角的下标,dr和dc是特殊方格的下标,
//size是棋盘的大小,t已初始化为0
{
if(size==1)return;//棋盘只有一个方格且是特殊方格
t++;//L型骨牌号
s=size/2;//划分棋盘
//覆盖左上角子棋盘
if(dr
ChessBoard(tr,tc,dr,dc,s);//递归处理子棋盘
else{//用t号L型骨牌覆盖右下角,再递归处理子棋盘
board[tr+s-1][tc+s-1]=t;
ChessBoard(tr,tc,tr+s-1,tc+s-1,s);
}
//覆盖右上角子棋盘
if(dr
|
=tc+s)//特殊方格在右上角子棋盘中ChessBoard(tr,tc+s,dr,dc,s);//递归处理子棋盘
else{//用t号L型骨牌覆盖左下角,再递归处理子棋盘
board[tr+s-1][tc+s]=t;
ChessBoard(tr,tc+s,tr+s-1,tc+s,s);}
//覆盖左下角子棋盘
if(dr>=tr+s&&dcChessBoard(tr+s,tc,dr,dc,s);//递归处理子棋盘
else{//用t号L型骨牌覆盖右上角,再递归处理子棋盘
board[tr+s][tc+s-1]=t;
ChessBoard(tr+s,tc,tr+s,tc+s-1,s);}
//覆盖右下角子棋盘
if(dr>=tr+s
展开阅读全文
相关搜索