版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
递归6.1递归的概念6.2递归算法的执行过程6.3递归算法的设计方法6.4递归过程和运行时栈6.5递归算法的效率分析*6.6递归算法到非递归算法的转换6.7设计举例6.8本章小结和前几章讨论的内容不同,递归不是一种数据结构,而是一种有效的算法设计方法。存在自调用的算法称作递归算法。本章主要介绍递归的概念、递归算法的执行过程、递归算法的设计方法以及递归算法的效率。递归算法是解决许多复杂应用问题的重要方法。
通常的算法中不存在算法调用自己本身的情况,但是,在如下两种情况下存在算法调用自己本身的情况。6.1递归的概念
1.问题的定义是递推的
许多数学概念是递推定义的。阶乘函数的常见定义是:
(6-1)
显然,这是一个循环过程定义,一旦n给定,我们就可由这个循环过程定义得出n!。例如n=4,则有4!=4×3×2×1。但是,阶乘函数也可定义为:
(6-2)
这种定义方法是用阶乘函数自己本身定义了阶乘函数,我们称式(6-2)是阶乘函数的递推定义式。
另一个递推定义的例子是斐波那契数列。斐波那契数列的递推定义是:
(6-3)
2.问题的解法存在自调用
有些问题的解法存在自调用。一个典型的例子是在有序数组中查找一个数据元素是否存在的折半查找算法。
在有序数组a中查找一个数据元素x是否存在的折半查找算法思想是:设有序数组a中的数据元素为由小到大的正序,设有序数组a的下界下标为low,上界下标为high。有序数组a中是否存在数据元素x可用如下方法查找:首先计算出数组a的中间位置下标mid,有mid=(low+high)/2(注:C语言中整数除以整数时商只取整数部分,如7/2=3)。
然后比较x和a[mid],若x=a[mid],则查找成功;若x<a[mid],则随后调用算法自身在下界下标为low,上界下标为mid-1的区间继续查找;若x>a[mid],则随后调用算法自身在下界下标为mid+1,上界下标为high的区间继续查找。
图6-1是折半查找算法查找过程的示例。其中,有序数组a中的数据元素为{1,3,4,5,17,18,31,33},初始下界下标low=0,初始上界下标high=7,待查元素x=17。
图6-1折半查找过程上述两种情况下都存在算法调用自己本身的情况。
定义:若一个算法直接地或间接地调用自己本身,则称这个算法是递归算法,简称递归。
递归算法用把问题分解为形式更加简单的子问题的方法来求解问题。递归算法既是一种有效的分析问题的方法,也是一种有效的算法设计方法。递归算法是解决许多复杂应用问题的重要方法。
【例6-1】给出按照公式(6-2)计算阶乘函数的递归算法,并给出n=3时递归算法的执行过程。
设计按照公式(6-2)计算阶乘函数的递归算法如下:longintFact(intn)
{
intx;
longinty;6.2递归算法的执行过程
if(n<0)/*n<0时阶乘无定义*/
{
printf("参数错!");
return-1;
}
if(n==0)return1;
else
{
y=Fact(n-1);/*递归调用*/
returnn*y;
}
}为说明该递归算法的执行过程,设计主函数如下:main(void)
{
longintfn;
fn=Fact(3);
}主函数用实参n=3调用了递归算法Fact(3),而Fact(3)要通过调用Fact(2),Fact(2)要通过调用Fact(1),Fact(1)要通过调用Fact(0)来得出计算结果。Fact(3)的调用过程和结果计算过程如图6-2所示,其中,实箭头表示函数调用,虚箭头表示函数的返回值。
图6-2Fact(3)的递归调用过程【例6-1】给出在有序数组a中查找数据元素x是否存在的递归算法,并给出如图6-1所示实际数据的递归算法的执行过程。
设计算法的参数为:有序数组a,要查找的数据元素x,数组下界下标low,数组上界下标high。当在数组a中查找到数据元素x时函数返回数组a的下标;当在数组a中查找不到数据元素x时函数返回-1。
递归算法如下:intBSearch(inta[],intx,intlow,inthigh)
{
intmid;
if(low>high)return-1;/*查找不成功*/
mid=(low+high)/2;
if(x==a[mid])returnmid;/*查找成功*/
elseif(x<a[mid])
returnBSearch(a,x,low,mid-1);/*在下半区查找*/
else
returnBSearch(a,x,mid+1,high);/*在上半区查找*/
}主函数设计如下:#include<stdio.h>
main(void)
{
inta[]={1,3,4,5,17,18,31,33};
intx=17;
intbn;
bn=BSearch(a,x,0,7);
if(bn==-1)printf("x不在数组a中");
elseprintf("x在数组a的下标%d中",bn);
}BSearch(a,x,0,7)的递归调用过程如图6-3所示,其中,实箭头表示函数调用,虚箭头表示函数的返回值。
图6-3BSearch(a,x,0,7)的递归调用过程图6-1给出的数据是查找成功的例子。对于查找不成功的例子,如要查找的数据元素x=16,此时查找过程和x=17的查找过程类同,只是最后一次x=16和a[4]=17比较后,因x<a[4],所以要再进行一次递归调用BSearch(a,x,low,mid-1),此时实参为low=4,high=3。数组的查找区间下界下标为4,上界下标为3,说明该数组的元素个数为0。因此,查找不成功的判断条件是low>high。
递归算法就是算法中有直接或间接调用算法本身的算法。递归算法既是一种有效的算法设计方法,也是一种有效的分析问题的方法。递归算法的求解问题的基本思想是:对于一个较为复杂的问题,把原问题分解成若干个相对简单且类同的子问题,6.3递归算法的设计方法
这样较为复杂的原问题就变成了相对简单的子问题;而简单到一定程度的子问题可以直接求解;这样,原问题就可递推得到解。
并不是每个问题都适宜于用递归算法求解。适宜于用递归算法求解的问题的充分必要条件是:
(1)问题具有某种可借用的类同自身的子问题描述的性质。
(2)某一有限步的子问题(也称作本原问题)有直接的解存在。
当一个问题存在上述两个基本要素时,设计该问题的递归算法的方法是:
(1)把对原问题的求解设计成包含有对子问题求解的形式。
(2)设计递归出口。
下面的例子具体示范了问题的递归算法设计方法。
设计模拟汉诺塔问题求解过程的算法。汉诺塔问题的描述是:设有3根标号为A,B,C的柱子,在A柱上放着n个盘子,每一个都比下面的略小一点,要求把A柱上的盘子全部移到C柱上,移动的规则是:
(1)一次只能移动一个盘子;
(2)移动过程中大盘子不能放在小盘子上面;
(3)在移动过程中盘子可以放在A,B,C的任意一个柱子上。
设计问题分析:可以用递归方法求解n个盘子的汉诺塔问题。基本思想是:1个盘子的汉诺塔问题可直接移动。n个盘子的汉诺塔问题可递归表示为,首先把上边的n-1个盘子从A柱移到B柱,然后把最下边的一个盘子从A柱移到C柱,最后把移到B柱的n-1个盘子再移到C柱。4个盘子汉诺塔问题的递归求解示意图如图6-4所示。
图6-4汉诺塔问题的递归求解示意图算法设计:首先,盘子的个数n是必需的一个输入参数,对n个盘子,我们可从上至下依次编号为1,2,…,n;其次,输入参数还需有3个柱子的代号,我们令3个柱子的参数名分别为fromPeg,auxPeg和toPeg;最后,汉诺塔问题的求解是一个处理过程,因此算法的输出是n个盘子从柱子fromPeg借助柱子auxPeg移动到柱子toPeg的移动步骤,我们设计每一步的移动为屏幕显示如下形式的信息:
MoveDiskifromPegXtoPegY
这样,汉诺塔问题的递归算法可设计如下:voidtowers(intn,charfromPeg,chartoPeg,charauxPeg)
{
if(n==1)/*递归出口*/
{
printf("%s%c%s%c\n","movedisk1frompeg",
fromPeg,"topeg",toPeg);
return;
}
/*把n-1个圆盘从fromPeg借助toPeg移至auxPeg*/
towers(n-1,fromPeg,auxPeg,toPeg);
/*把圆盘n由fromPeg直接移至toPeg*/
printf("%s%d%s%c%s%c\n","movedisk",n,
"frompeg",fromPeg,"topeg",toPeg);
/*把n-1个圆盘从auxPeg借助fromPeg移至toPeg*/
towers(n-1,auxPeg,toPeg,fromPeg);
}设计一个测试主函数如下:#include<stdio.h>
voidmain(void)
{
Towers(4,'A','C','B');
}程序运行的输出信息如下:MoveDisk1fromPegAtoPegB
MoveDisk2fromPegAtoPegC
MoveDisk1fromPegBtoPegC
MoveDisk3fromPegAtoPegB
MoveDisk1fromPegCtoPegA
MoveDisk2fromPegCtoPegB
MoveDisk1fromPegAtoPegB
MoveDisk4fromPegAtoPegC
MoveDisk1fromPegBtoPegC
MoveDisk2fromPegBtoPegA
MoveDisk1fromPegCtoPegA
MoveDisk3fromPegBtoPegC
MoveDisk1fromPegAtoPegB
MoveDisk2fromPegAtoPegC
MoveDisk1fromPegBtoPegC从程序的运行输出信息可见,上述算法实现了递归求解汉诺塔问题。递归算法把移动n个盘子的汉诺塔问题分解为移动n-1个盘子的汉诺塔问题,把移动n-1个盘子的汉诺塔问题分解为移动n-2个盘子的汉诺塔问题,…,把移动2个盘子的汉诺塔问题分解为移动1个盘子的汉诺塔问题。对于1个盘子的汉诺塔问题直接求解。
在1个盘子的汉诺塔问题解决后,可以解决2个盘子的汉诺塔问题,…,在n-1个盘子的汉诺塔问题解决后,可以解决n个盘子的汉诺塔问题。这样n个盘子的汉诺塔问题最终就得以解决。
结合本节和6.2节的讨论,我们可总结如下:递归算法的执行过程是不断地自调用,直到到达递归出口才结束自调用过程;到达递归出口后,递归算法开始按最后调用的过程最先返回的次序返回;返回到最外层的调用语句时递归算法执行过程结束。
我们知道,对非递归函数的调用,在函数调用前系统要保存以下三种信息:
(1)返回地址;
(2)本次函数调用时的实参值和函数值;
(3)被调用函数的局部变量值。6.4递归过程和运行时栈当函数调用返回时,系统首先要释放实参内存空间和局部变量内存空间,然后按所保存的返回地址返回。
递归函数调用时系统要做的工作和非递归函数调用时系统要做的工作在形式上类同,但保存信息的方法不同。对递归函数的调用,系统在函数调用前也要保存上述三种信息,但因为递归函数的自调用特性,上述保存信息的方法将由于函数不断地自调用,
返回地址、实参值和局部变量值互相重叠,因此不能使用。由于堆栈的后进先出特性刚好和递归调用的过程吻合,因此,高级程序设计语言(如C语言)利用堆栈保存递归函数调用时的信息,系统中用于保存递归函数调用信息的堆栈称作运行时栈。
递归函数调用时,系统的运行时栈也要保存上述三种信息。每一层递归调用所需保存的信息构成运行时栈的一个工作记录,在每进入下一层递归调用时就建立一个新的工作记录,并把这个工作记录进栈成为运行时栈新的栈顶;每退出一层递归调用,就退栈一个工作记录。因为栈顶的工作记录必定是当前正在运行层的工作记录,所以栈顶的工作记录称为活动记录。
下面我们重新给出例6-1计算阶乘函数的递归算法,并以此例为例说明递归函数调用时运行时栈中工作记录的变化过程。计算阶乘函数的递归算法如下:longintFact(intn)
{
intx;
longinty;
if(n<0)/*n<0时阶乘无定义*/
{
printf("参数错!");
return-1;
}
if(n==0)return1;
else
{
y=Fact(n-1);/*递归调用*/
returnn*y;
}
}当某个函数调用上述递归函数时,按照前面的分析可知,系统的运行时栈要保存调用函数的返回地址、本次函数调用时的实参值和函数值以及被调用函数的局部变量值。当调用递归函数Fact()时,系统的运行时栈需要保存实参n、函数Fact以及局部变量x和y的值。图6-5给出了用实参n=3调用阶乘递归函数时运行时栈的变化过程。另外,系统的运行时栈中还要保存调用函数的返回地址,因函数的地址是系统动态分配的,调用函数的返回地址因此也是动态变化的,不好给出具体数值,所以图6-5的运行时栈变化过程中没有给出调用函数的返回地址值。
图6-5运行时栈的变化过程
(a)初始时;(b)Fact(3);(c)Fact(2);(d)Fact(1);(e)Fact(0);(f)y=Fact(0);(g)y=Fact(1);(h)y=Fact(2);(i)结束时
我们以斐波那契数列递归算法的执行效率为例来讨论递归算法的执行效率问题。
斐波那契数列Fib(n)的递归定义是:
Fib(n)=0当n=0时
1当n=1时
Fib(n-1)+Fib(n-2)当n>1时
按照上式求第n项斐波那契数列的递归函数如下:longFib(intn)
6.5递归算法的效率分析
{
if(n==0||n==1)returnn;/*递归出口*/
elsereturnFib(n-1)+Fib(n-2);/*递归调用*/
}上述递归函数求Fib(5)的递归计算过程如图6-6所示。由图可见,若要求Fib(5),要先求Fib(4)和Fib(3);而求Fib(4)时需先求Fib(3)和Fib(2),求Fib(3)时需先求Fib(2)和Fib(1);如此等等,总共需计算Fib(4)1次,计算Fib(3)2次,计算Fib(2)3次,计算Fib(1)5次,计算Fib(0)3次,累计递归调用函数的次数为24-1=15次。
图6-6Fib(5)的递归调用树按归纳法可得出求斐波那契数列的递归函数Fib(n)的递归调用次数等于2n-1;若把图6-6左下角的两个子树Fib(1)和Fib(0)放到右下角的结点Fib(1)下,则图6-6是一棵完全二叉树,由完全二叉树的性质也可得出求斐波那契数列的递归函数Fib(n)的递归调用次数等于2n-1(关于完全二叉树和完全二叉树的性质将在下一章讨论)。因此,上述计算斐波那契数列的递归函数Fib(n)的时间复杂度为O(2n)。对于计算斐波那契数列Fib(n)问题,我们也可根据公式写出循环方式求解的函数如下:
longFib2(intn)
{
longintoneBack,twoBack,current;
inti;
if(n==0||n==1)returnn;
else
{
oneBack=1;
twoBack=0;
for(i=2;i<=n;i++)
{
current=oneBack+twoBack;
twoBack=oneBack;
oneBack=current;
}
returncurrent;
}
}显然,上述循环方式的计算斐波那契数列的函数Fib2(n)的时间复杂度为O(n)。对比Fib2(n)和递归结构的Fib(n)可发现,循环方式的Fib2(n)算法在计算第n项的斐波那契数列时保存了当前已经计算得到的第n-1项和第n-2项的斐波那契数列,因此其时间复杂度为O(n);而递归方式的Fib(n)算法在计算第n项的斐波那契数列时,必须首先计算第n-1项和第n-2项的斐波那契数列,而某次递归计算得出的斐波那契数列,如Fib(3)无法保存,下一次要用到时还需要递归计算,因此其时间复杂度为O(2n)。
对于同一个问题,递归算法的时间复杂度是O(2n),而循环算法的时间复杂度是O(n),可见,求解斐波那契数列问题的循环算法效率要比递归算法效率好很多。一般情况下,若循环方式的算法和递归方式的算法均能求解该问题,通常循环方式算法的时间复杂度要比递归方式算法的时间复杂度低很多。
一般来说,存在如下两种情况的递归算法:
(1)存在不借助堆栈的循环结构的非递归算法,如阶乘计算问题、斐波那契数列的计算问题、折半查找问题等。
(2)存在借助堆栈的循环结构的非递归算法,所有递归算法都可以借助堆栈设计成循环结构的非递归算法,如下面例6-4设计的汉诺塔问题借助堆栈的循环结构的非递归算法。*6.6递归算法到非递归算法的转换
有些问题需要用低级程序设计语言来实现,而低级程序设计语言(如汇编语言)一般不支持递归,此时需要采用问题的非递归结构算法。对于第一种情况,可以直接选用循环结构的算法。由6.5节的讨论知,循环结构算法的时间效率通常也比递归结构算法的时间效率高很多。
对于第二种情况,可以把递归算法转换成相应的非递归算法,此时有两种转换方法:一种方法是借助堆栈用非递归算法形式化模拟递归算法的执行过程;另一种方法是根据问题设计借助堆栈的循环结构算法。
这两种方法都需要使用堆栈,这是因为堆栈的后进先出特点正好和递归调用过程后调用的函数先执行的特性吻合。下面讨论的例6-4是第一种转换方法的例子。此例也可看作6.2节讨论的递归算法执行过程的练习。
借助堆栈模拟系统的运行时栈,转换汉诺塔问题的递归算法为非递归算法。
汉诺塔问题的递归算法如下:
voidtowers(intn,charfromPeg,chartoPeg,charauxPeg)
{
if(n==1)/*递归出口*/
{
printf("%s%c%s%c\n","movedisk1frompeg",
fromPeg,"topeg",toPeg);
return;
}
/*把n-1个圆盘从fromPeg借助toPeg移至auxPeg*/
towers(n-1,fromPeg,auxPeg,toPeg);
/*把圆盘n由fromPeg直接移至toPeg*/
printf("%s%d%s%c%s%c\n","movedisk",n,
"frompeg",fromPeg,"topeg",toPeg);
/*把n-1个圆盘从auxPeg借助fromPeg移至toPeg*/
towers(n-1,auxPeg,toPeg,fromPeg);
}
设计问题分析:由于汉诺塔问题的递归算法中有两处递归调用,再加上主调函数,所以非递归模拟算法中应有三个模仿返回地址,三个返回地址分别为返回主调函数、返回第一次递归调用处和返回第二次递归调用处,在模拟算法中这三个返回地址分别对应三个语句标号lable1、lable2和lable3,我们用整数值1,2,3模仿三个返回地址;递归算法中共有四个参数;递归函数的局部变量在整个递归调用中未发生变化,所以不用保存。因此,非递归模拟算法中每次需保存算法的四个参数和一个模拟返回地址。本应用例子从另一个角度也可看作是顺序堆栈抽象数据类型的应用,我们用一个顺序堆栈来模拟系统的运行时栈,顺序堆栈的数据元素类型Datatype定义如下:typedefstruct
{
intretAddr;
intnPara;
charfromPegPara;
chartoPegPara;
charauxPegPara;
}DataType;汉诺塔问题的非递归算法设计如下:voidSimTowers(intn,charfromPeg,chartoPeg,charauxPeg)
/*把n个盘子从fromPeg借助auxPeg移到toPeg*/
{
DataTypecurrarea;
SeqStacks;
chartemp;
inti;
StackInitiate(&s);/*堆栈初始化*/
/*当前工作区初始化*/
currarea.nPara=n;
currarea.fromPegPara=fromPeg;
currarea.toPegPara=toPeg;
currarea.auxPegPara=auxPeg;
currarea.retAddr=1;
StackPush(&s,currarea);
/*以下模拟出口*/
start:
if(currarea.nPara==1)
{
printf("%s%c%s%c\n","movedisk1frompeg",
currarea.fromPegPara,"to.peg",currarea.toPegPara);
i=currarea.retAddr;
StackPop(&s,&currarea);
switch(i)
{
case1:gotolable11;
case2:gotolable12;
case3:gotolable13;
}
}
/*以下模拟递归自调用过程*/
StackPush(&s,currarea);
currarea.nPara--;
temp=currarea.auxPegPara;
currarea.auxPegPara=currarea.toPegPara;
currarea.toPegPara=temp;
currarea.retAddr=2;
gotostart;
/*以下模拟返回第一次递归调用*/
lable12:
printf("%s%d%s%c%s%c\n","movedisk",currarea.nPara,"frompeg",
currarea.fromPegPara,"topeg",currarea.toPegPara);
StackPush(&s,currarea);
currarea.nPara--;
temp=currarea.fromPegPara;
currarea.fromPegPara=currarea.auxPegPara;
currarea.auxPegPara=temp;
currarea.retAddr=3;
gotostart;
/*以下模拟返回第二次递归调用*/
lable13:
i=currarea.retAddr;
StackPop(&s,&currarea);
switch(i)
{
case1:gotolable11;
case2:gotolable12;
case3:gotolable13;
}
/*以下模拟返回主调函数*/
lable11:
return;
}由于汉诺塔问题本身算法的复杂度是O(2n),因此这个模拟算法的时间复杂度也是O(2n)。
6.7.1一般递归算法设计举例
设计一个输出如下形式数值的递归算法。
〖LM〗nnn…n
…
333
22
16.7设计举例
设计该问题可以看成由两部分组成:一部分是输出一行值为n的数值;另一部分是原问题的子问题,其参数为n-1。当参数减到0时不再输出任何数据值,因此递归的出口是当参数n≤0时空语句返回。
递归算法设计如下:voidDisplay(intn)
{
inti;
for(i=1;i<=n;i++)
printf("%d",n);
printf("%\n");
if(n>0)Display(n-1);/*递归*/
/*n≤0为递归出口,递归出口为空语句*/
}设计求解委员会问题的算法。委员会问题是:从一个有n个人的团体中抽出k(k≤n)个人组成一个委员会,计算共有多少种构成方法。设计问题分析:从n个人中抽出k(k≤n)个人的问题是一个组合问题。把n个人固定位置后,从n个人中抽出k个人的问题可分解为两部分之和:第一部分是第一个人包括在k个人中,第二部分是第一个人不包括在k个人中。对于第一部分,则问题简化为从n-1个人中抽出k-1个人的问题;对于第二部分,则问题简化为从n-1个人中抽出k个人的问题。图6-7给出了n=5,k=2时问题的分解示意图。
图6-7委员会问题分解示意图当n=k或k=0时,该问题可直接求解,数值均为1,这是算法的递归出口。因此,委员会问题的递推定义式为:
comm(n,k)=1当k=0时
1当n=k时
comm(n-1,k-1)+comm(n-1,k)其他
根据上述递推定义式设计的递归算法如下:intComm(intn,intk)
{
if(n<1||k<0||k>n)return0;/*参数出错*/
if(k==0)return1;/*递归出口*/
if(n==k)return1;/*递归出口*/
returnComm(n-1,k-1)+Comm(n-1,k);/*递归*/
}求两个正整数n和m的最大公约数的递推定义式为:
Gcd(n,m)=Gcd(m,n)当n<m时
n当m=0时
Gcd(m,n%m)当m>0时
要求:
(1)编写求解该问题的递归算法;
(2)分析当调用语句为Gcd(30,4)时算法的执行过程和执行结果;
(3)分析当调用语句为Gcd(97,5)时算法的执行过程和执行结果;
(4)编写求解该问题的循环结构算法。
解(1)递归算法如下:intGcd(intn,intm)
{
if(n<0||m<0)exit(0);
if(m==0)returnn;
elseif(m>n)returnGcd(m,n);
elsereturnGcd(m,n%m);
}(2)调用语句为Gcd(30,4)时,因m<n,递归调用Gcd(4,2);因m<n,递归调用Gcd(2,0);因m=0,到达递归出口,函数最终返回值为n=2,即30和4的最大公约数为2。
(3)调用语句为Gcd(5,97)时,因m>n,递归调用Gcd(97,5);因m<n,递归调用Gcd(5,2);因m<n,递归调用Gcd(2,1);因m<n,递归调用Gcd(1,0);因m=0,到达递归出口,函数最终返回值为n=1,即5和97的最大公约数为1。
(4)分析上述递归算法可以发现,该递归算法前面一次的递归调用只是为了交换两个参数的位置,可以用常规的方法替代。最后一行的递归调用可以转换为循环结构实现。因此,循环结构算法可设计如下:
intGcd2(intn,intm)
{
inttn,tm,temp;
if(n<0||m<0)
{
printf("参数错!");
return-1;
}
if(m>n)/*交换参数位置*/
{
tn=m;
tm=n;
}
else/*不交换参数位置*/
{
tn=n;
tm=m;
}
while(tm!=0)/*用循环结构替代递归*/
{
temp=tn;
tn=tm;
tm=temp%tm;
}
returntn;/*返回最大公约数*/
}*6.7.2回溯法及设计举例
回溯法是递归算法的一种特殊形式,其基本思想是:对一个包括有很多结点,每个结点有若干个搜索分支的问题,把算法分解为对若干个子问题的搜索算法。当搜索到某个结点,发现无法再继续搜索下去时,就让搜索过程回溯退到该结点的前一结点,继续搜索这个结点的其他尚未搜索过的分支;如果发现这个结点也无法再继续搜索下去时,就让搜索过程继续回溯退到这个结点的前一结点,继续这样的搜索过程;这样的搜索过程一直继续到搜索到问题的解或搜索完全部可搜索分支没有解存在为止。由于回溯法每前进一步在新的结点上进行的搜索过程和前一个结点的搜索过程类同,且使整个问题的搜索范围缩小了一步,因此回溯法属于一种特殊形式的递归算法。下面我们以迷宫问题为例讨论回溯法的算法设计方法。
设计求解迷宫问题的算法并用实际例子测试。一个迷宫是一些互相连通的交叉路口的集合,给定一个迷宫入口,一个迷宫出口,当从入口到出口存在通路时输出其中的一条通路,当从入口到出口不存在通路时输出无通路存在。每个交叉路口除进来的路外有三个路口,分别是向左、向前和向右。为简化设计,假设迷宫中不存在环路。图6-8是一个没有环路的迷宫示意图。
图6-8迷宫问题设计问题分析:用回溯法的分析方法考虑迷宫问题。迷宫问题中包括有很多路口,每个路口最多有三个搜索分支,把算法设计为如下的搜索过程:把整个搜索分解为向左、向前和向右三个方向上子问题的搜索。当搜索到某个路口、发现无法再继续搜索下去时,就让搜索过程回溯退到该结点的前一结点,继续搜索这个结点的其他尚未搜索过的路口;如果发现这个路口也无法再继续搜索下去时,就让搜索过程继续回溯退到这个路口的前一路口,继续这样的搜索过程。这样的搜索过程一直继续到找到出口或搜索完全部可连通的路口没有找到出口为止。数据结构设计:要用计算机模仿迷宫问题,首先要把迷宫问题数值化。我们把每个路口定义成一个包括left、forward和right三个域的结构体,分别表示向左、向前和向右。如果某个域的值为非0,表示该方向上有通路可走到该非0值所对应的路口;如果某个域的值为0,则表示该方向上是死路。typedefstruct/*路口的结构体定义*/
{
intleft;/*向左*/
intforward;/*向前*/
intright;/*向右*/
}InterSection;描述一个迷宫问题的数据应包括全部路口数据的集合和迷宫的出口数据。另外,为使算法设计方便,迷宫问题的数据还应包括迷宫问题路口的个数。因此,可定义迷宫问题的结构体如下:typedefstruct/*迷宫的结构体定义*/
{
intmazeSize;/*路口个数*/
InterSection*intSec;/*路口集合*/
intExit;/*出口*/
}Maze;用上述数据结构的数值模拟图6-8并存放在文件“Maze1.dat”中,文件“Maze1.dat”中就保存了如下的数据集合:
6
020
356
004
000
000
700
7
说明:文件的第一行是迷宫的路口个数,这里路口共有6个;第二到第七行是编号1到编号6的6个路口的状态,如第2行的020表示编号1的路口的状态是向左不通,向前通到2号路口,向右不通;最后一行表示迷宫的出口为路口7。
迷宫问题的回溯搜索算法设计如下:
intTravMaze(Maze*m,intcurrSetValue)
/*用回溯法搜索迷宫的所有分支,输入参数currSetValue为当前所处的路口*/
{
/*currSetValue>0为有路径存在,可以继续探索发现一条路径*/
if(currSetValue>0)
{
if(currSetValue==m->Exit)/*递归出口*/
{
printf("%d<==",currSetValue);/*输出路口号*/
return1;
}
elseif(TravMaze(m,m->intSec[currSetValue].left)==1)
/*向左探索*/
{
/*只有探索成功,即返回值为1,才执行以下语句*/
printf("%d<==",currSetValue);/*输出路口号*/
return1;
}
elseif(TravMaze(m,m->intSec[currSetValue].forward)==1)
/*向前探索*/
{
printf("%d<==",currSetValue);/*输出路口号*/
return1;
}
elseif(TravMaze(m,m->intSec[currSetValue].right)==1)
/*向右探索*/
{
printf("%d<==",currSetValue);/*输出路口号*/
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 宗祠财务管理规章制度
- 售后部门绩效考核制度
- 募集资金内部审计制度
- 初创公司绩效考核制度
- 小学继续教育总培训制度
- 地质勘查绩效考核制度
- 大数据审计规章制度汇编
- 区税务局教育培训制度
- 口腔诊所绩效考核制度
- 公司财务室规章制度模板
- 《1+X幼儿照护(中级)》课件-气管异物急救处理
- 情感体验量表DESⅡ-附带计分解释
- 水泵设备安装施工方案百度
- 铝锭买卖协议范本
- GB/T 43103-2023金属材料蠕变-疲劳损伤评定与寿命预测方法
- QCC-SMT降低空焊虚焊不良专案课件
- 超星尔雅学习通《高职劳动教育(金华职业技术学院)》2023章节测试附答案
- 勾股定理的逆定理(人教版八年级数学下册课件)
- GB/T 22751-2008台球桌
- GA/T 148-2019法医学病理检材的提取、固定、取材及保存规范
- 《山东省房屋修缮工程消耗量定额》建筑分册交底资料
评论
0/150
提交评论