汉诺塔函数递归调用问题给个解释
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 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)时,就结束,那么代码如: