标题:C语言:汉诺塔
水墨不写bug
(图片来源于网络)
正文开始:
相信你在写函数递归的时候,一定会尝试一种最简单的递归,但实际上它并 不是 递归的 正确写法:
e.g.1
#include<stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
你会发现:
程序从main()函数进入后会再次调用main()函数,似乎永远不会停下来;
结果:
程序最终崩溃了,但是什么造成了程序崩溃?
如果按F10调试,你会找到一条详细的报错信息:
报错信息就是:stack overflow!
(一)由递归导致栈溢出
(1)汉诺塔
相信你一定知道汉诺塔的问题:
大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
i,特殊的视角
我们先从简单的情况开始分析:
当移动4片时,在移动的过程中,一定会存在一个状态,在这个状态下:
1.最下层的4号盘子放在目标柱子上;(其他盘子的状态就显得不那么重要了)
2.其他的盘子从上到下,从小到大,摆在第一个柱子上;(为了使游戏继续进行,不得不这样做)
这时,我们完成了移动最底层的盘子。
于是,此时盘子的状态:
这时:
你会发现,接下来就是 移动 三层盘子的问题了,相信对你来说这不是个难题。
ii,问题回归
当我们尝试将n个盘子从 “ start ” 移动到 “ dest ” 时:
1.可以将上面的(n-1)个盘子看作是一个整体,
2.最下面一个盘子:第n个盘子看作与整体等地位的个体;
于是,我们将n个盘子从 “ start ” 移动到 “ dest ” 的目标就可以分解成:
先将上面(n-1)个盘子移动到 “ dest ” ,再将第 n 个盘子移动到 “ dest ” 。
但是,我们的力量是有限的,一次只能移动一个盘子。于是,我们可以再次动用上述方法;
先将上面(n-2)个盘子移动到 “ dest ” ,再将第 (n-1)个盘子移动到 “ dest ” 。
......
iii,问题解决
在这里,为了表述方便,我引入 “ 降n程序 ” 的概念:
每执行一次降n程序,都会使 剩下的 需要移动的 盘子数 减 1 ;
而每执行一次 “ 降 n 程序 ” ,都要移动上方的 (n-1)个盘子两次;
在一次只能移动一个盘子的前提下,我们把移动 n 个盘子的操作称为 “ 移n操作 ”;
试想:
在上方(n-1)个盘子不等于 1 的情况下,由于我们每次只能移动一个盘子,所以——每次移动上方的 (n-1)个盘子,也就必须先移动上方的(n-2)个盘子,和第(n-1)个盘子。
也就是说:
1.一次 ” 将 n 程序 “ 包含两次 “移n操作” ;
2.“移n操作想要完成,当且仅当n = 1 的时候才可以实现;
于是,移动n个盘子需要的次数是:2^n - 1 次!(为什么要 ” -1 “呢?——因为当n = 1的时候,一次就可以完成移动了,不用再分)
(2)子过程
当你回顾汉诺塔问题时,会发现:
我们要移动n个盘子到目的地,就必须先移动上方(n-1)个盘子,而移动上方(n-1)个盘子,又要先移动上方(n-2)个盘子......
也就是说,如果第(n-2)个盘子未完成移动,那么第2次的 ” 降n程序 “ 就没有完成!如果第(n-3)个盘子未完成移动,那么第3次的 ” 降n程序 “ 就没有完成!
......
也就是说,深一层的递归是本层递归的一个子过程!
如果这一过程发生在计算机的内存中呢?
每一次递归都要在内存的栈区开辟一块新的空间。
这就很容易找到原因了。
(二)计算机的内存(为什么会栈溢出)
其实,计算机的内存可以分为三个区域:栈区,堆区,静态区。
它们在存储使用时遵循不同的规则,并且存储的内容也不同。
如图:
而我们每一次递归时,上一次的递归程序仍然没有结束,也就是上一次递归的函数仍然占据着内存栈区的空间;
当我们递归调用函数没有跳出条件时(也就是e.g.1的情形),栈就会溢出。
于是,当我们理解了栈溢出的原理,就会发现:
递归程度太深也会造成栈溢出,
e.g.2
#include<stdio.h>
void conduct(int n)
{
if (n < 10000)
{
conduct(n + 1);
}
}
int main()
{
conduct(1);
return 0;
}
由于每一次递归时上一次调用的conduct()函数并没有结束,所以上一次调用的 conduct()函数在内存的栈区仍然占有空间。
程序的执行结果:
仍然造成了 stack overflow,由此可见:当递归程度太深也会造成栈溢出。
(三)什么是栈溢出
栈溢出的原因主要是程序访问了不合法的内存地址,导致写入了超过栈空间大小的数据,从而覆盖了栈中其他变量或函数的返回地址、参数和局部变量等数据。
(四)小结
1.栈溢出是内存栈区被不合理使用造成的;
2.写函数递归的时候要避免死递归;
3.递归成都太深也会造成栈溢出;
回顾:
目录
完~
未经作者同意禁止转载