汉诺塔最清晰的理解

版权声明:all right was reserved https://blog.csdn.net/yangming2466/article/details/82915824

                                               汉诺塔最清晰的理解——从函数调用角度

  • 阅读本文,你将会理解:
  1. 汉诺塔规律
  2. 汉诺塔算法函数递归调用次序
  3. 清晰明了的认识(不是靠死背,能够在头脑中想象,能够独立写出算法)

  • 汉诺塔:
  1. 法国数学家爱德华·卢卡斯曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。(如图,看得一脸懵逼,不慌,大佬带你飞,往下看)

                  

                                                                      

 2.汉诺塔的编程解决算法:递归实现。什么是递归?

  • 程序调用自身的编程技巧称为递归( recursion),例如:

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,

这时候

  • n=0,return 0
  • return后回到1+fun(0),fun(0) 返回0,则执行1+0,返回1+0=1,return 1
  • return后回到2+fun(1),fun(1) 返回1,则执行2+1,返回2+1=1,return 3
  • .
  • .
  • .
  • return 后回到10+fun(9),fun(9)返回9+8+...+2+1+0,则执行45+10=55,return 55,即是结果:

妙吧?

如果不太清楚,请仔细重复一二。

3.汉诺塔编程算法

先不解释,看看代码,一下子是想不出来的,我们可以从代码的运行过程中找到解决的思路,这是一种方法之一,简称:逆向工程。

(快速浏览一下代码,在往下看下面的解释)

#include<stdio.h>
#include<stdlib.h>
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棒上,移动的过程中没有违背规则:不管在哪根棒上,小片必须在大片上面。

我们再从逻辑宏观来分析:为什么能?

推广到三阶的时候,我们将小环和中环视为一个整体,变成了执行二阶汉诺塔
那么四阶前三个环视为整体,五阶前四个环视为整体…

                                   

猜你喜欢

转载自blog.csdn.net/yangming2466/article/details/82915824