清晰明了的认识(不是靠死背,能够在头脑中想象,能够独立写出算法)
2.汉诺塔的编程解决算法:递归实现。什么是递归?
void fun()
{
fun();
}
int main(void)
{
fun();
}
不好意思,如果你这么写的话,程序要炸,你会发现内存被耗尽,电脑死机,别去尝试!
fucking why?
首先,我们要知道,计算机是怎么调用函数的
那么,递归,函数自己调用自己,将会发生什么?
永远循环在第一步和第二步,不断的耗尽内存,直到崩溃。
所以说,老司机告诫递归函数要有返回的条件,就是不要一直循环下去。
什么是返回的条件?就是导演喊‘咔’,不要往下演了,如下:计算n+(n-1)+...+2+1
int fun(int n)
{
if(n>0)
{
return n+fun(n-1);
}
else
{
return 0;
}
}
int main(void)
{
int sum = 0;
sum = fun(10);
printf("sum = : %d",sum);
}
用小拇指算一下,是不是等于55.
函数调用过程:
fun函数是可以看成这样(只是变变形式而已):
int fun(int n)
{
if(n>0)
{
n = n+fun(n-1);
return n;
}
else
{
return 0;
}
}
可想而知函数执行过程中:
开始:入参n=10调用fun(10-1),即fun(9),把9当做入参,n=9,调用fun(9-1),n=8,依次下去,直到n=0,
这时候
妙吧?
如果不太清楚,请仔细重复一二。
3.汉诺塔编程算法
先不解释,看看代码,一下子是想不出来的,我们可以从代码的运行过程中找到解决的思路,这是一种方法之一,简称:逆向工程。
(快速浏览一下代码,在往下看下面的解释)
#include
#include
static int count = 0;
void move(char getone, char putone) {
count ++;
printf("%c-->%c\n", getone, putone);
}
void hanoit(int n, char a, char b, char c) {
if(n == 1)
{
move(a, c);
}
else
{
hanoit(n - 1, a, c, b);
printf("count :%d\n",count);
move(a, c);
hanoit(n - 1, b, a, c);
}
}
int main() {
int m;
scanf("%d", &m);
hanoit(m, 'A', 'B', 'C');
printf("count :%d",count);
system("pause");
return 0;
}
上图中,有三根棒,左中右,分别命名为'A','B','C',移动‘A’棒的圆片到‘B’棒上,代码执行为:move('A','B')
依次类推:如吧'C'棒移动到‘A’棒,为move('C',‘A’)。
先从简单的开始,假设有三个圆片,最初从小到大放在A棒,移动之后,要从小到大依次放在C棒,圆片根据小到大命名:1,2,3
第一步,将1从A棒移动到C棒
第二步,将2从A棒移动到B棒
第三步,将1从C棒移动到B棒
第四步,将3从A棒移动到C棒
第五步,将1从B棒移动到A棒
第六步,将2从B棒移动到C棒
第七步,将1从A棒移动到C棒
此时,圆盘1,2,3按从小到大依次排列依次在C棒上,移动的过程中没有违背规则:不管在哪根棒上,小片必须在大片上面
如有不清楚,请重复一二。
如果清晰明白了上面的移动次序,结合递归,我们来破解一下汉诺塔程序:
void hanoit(int n, char a, char b, char c) {
if(n == 1)
{
move(a, c);
}
else
{
hanoit(n - 1, a, c, b);
printf("count :%d\n",count);
move(a, c);
hanoit(n - 1, b, a, c);
}
}
观察:
第一眼:发现函数自己调用自己,使用递归,第二眼发现n 取值为1时有返回,程序不会炸,第三眼发现是递减调用:hanoit(n - 1, a, c, b);
假设执行:hanoit(10, 'A', 'B', 'C'),一共有10个圆盘。
那么递归调用次序(从计算机调用角度,因为调用过程参数顺序发生变化,索性使用r1,r2,r3代表,注意入参为n-1):
hanoit(9, 'A', 'B', 'C')
-->hanoit(8, r1, r2, r3)
--->hanoit(7, r1, r2, r3)
.............依次一下去
move(a,c)
hanoit(7, r1, r2, r3)
move(a,c)
hanoit(8, r1, r2, r3)
move(a,c)
hanoit(9, b, a, c)
.....
简单检验:假设n =3
执行:hanoit(3, 'A', 'B', 'C')
调用次序(r1, r2, r3为实际使用的第一个,第二个,第三个参数,注意入参为n-1)
开始:
1.hanoit(2, r1, r2, r3)
2.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第一次move
3.move(r1, r3);-----------------------------------------------------------------------------------执行第二次move
4.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第三次move
5.move(a, c);----------------------------------------------------------------------------------------------执行第四次move
6.hanoit(2, r1, r2, r3)
7.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第五次move
8.move(r1, r3);-----------------------------------------------------------------------------------执行第六次move
9.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第七次move
结束(一共执行7次move ,也就是2^n-1,即n = 3,2^3-1 = 7次)
在程序中执行如下(图):
码源:https://download.csdn.net/download/yangming2466/10699026
为什么是2^n-1 次move?
我们知道唯一的返回条件的n==1,n==1 执行move,即:
1.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第一次move,返回
2.move(r1, r3);-----------------------------------------------------------------------------------执行第二次move
3.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第三次move,返回
那么n=3 的时候可以这么拆分
2
1 (执行一次)
1 (执行一次)
1 (执行一次)
1 (执行一次)
2
1 (执行一次)
1 (执行一次)
1 (执行一次)
把一的个数数起来,就是执行move的次数,我们知道2^n<十进制> -1= 1111...(n个1)...111 <二进制>
我们知道 {1111...(n个1)...111 <二进制>} = {1111...(n-1个1)...111 <二进制>} + 1 + {1111...(n-1个1)...111 <二进制>}
例如 1111<二进制> = 15,111<二进制> = 7,15 = 7 + 1 + 7,即:1111<二进制> = 111<二进制> + 1 + 111<二进制>
可能有人问了:what are you doing ?
注意看:
hanoit(2, r1, r2, r3):
1.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第一次move,返回
2.move(r1, r3);-----------------------------------------------------------------------------------执行第二次move
3.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第三次move,返回
n = 2,执行次数是3,等于11<二进制>,岂不是可以这样拆分
111拆分
start:
11
1
1
1
-----------------------------------------------
1
-----------------------------------------------
11
1
1
1
end
这是和递归调用的深度数是何其的相似!1的个数 = 111<二进制> = 2^n-1,得出了这个结论,下面我们来说说递推的移动过程:
我们来详细把三个圆盘的移动推导过程过一下,特别是入参:
函数
void hanoit(int n, char a, char b, char c) {
if(n == 1)
{
move(a, c);
}
else
{
hanoit(n - 1, a, c, b);
printf("count :%d\n",count);
move(a, c);
hanoit(n - 1, b, a, c);
}
}
开始:
(r1, r2, r3为当前函数实际使用的第一个,第二个,第三个参数)
hanoit(3, 'A', 'B', 'C')---------------r1= 'A',r2='B',r3='C'
1.hanoit(2, r1, r2, r3)---------------r1= 'A',r2='C',r3='B'
2.hanoit(1, r1, r2, r3)------r1= 'A',r2='B',r3='C'--------------执行第一次move(r1,r3),即move("A', 'C')
3.move(r1, r3);-----------------------------------------------------执行第二次move(r1,r3),即move('A', 'B')
4.hanoit(1, r1, r2, r3)------r1= 'C',r2='A',r3='B'--------------执行第三次move(r1,r3),即move('C','B')
5.move(a, c);----------------------------------------------------------------执行第四次move ( r1,r3 ), 即move('A','C')
6.hanoit(2, r1, r2, r3)
7.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第五次move
8.move(r1, r3);-----------------------------------------------------------------------------------执行第六次move
9.hanoit(1, r1, r2, r3)---------------------------------------------------------------------------执行第七次move
结束(一共执行7次move ,也就是2^n-1,即n = 3,2^3-1 = 7次)
实践是检验真理的唯一标准:
码源:https://download.csdn.net/download/yangming2466/10699026
此时,圆盘1,2,3按从小到大依次排列依次在C棒上,移动的过程中没有违背规则:不管在哪根棒上,小片必须在大片上面。
我们再从逻辑宏观来分析:为什么能?
推广到三阶的时候,我们将小环和中环视为一个整体,变成了执行二阶汉诺塔
那么四阶前三个环视为整体,五阶前四个环视为整体…