0
点赞
收藏
分享

微信扫一扫

数据结构_Java_递归、汉诺塔问题的理解


写在前面

汉诺塔问题时很经典的递归问题,思考理解它很有必要

接下来我就谈谈对递归和汉诺塔问题的理解

汉诺塔问题

  • 来源
    汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
  • 题目理解
    三根柱子 a,b,c,a柱子上有从下到上 大小 递减 的盘子,编号64 - 1,即小盘子对应小编号
    现在要求把a柱上的64个盘子移动到c柱上,并且要求:
  • 每次只能移动一个盘子
  • 任何时刻大的盘子必须在下面(移动过程中盘子序号可以不连续)
  • 代码

import java.util.Date;

public class DemoHanNuoTa {
    //定义成员变量,方便记录移动的次数

    static int count = 0;

    public static void main(String[] args) {



        hanNuoTa(10,'a','b','c');




    }

    private static void hanNuoTa(int num,char a,char b,char c) {


        if(num == 1){

            System.out.println("这是第 " + (++count) + " 次移动 ,移动" + num + "号盘从 " +  a + " 移动到 " + c);
        }
        else{
            hanNuoTa(num - 1,a,c,b);
            System.out.println("这是第 " + (++count) + " 次移动 ,移动" + num + "号盘从 " +  a + " 移动到 " + c);
            hanNuoTa(num - 1,b,a,c);
        }
    }
}

数据结构_Java_递归、汉诺塔问题的理解_java

  • 关于时间问题

计算机的速度是很快的,10个盘子需要移动1023次,可以试试跑·64个盘子需要多长时间

假设有n片,移动次数是f(n)。显然f(1)=1,f(2)=3,f(3)=7,且f(k+1)=2*f(k)+1。不难证明f(n)=2n-1。(深度为n的满二叉树的总结点个数也恰为2n-1。这两者间该有什么对应?)

n=64时,假如每秒钟一次,共需多长时间呢?一个平年365天有31536000 秒,闰年366天有31622400秒,平均每年31556952秒,计算一下:

18446744073709551615秒

这表明移完这些金片需要5845.54亿年以上,而地球存在至今不过45亿年,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,不说太阳系和银河系,至少地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。
(引自知乎)

分析

上述代码看不明白没关系,接下来一步一步分解理解

首先脑子里面应该有个图

数据结构_Java_递归、汉诺塔问题的理解_算法_02

  1. 先理解简单的递归思想
    递归体现就是函数自己调用自己,每次调用就压栈,直到达到递归的出口(一般是达到某条件 后,最后一次函数调用获得return之后,开始弹栈,把自己的return 给调用自己的 距离栈顶 2 位置的函数(谁调用的你你出栈的时候需要给个结果,然后调用者的任务随机结束), 接着2位置函数变成栈顶函数 ,然后调用 栈顶的已经弹栈的 2函数也会获得一个结果开始弹栈,依次类推…直到全部出栈完毕,递归完成)
    如果还是不太清楚,可以看看往期博客:Java课堂篇2_递归_连续逐个输入若干字符,以 #号结束,实现结束时 输出 输入字符的逆序(借此问题形象理解递归、字符串的 == 和 equals()的区别)
    里面举了一个士兵战斗的小栗子,更形象一些。
  2. 问题化小
    盘子个数太多不好理解,那么就从个数少的开始
  • 假如只有一个盘子:把1号盘子 a---->c 即可(对应的是不进入递归的 num == 1的情况)
  • 假如只有两个盘子:把 1 号盘子a---->b 然后 2号盘子 a---->c 然后 1 号盘子 b---->a
  • 假如只有三个盘子:把 1 号盘子a---->b 然后 2号盘子 a---->c 然后 1 号盘子 b---->c (1和2已经完成要求,但是3还在a)
    然后 3号盘子 a---->b (此时 3 在 b,1和2在c)
    然后 1号盘子c---->a 然后 2号盘子c---->b (1在a,2和3在b)
    然后 1号盘子 a---->c (1在c,2和3在b)
    然后 2号盘子b---->a
    然后 1号盘子c----->a (c空了出来)
    然后 3号盘子b----->c
    然后就是只有两个盘子的情况了
  1. 代码分解
    问题化小到3个盘子发现就需要移动7次,脑子里面有个大致的移动画面,接着分解代码

if(num == 1){
		//能执行到这一步,证明此层函数不在接着向下调用,获得递归的结束条件,
		//就证明调用此层 函数的 上一层函数待这层执行完之后,也会从递归中出来,执行下面语句
        System.out.println("这是第 " + (++count) + " 次移动 ,移动" + num + "号盘从 " +  a + " 移动到 " + c);
    }
    else{
        hanNuoTa(num - 1,a,c,b);
        System.out.println("这是第 " + (++count) + " 次移动 ,移动" + num + "号盘从 " +  a + " 移动到 " + c);
		//假如没有下层代码会是什么情况呢
        //hanNuoTa(num - 1,b,a,c);
    }

数据结构_Java_递归、汉诺塔问题的理解_数据结构_03


发现10个盘子,移动10次,最后b从上到下 9, 7,5, 3 ,1。 c从上到下 10,8,6,4,2。

交替b、c这是因为 函数的形参 顺序不同导致的,我们关心的是 盘都是 上面的大于下面的,也就是说下面的代码 功能是实现 参数逆转 + 保证上面的盘子 小于 下面的盘子的

  • 进一步分析
    对于3个盘子来说,我们的目的很明确,就是以从上到下 1 2 3 的顺序 从 a 挪到 c
    但是3个盘子不像1个盘子,不可以直接移动,我们理想状态就是先移动3到c 然后移动 2 到 c 最后 移动 1 到 c
    但是3 上面2 和 1压着,移动不得,2上面有 1 压着,移动不得。对于64个也是,64上面有63-1个压着。因此我们需要分块,一块是3,一块是2-1
    所以我们需要递归的第一步,就是把2-1移动到不是c的位置(借助递归),然后移动 3 到 c,最后把2-1移动到c(借助递归
    递归 + 移动 + 递归 是不是和上面的代码照应上了
  • 3个盘子,首先进入第一层递归,转化为2个盘子,然后进入第二层递归的第一个递归,转化为1个盘子,然后进入第三次递归, 但是此时一个盘子是可以移动的(第一次移动),结束第三次递归,第三次递归获得结果,接着执行第二层递归的移动(第二次移动),然后进入第层次递归的第二个递归,进入第三层递归,但是此时一个盘子是可以移动的(第三次移动),结束第三层递归,第三层递归获得结果,第二层递归的语句(包括 第一个递归 + 移动 + 第二个递归)结束,开始执行第一层递归的 移动(第四次移动),后面的为进入第一层递归的第二次递归,与上面同理
  • 参考图解
  • 总结
    假如n个盘子,则需要依次进栈n-1层递归,第n-1层递归调用最后一层递归n (对应num = 1,只有一个移动语句,开始出栈,结束第n层递归)
    开始执行 n-1层的移动语句
    接着进入第n-1层的第二个递归(每层递归有两个递归函数),第n-1层递归调用最后一层(对应num = 1,只有一个移动语句,开始出栈,结束第n层递归),此时第n-1等的递归已经完成
    这时要从n-2层的第一个递归函数出来,执行第n-2层的输出语句,然后进入第n-2层的第二个递归

    整个过程就是不断进栈直到能出栈,然后出栈,移动之后,接着进栈,直到能出栈,就移动…

感觉自己说的还是不够形象易懂,无奈还是先到这吧~


举报

相关推荐

0 条评论