分治法所能解决的问题一般具有以下几个特征:(1)该问题的规模缩小到一定的程度就可以容易地解决;
(2)该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质; (3)利用该问题分解出的子问题的解可以合并为该问题的解;
(4)该问题所分解出的各个子问题是相互的,即子问题之间不包含公共的子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加; 第二条特征是应用分治法的前提,它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。 3.分治法的基本步骤
分治法在每一层递归上都有三个步骤:
(1)分解:将原问题分解为若干个规模较小,相互,与原问题形式相同的子问题; (2)求解:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题; (3)合并:将各个子问题的解合并为原问题的解。 它的一般的算法设计模式如下: Divide_and_Conquer(P)
if |P|≤n0
then return(ADHOC(P))
将P分解为较小的子问题P1、P2、„、Pk for i←1 to k do
yi ← Divide-and-Conquer(Pi) △ 递归解决Pi T ← MERGE(y1,y2,„,yk) △ 合并子问题 Return(T)
其中 |P| 表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时,直接用算法ADHOC(P)求解。
算法MERGE(y1,y2,„,yk)是该分治法中的合并子算法,用于将P的子问题P1、P2、„、Pk的相应的解y1、y2、„、yk合并为P的解。
共17页 第 10 页
软件设计师(原高级程序员)复习资料
根据分治法的分割原则,原问题应该分为多少个子问题才较适宜?各个子问题的规模应该怎样才为适当?这些问题很难予以肯定的回答。但人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。换句话说,将一个问题分成大小相等的k个子问题的处理方法是行之有效的。许多问题可以取k=2。这种使子问题规模大致相等的做法是出自一种平衡子问题的思想,它几乎总是比子问题规模不等的做法要好。
分治法的合并步骤是算法的关键所在。有些问题的合并方法比较明显,有些问题合并方法比较复杂,或者是有多种合并方案;或者是合并方案不明显。究竟应该怎样合并,没有统一的模式,需要具体问题具体分析。
【问题】循环赛日程表
k
问题描述:设有n=2个运动员要进行网球循环赛。设计一个满足以下要求的比赛日程表: (1)每个选手必须与其他n-1个选手各赛一次; (2)每个选手一天只能参赛一次; (3)循环赛在n-1天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。
按分治策略,我们可以将所有的选手分为两半,则n个选手的比赛日程表可以通过n/2个选手的比赛日程表来决定。递归地用这种一分为二的策略对选手进行划分,直到只剩下两个选手时,比赛日程表的制定就变得很简单。这时只要让这两个选手进行比赛就可以了。
1 2
1 2 1
1 2 3 4
1 2 1 4 3
2 3 4 1 2
3 4 3 2 1
1 2 3 4 5 6 7 8 1 2 1 4 3 6 5 8 7 2 3 4 1 2 7 8 5 6 3 4 3 2 1 8 7 6 5 4 5 6 7 8 1 2 3 4 5 6 7 8 5 4 1 2 3 6 7 8 5 6 3 4 1 2 7 8 5 6 7 2 3 4 1 (1) (2) (3) 图表示 2个、4个和8个选手的比赛日程表
图1所列出的正方形表(3)是8个选手的比赛日程表。其中左上角与左下角的两小块分别为选手1至选手4和选手5至选手8前3天的比赛日程。据此,将左上角小块中的所有数字按其相对位置抄到右下角,又将左下角小块中的所有数字按其相对位置抄到右上角,这样我们就分别安排好了选手1至选手4和选手5至选手8在后4天的比赛日程。依此思想容易将这个比赛日程表推广到具有任意多个选手的情形。 1.7 动态规划法
经常会遇到复杂问题不能简单地分解成几个子问题,而会分解出一系列的子问题。简单地采用把大问题分解成子问题,并综合子问题的解导出大问题的解的方法,问题求解耗时会按问题规模呈幂级数增加。 为了节约重复求相同子问题的时间,引入一个数组,不管它们是否对最终解有用,把所有子问题的解存于该数组中,这就是动态规划法所采用的基本方法。以下先用实例说明动态规划方法的使用。 ◆动态规划的适用条件
任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。 (1)最优化原理(最优子结构性质)
最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
共17页 第 11 页
软件设计师(原高级程序员)复习资料
图2
例如图2中,若路线I和J是A到C的最优路径,则根据最优化原理,路线J必是从B到C的最优路线。这可用反证法证明:假设有另一路径J’是B到C的最优路径,则A到C的路线取I和J’比I和J更优,矛盾。从而证明J’必是B到C的最优路径。
最优化原理是动态规划的基础,任何问题,如果失去了最优化原理的支持,就不可能用动态规划方法计算。根据最优化原理导出的动态规划基本方程是解决一切动态规划问题的基本方法。 (2)无后向性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。 (3)子问题的重叠性
动态规划算法的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。选择动态规划算法是因为动态规划算法在空间上可以承受,而搜索算法在时间上却无法承受,所以我们舍空间而取时间。
所以,能够用动态规划解决的问题还有一个显著特征:子问题的重叠性。这个性质并不是动态规划适用的必要条件,但是如果该性质无法满足,动态规划算法同其他算法相比就不具备优势。 ◆动态规划的基本思想
前文主要介绍了动态规划的一些理论依据,我们将前文所说的具有明显的阶段划分和状态转移方程的动态规划称为标准动态规划,这种标准动态规划是在研究多阶段决策问题时推导出来的,具有严格的数学形式,适合用于理论上的分析。在实际应用中,许多问题的阶段划分并不明显,这时如果刻意地划分阶段法反而麻烦。一般来说,只要该问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解(即满足最优子化原理),则可以考虑用动态规划解决。
动态规划的实质是分治思想和解决冗余,因此,动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略。
由此可知,动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。
贪心法的当前选择可能要依赖已经作出的所有选择,但不依赖于有待于做出的选择和子问题。因此贪心法自顶向下,一步一步地作出贪心选择;
而分治法中的各个子问题是的(即不包含公共的子问题),因此一旦递归地求出各子问题的解后,便可自下而上地将子问题的解合并成问题的解。
不足之处:如果当前选择可能要依赖子问题的解时,则难以通过局部的贪心策略达到全局最优解;如果各子问题是不的,则分治法要做许多不必要的工作,重复地解公共的子问题。
解决上述问题的办法是利用动态规划。该方法主要应用于最优化问题,这类问题会有多种可能的解,每个解都有一个值,而动态规划找出其中最优(最大或最小)值的解。若存在若干个取最优值的解的话,它只取其中的一个。在求解过程中,该方法也是通过求解局部子问题的解达到全局最优解,但与分治法和贪心法不同的是,动态规划允许这些子问题不,(亦即各子问题可包含公共的子问题)也允许其通过自身子问题的解作出选择,该方法对每一个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。
因此,动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。动态规划法的关键就在于,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。 3、动态规划算法的基本步骤
设计一个标准的动态规划算法,通常可按以下几个步骤进行:
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。注意这若干个阶段一定要是有序的或者是可排序的(即无后向性),否则问题就无法用动态规划求解。
(2)选择状态:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:之所以把这两步放在一起,是因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以,如果我们确定了决策,状态转移方程也就写出来了。但事实上,我们常常是反过来做,根据相邻两段的各状态之间的关系来确定决策。 (4)写出规划方程(包括边界条件):动态规划的基本方程是规划方程的通用形式化表达式。
共17页 第 12 页
软件设计师(原高级程序员)复习资料
一般说来,只要阶段、状态、决策和状态转移确定了,这一步还是比较简单的。动态规划的主要难点在于理论上的设计,一旦设计完成,实现部分就会非常简单。根据动态规划的基本方程可以直接递归计算最优值,但是一般将其改为递推计算,实现的大体上的框架如下: 标准动态规划的基本框架
1. 对fn+1(xn+1)初始化; {边界条件} for k:=n downto 1 do for 每一个xk∈Xk do for 每一个uk∈Uk(xk) do begin
fk(xk):=一个极值; {∞或-∞} xk+1:=Tk(xk,uk); {状态转移方程}
t:=φ(fk+1(xk+1),vk(xk,uk)); {基本方程(9)式}
if t比fk(xk)更优 then fk(xk):=t; {计算fk(xk)的最优值} end;
t:=一个极值; {∞或-∞} for 每一个x1∈X1 do
if f1(x1)比t更优 then t:=f1(x1); {按照10式求出最优指标} 输出t;
但是,实际应用当中经常不显式地按照上面步骤设计动态规划,而是按以下几个步骤进行: (1)分析最优解的性质,并刻划其结构特征。 (2)递归地定义最优值。
(3)以自底向上的方式或自顶向下的记忆化方法(备忘录法)计算出最优值。 (4)根据计算最优值时得到的信息,构造一个最优解。
步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省略,若需要求出问题的一个最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速地构造出一个最优解。
总结:动态规划实际上就是最优化的问题,是指将原问题的大实例等价于同一最优化问题的较小实例,自底向上的求解最小实例,并将所求解存放起来,存放的结果就是为了准备数据。与递归相比,递归是不断的调用子程序求解,是自顶向下的调用和求解。 【问题】凸多边形的最优三角剖分问题
问题描述:多边形是平面上一条分段线性的闭曲线。也就是说,多边形是由一系列首尾相接的直线段组成的。组成多边形的各直线段称为该多边形的边。多边形相接两条边的连接点称为多边形的顶点。若多边形的边之间除了连接顶点外没有别的公共点,则称该多边形为简单多边形。一个简单多边形将平面分为3个部分:被包围在多边形内的所有点构成了多边形的内部;多边形本身构成多边形的边界;而平面上其余的点构成了多边形的外部。当一个简单多边形及其内部构成一个闭凸集时,称该简单多边形为凸多边形。也就是说凸多边形边界上或内部的任意两点所连成的直线段上所有的点均在该凸多边形的内部或边界上。 通常,用多边形顶点的逆时针序列来表示一个凸多边形,即P=表示具有n条边v0v1,v1v2,„,vn-1vn的一个凸多边形,其中,约定v0=vn 。若vi与vj是多边形上不相邻的两个顶点,则线段vivj称为多边形的一条弦。弦将多边形分割成凸的两个子多边形和。多边形的三角剖分是一个将多边形分割成互不重迭的三角形的弦的集合T。图1是一个凸多边形的两个不同的三角剖分。(a)
(b)
图1 一个凸多边形的2个不同的三角剖分
在凸多边形P的一个三角剖分T中,各弦互不相交且弦数已达到最大,即P的任一不在T中的弦必与T中某一弦相交。在一个有n个顶点的凸多边形的三角刮分中,恰好有n-3条弦和n-2个三角形。
凸多边形最优三角剖分的问题是:给定一个凸多边形P=以及定义在由多边形的边和弦组成的三角形上的权函数ω。要求确定该凸多边形的一个三角剖分,使得该三角剖分对应的权即剖分中诸三角形上的权之和为最小。共17页 第 13 页
软件设计师(原高级程序员)复习资料
可以定义三角形上各种各样的权函数ω。例如:定义ω(△vivjvk)=| vivj |+| vivk |+| vkvj |,其中,| vivj |是点vi到vj的欧氏距离。相应于此权函数的最优三角剖分即为最小弦长三角剖分。 (1)最优子结构性质
凸多边形的最优三角剖分问题有最优子结构性质。事实上,若凸(n+1)边形P=的一个最优三角剖分T包含三角形v0vkvn,1≤k≤n-1,则T的权为3个部分权的和,即三角形v0vkvn的权,子多边形的权和的权之和。可以断言由T所确定的这两个子多边形的三角剖分也是最优的,因为若有或的更小权的三角剖分,将会导致T不是最优三角剖分的矛盾。(2)最优三角剖分对应的权的递归结构
首先,定义t[i,j](1≤i的最优三角剖分所对应的权值,即最优值。为方便起见,设退化的多边形具有权值0。据此定义,要计算的凸(n+1)边多边形P对应的权的最优值为t[1,n]。t[i,j]的值可以利用最优子结构性质递归地计算。由于退化的2顶点多边形的权值为0,所以t[i,i]=0,i=1,2,„,n 。当j一i≥1时,子多边形至少有3个顶点。由最优于结构性质,t[i,j]的值应为t[i,k]的值加上t[k+1,j]的值,再加上△vi-1vkvj的权值,并在i≤k≤j-1的范围内取最小。由此,t[i,j]可递归地定义为:(3)计算最优值
下面描述的计算凸(n+1)边形P=的三角剖分最优权值的动态规划算法MINIMUM_WEIGHT,输入是凸多边形P=的权函数ω,输出是最优值t[i,j]和使得t[i,k]+t[k+1,j]+ω(△vi-1vkvj)达到最优的位置(k=)s[i,j],1≤i≤j≤n 。 Procedure MINIMUM_WEIGHT(P,w); Beginn=length[p]-1; for i=1 to n do
t[i,i]:=0; for ll=2 to n do
for i=1 to n-ll+1 do begin
j=i+ll-1; t[i,j]=∞;
for k=i to j-1 do begin
q=t[i,k]+t[k+1,j]+ω(△vi-1vkvj);
if qt[i,j]=q; s[i,j]=k; end;end;
end;
return(t,s); end;
23
算法MINIMUM_WEIGHT_占用θ(n)空间,耗时θ(n)。 (4)构造最优三角剖分
如我们所看到的,对于任意的1≤i≤j≤n ,算法MINIMUM_WEIGHT在计算每一个子多边形的最优三角剖分所对应的权值t[i,j]的同时,还在s[i,j]中记录了此最优三角剖分中与边(或弦)vi-1vj构成的三角形的第三个顶点的位置。因此,利用最优子结构性质并借助于s[i,j],1≤i≤j≤n ,凸(n+l)边形P=的最优三角剖分可容易地在Ο(n)时间内构造出来。 1.8 回溯法回溯法也称为试探法,该方法首先暂时放弃关于问题规模大小的,并将问题的候选解按某种顺序逐一枚举和检验。当发现当前候选解不可能是解时,就选择下一个候选解;倘若当前候选解除了还不满足问题规模要求外,满足所有其他要求时,继续扩大当前候选解的规模,并继续试探。如果当前候选解满足
共17页 第 14 页
软件设计师(原高级程序员)复习资料
包括问题规模在内的所有要求时,该候选解就是问题的一个解。在回溯法中,放弃当前候选解,寻找下一个候选解的过程称为回溯。扩大当前候选解的规模,以继续试探的过程称为向前试探。 1、回溯法的一般描述
可用回溯法求解的问题P,通常要能表达为:对于已知的由n元组(x1,x2,„,xn)组成的一个状态空间E={(x1,x2,„,xn)∣xi∈Si ,i=1,2,„,n},给定关于n元组中的一个分量的一个约束集D,要求E中满足D的全部约束条件的所有n元组。其中Si是分量xi的定义域,且 |Si| 有限,i=1,2,„,n。我们称E中满足D的全部约束条件的任一n元组为问题P的一个解。
解问题P的最朴素的方法就是枚举法,即对E中的所有n元组逐一地检测其是否满足D的全部约束,若满足,则为问题P的一个解。但显然,其计算量是相当大的。
我们发现,对于许多问题,所给定的约束集D具有完备性,即i元组(x1,x2,„,xi)满足D中仅涉及到x1,x2,„,xi的所有约束意味着j(jj。因此,对于约束集D具有完备性的问题P,一旦检测断定某个j元组(x1,x2,„,xj)违反D中仅涉及x1,x2,„,xj的一个约束,就可以肯定,以(x1,x2,„,xj)为前缀的任何n元组(x1,x2,„,xj,xj+1,„,xn)都不会是问题P的解,因而就不必去搜索它们、检测它们。回溯法正是针对这类问题,利用这类问题的上述性质而提出来的比枚举法效率更高的算法。
回溯法首先将问题P的n元组的状态空间E表示成一棵高为n的带权有序树T,把在E中求问题P的所有解转化为在T中搜索问题P的所有解。树T类似于检索树,它可以这样构造:
(1)(2)(mi-1)
设Si中的元素可排成xi ,xi ,„,xi ,|Si| =mi,i=1,2,„,n。从根开始,让T的第I层
(1)(2)
的每一个结点都有mi个儿子。这mi个儿子到它们的双亲的边,按从左到右的次序,分别带权xi+1 ,xi+1 ,„,(mi)
xi+1 ,i=0,1,2,„,n-1。照这种构造方式,E中的一个n元组(x1,x2,„,xn)对应于T中的一个叶子结点,T的根到这个叶子结点的路径上依次的n条边的权分别为x1,x2,„,xn,反之亦然。另外,对于任意的0≤i≤n-1,E中n元组(x1,x2,„,xn)的一个前缀I元组(x1,x2,„,xi)对应于T中的一个非叶子结点,T的根到这个非叶子结点的路径上依次的I条边的权分别为x1,x2,„,xi,反之亦然。特别,E中的任意一个n元组的空前缀(),对应于T的根。
因而,在E中寻找问题P的一个解等价于在T中搜索一个叶子结点,要求从T的根到该叶子结点的路径上依次的n条边相应带的n个权x1,x2,„,xn满足约束集D的全部约束。在T中搜索所要求的叶子结点,很自然的一种方式是从根出发,按深度优先的策略逐步深入,即依次搜索满足约束条件的前缀1元组(x1i)、前缀2元组(x1,x2)、„,前缀I元组(x1,x2,„,xi),„,直到i=n为止。
在回溯法中,上述引入的树被称为问题P的状态空间树;树T上任意一个结点被称为问题P的状态结点;树T上的任意一个叶子结点被称为问题P的一个解状态结点;树T上满足约束集D的全部约束的任意一个叶子结点被称为问题P的一个回答状态结点,它对应于问题P的一个解。 1.9 分支定界法:
分支限界法:
这是一种用于求解组合优化问题的排除非解的搜索算法。类似于回溯法,分枝定界法在搜索解空间时,也经常使用树形结构来组织解空间。然而与回溯法不同的是,回溯算法使用深度优先方法搜索树结构,而分枝定界一般用宽度优先或最小耗费方法来搜索这些树。因此,可以很容易比较回溯法与分枝定界法的异同。相对而言,分枝定界算法的解空间比回溯法大得多,因此当内存容量有限时,回溯法成功的可能性更大。
算法思想:分枝定界(branch and bound)是另一种系统地搜索解空间的方法,它与回溯法的主要区别在于对E-节点的扩充方式。每个活节点有且仅有一次机会变成E-节点。当一个节点变为E-节点时,则生成从该节点移动一步即可到达的所有新节点。在生成的节点中,抛弃那些不可能导出(最优)可行解的节点,其余节点加入活节点表,然后从表中选择一个节点作为下一个E-节点。从活节点表中取出所选择的节点并进行扩充,直到找到解或活动表为空,扩充过程才结束。
有两种常用的方法可用来选择下一个E-节点(虽然也可能存在其他的方法):
1) 先进先出(F I F O) 即从活节点表中取出节点的顺序与加入节点的顺序相同,因此活 节点表的性质与队列相同。
2) 最小耗费或最大收益法在这种模式中,每个节点都有一个对应的耗费或收益。如果查找 一个具有最小耗费的解,则活节点表可用最小堆来建立,下一个E-节点就是具有最小耗费 的活节点;如果希望搜索一个具有最大收益的解,则可用最大堆来构造活节点表,下一个 E-节点是具有最大收益的活节点。 2.几个重要的算法程序
共17页 第 15 页
软件设计师(原高级程序员)复习资料
2.1 堆排序
堆排序也是选择排序的一种,其特点是,在以后各趟的“选择”中利用在第一趟选择中已经得到的关键字比较的结果。
堆的定义: 堆是满足下列性质的数列{r1, r2, „,rn}: 或 若将此数列看成是一棵完全二叉树,则堆或是空树或是满足下列特性的完全二叉树:其左、右子树分别是堆,并且当左/右子树不空时,根结点的值小于(或大于)左/右子树根结点的值。
由此,若上述数列是堆,则r1必是数列中的最小值或最大值,分别称作小顶堆或大顶堆。
堆排序即是利用堆的特性对记录序列进行排序的一种排序方法。具体作法是:先建一个“大顶堆”,即先选得一个关键字为最大的记录,然后与序列中最后一个记录交换,之后继续对序列中前n-1记录进行“筛选”,重新将它调整为一个“大顶堆”,再将堆顶记录和第n-1个记录交换,如此反复直至排序结束。 所谓“筛选”指的是,对一棵左/右子树均为堆的完全二叉树,“调整”根结点使整个二叉树为堆。 堆排序的算法如下所示: template
void HeapSort ( Elem R[], int n ) { // 对记录序列R[1..n]进行堆排序。 for ( i=n/2; i>0; --i )
// 把R[1..n]建成大顶堆 HeapAdjust ( R, i, n ); for ( i=n; i>1; --i ) { R[1]←→R;
// 将堆顶记录和当前未经排序子序列 // R[1..i]中最后一个记录相互交换 HeapAdjust(R, 1, i-1);
// 将R[1..i-1] 重新调整为大顶堆 }
} // HeapSort
其中筛选的算法如下所示。为将R[s..m]调整为“大顶堆”,算法中“筛选”应沿关键字较大的孩子结点向下进行。 Template
void HeapAdjust (Elem R[], int s, int m) { // 已知R[s..m]中记录的关键字除R[s].key之 // 外均满足堆的定义,本函数调整R[s] 的关 // 键字,使R[s..m]成为一个大顶堆(对其中 // 记录的关键字而言) rc = R[s];
for ( j=2*s; j<=m; j*=2 ) {// 沿key较大的孩子结点向下筛选 if ( j if ( rc.key >= R[j].key ) break; // rc应插入在位置s上 R[s] = R[j]; s = j; }
R[s] = rc; // 插入 } // HeapAdjust
堆排序的时间复杂度分析:
1. 对深度为k的堆,“筛选”所需进行的关键字比较的次数至多为2(k-1);
2.对n个关键字,建成深度为+1)log2nh(=的堆,所需进行的关键字比较的次数至多为4n; 3. 调整“堆顶”n-1次,总共进行的关键字比较的次数不超过 +2(log2(n-1) + …+log22)log2(n-2)归并排序:是通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度;归并排序的基本思想是:将两个或两个以上的有序子序列“归并”为一个有序序列。在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的有序子序列 归并为一个有序序列。 “归并”算法描述如下: template
void Merge (Elem SR[], Elem TR[], int i, int m, int n) { // 将有序的SR[i..m]和SR[m+1..n]归并为 // 有序的TR[i..n]
for (j=m+1, k=i; i<=m && j<=n; ++k) { // 将SR中记录由小到大地并入TR if (SR.key<=SR[j].key) TR[k] = SR[i++]; else TR[k] = SR[j++];
共17页 第 16 页
软件设计师(原高级程序员)复习资料
}
if (i<=m) TR[k..n] = SR[i..m];
// 将剩余的SR[i..m]复制到TR if (j<=n) TR[k..n] = SR[j..n];
// 将剩余的SR[j..n]复制到TR } // Merge
归并排序的算法可以有两种形式:递归的和递推的,它是由两种不同的程序设计思想得出的。在此,只讨论递归形式的算法。
这是一种自顶向下的分析方法:
如果记录无序序列R[s..t]的两部分](s+t)/2R[s..和(s+t)/2+1..tR[分别按关键字有序,则利用上述归并算法很容易将它们归并成整个记录序列是一个有序序列,由此,应该先分别对这两部分进行2-路归并排序。 template
void Msort ( Elem SR[], Elem TR1[], int s, int t ) { // 将SR[s..t]进行2-路归并排序为TR1[s..t]。 if (s==t) TR1[s] = SR[s]; else {
m = (s+t)/2;
// 将SR[s..t]平分为SR[s..m]和SR[m+1..t] Msort (SR, TR2, s, m);
// 递归地将SR[s..m]归并为有序的TR2[s..m] Msort (SR, TR2, m+1, t);
//递归地SR[m+1..t]归并为有序的TR2[m+1..t] Merge (TR2, TR1, s, m, t);
// 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] }
} // MSort
template
void MergeSort (Elem R[]) {
// 对记录序列R[1..n]作2-路归并排序。 MSort(R, R, 1, n); } // MergeSort
容易看出,对n个记录进行归并排序的时间复杂度为Ο(nlogn)。即:每一趟归并的时间复杂度为O(n),总共需进行logn趟。
下面我们比较一下上面谈到的各种内部排序方法 首先,从时间性能上说:
1. 按平均的时间性能来分,有三类排序方法:
时间复杂度为O(nlogn)的方法有:快速排序、堆排序和归并排序,其中以快速排序为最好;
时间复杂度为O(n2)的有:直接插入排序、起泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此; 时间复杂度为O(n)的排序方法只有,基数排序。
2. 当待排记录序列按关键字顺序有序时,直接插入排序和起泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能蜕化为O(n2),因此是应该尽量避免的情况。 3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。 其次,从空间性能上说:
指的是排序过程中所需的辅助空间大小。
1. 所有的简单排序方法(包括:直接插入、起泡和简单选择)和堆排序的空间复杂度为O(1); 2. 快速排序为O(logn),为栈所需的辅助空间;
3. 归并排序所需辅助空间最多,其空间复杂度为O(n);
4. 链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)。 再次,从排序方法的稳定性能上说:
稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。对于不稳定的排序方法,只要能举出一个实例说明即可。我们需要指出的是:快速排序和堆排序是不稳定的排序方法。
共17页 第 17 页