汉诺塔函数递归调用问题给个解释

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

汉诺塔函数递归调用问题给个解释

[ 标签:汉诺塔,递归调用,递归]

给答案前先确定你认为你的答案是对的,不要从网上直接复制过来,只想要个详细一步步怎么实现的解释,

『→0←』回答:1 人气:1 提问时间:2009-11-17 20:08

答案

13.5 函数的递归调用(选修)

第4次从洗手间里走出来。在一周前拟写有关函数的章节时,我就将递归调用的内容放到了最后。

函数递归调用很重要,但它确实不适于初学者在刚刚接触函数的时候学习。13.5.1 递归和递归的危险

递归调用是解决某类特殊问题的好方法。但在现实生活中很难找到类似的比照。有一个广为流传的故事,倒是可以看出点“递归”的样子。

“从前有座山,山里有座庙,庙里有个老和尚,老和尚对小和尚说故事:从前有座山……”。

在讲述故事的过程中,又嵌套讲述了故事本身。这是上面那个故事的好玩之处。

一个函数可以直接或间接地调用自已,这就叫做“递归调用”。

C,C++语言不允许在函数的内部定义一个子函数,即它无法从函数的结构上实现嵌套,而递归调用的实际上是一种嵌套调用的过程,所以C,C++并不是实现递归调用的最好语言。但只要我们合理运用,C,C++还是很容易实现递归调用这一语言特性。

先看一个最直接的递归调用:

有一函数F();

void F()

{

F();

}

这个函数和“老和尚讲故事”是否很象?在函数F()内,又调用了函数F()。

这样会造成什么结果?当然也和那个故事一样,没完没了。所以上面的代码是一段“必死”的程序。不信你把电脑上该存盘的存盘了,然后建个控制台工程,填入那段代码,在主函数main()里调用F()。看看结果会怎样?WinNT,2k,XP可能好点,98,ME就不好说了……反正我不负责。出于“燃烧自己,照亮别人”的理念,我在自已的XP+CB6上试了一把,下面是先后出现的两个报错框:

这是CB6的调试器“侦察”到有重大错误将要发生,提前出来的一个警告。我点OK,然后无厌无悔地再按下一次F9,程序出现真正的报错框:

这是程序抛出的一个异常,EStackOverflow这么看:E字母表示这是一个错误(Error),Stack正是我们前面讲函数调用过程的“栈”,Overflow意为“溢出”。整个 StasckOverflow 意思就:栈溢出啦!

“栈溢出”是什么意思你不懂?拿个杯子往里倒水,一直倒,直到杯子满了还倒,水就会从杯子里溢出了。栈是用来往里“压入”函数的参数或返回值的,当你无限次地,一层嵌套一层地调用函数时,栈内存空间就会不够用,于是发生“栈溢出”。

(必须解释一下,本例中,void F()函数既没有返回值也没有参数,为什么还会发生栈溢出?事实上,调用函数时,需要压入栈中的,不仅仅是二者,还有某些寄存器的值,在术语称为“现场保护”。正因为C,C++使用了在调用时将一

些关键数值“压入”栈,以后再“弹出”栈来实现函数调用,所以C,C++语言能够实现递归。)

这就是我们学习递归函数时,第一个要学会的知识:

逻辑上无法自动停止的递归调用,将引起程序死循环,并且,很快造成栈溢出。

怎样才能让程序在逻辑上实现递归的自动停止呢?这除了要使用到我们前面辛辛苦苦学习的流程控制语句以后,还要掌握递归调用所引起的流程变化。

13.5.2 递归调用背后隐藏的循环流程

递归引起什么流程变化?前面的黑体字已经给出答案:“循环”。自已调用自已,当然就是一个循环,并且如果不辅于我们前面所学的if...语句来控制什么时候可以继续调用自身,什么时候必须结束,那么这个循环就一定是一个死循环。

如图:

递归调用还可间接形成:比如 A() 调用 B(); B() 又调用 A(); 虽然复杂点,但实质上仍是一个循环流程:

在这个循环之里,函数之间的调用都是系统实现,因此要想“打断”这个循环,我们只有一处“要害”可以下手:在调用会引起递归的函数之前,做一个条件分支判断,如果条件不成立,则不调用该函数。图中以红点表示。

现在你明白了吗?一个合理的递归函数,一定是一个逻辑上类似于这样的函数定义:

void F()

{

……

if(……) //先判断某个条件是否成立

{

F(); //然后才调用自身

}

……

}

在武侠小说里,知道了敌人的“要害”,就几乎掌握了必胜的机会;然而,“递归调用”并不是我们的敌人。我们不是要“除掉”它,相反我们利用它。所以尽管我们知道了它的要害,事情还要解决。更重要的是要知道:什么时候该打断它的循环?什么时候让它继续循环?

这当然和具体要解决问题有关。所以这一项能力有赖于大家以后自已在解决问题不断成长。就像我们前面的讲的流程控制,就那么几章,但大家今后却要拿它们在程序里解决无数的问题。

(有些同学开始合上课本准备下课)程序的各种流程最终目的是要合适地处理数据,而中间数据的变化又将影响流程的走向。在函数的递归调用过程中,最最重要的数据变化,就是参数。因此,大多数递归函数,最终依靠参数的变化来决定是否继续。(另外一个依靠是改变函数外的变量)。

所以我们必要彻底明了参数在递归调用的过程中如何变化。

13.5.3 参数在递归调用过程中的变化

我们将通过一个模拟过程来观察参数的变化。

这里是一个递归函数:

void F(int a)

{

F(a+1);

}

和前面例子有些重要区别,函数F()带了一个参数,并且,在函数体内调用自身时,我们传给它当前参数加1的值,作为新的参数。

红色部分的话你不能简单看过,要看懂。

现在,假设我们在代码中以1为初始参数,第一次调用F():

F(1);

现在,参数是1,依照我们前面“参数传递过程”的知识,我们知道1被“压入”栈,如图:

F()被第1次调用后,马上它就调用了自身,但这时的参数是 a+1,a就是原参数值,为1,所以新参数值应为2。随着F函数的第二次调用,新参数值也被入栈:

再往下模拟过程一致。第三次调用F()时,参数变成3,依然被压入栈,然后是第四次……递归背后的循环在一次次地继续,而参数a则在一遍遍的循环中不断变化。

由于本函数仍然没有做结束递归调用的判断,所以最后的最后:栈溢出。

要对这个函数加入结束递归调用的逻辑判断是非常容易的。假设我们要求参数变到10(不含10)时,就结束,那么代码如:

相关文档
最新文档