首页 > 其他分享 >汉诺塔与二进制、满二叉树的千丝万缕

汉诺塔与二进制、满二叉树的千丝万缕

时间:2023-04-04 22:56:09浏览次数:59  
标签:二进制 System 汉诺塔 二叉树 println 移动 out

汉诺塔(Tower of Hanoi)源于印度传说中,大梵天创造世界时造了三根金钢石柱子,其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

汉诺塔递归算法

3阶汉诺塔移动步骤:

image

汉诺塔解法思路

一个规模为n的问题,可以拆成互相独立且与原问题形式相同的子问题的问题,可以采用递归方式解决子问题,然后将各个子问题的解合并得到原问题的解(分而治之思想)。

理解过程

如图,3阶的一共需要七步,

因为盘子3是最大的,所以所有盘子都可放在它上面,所以我们可以忽略盘子3,既是把“前三步”看做一个整体,完成2阶移动即可,移动目标是从A移动到B(伪C);

接着执行第四步,从A移到C,此时最大的盘就完成移动了,因为是最大,所以所有盘子都可以移到C,可以忽略盘子3,此时,后续的操作可以将3阶看成2阶来处理了;

“前三步”将盘子1和2,从A移到B了,托盘A和托盘B是相对来说的,此时的托盘B可以看做是托盘A,所以“后三步”2阶移动和普通的2阶移动步骤一样,移动目标是B(伪A)到C。

从上面分析可以知道,所有的移动都是从“A”移动到“C”,只是第一大步最后一大步是要交换位置,分别是C交换成B、B交换从A(看代码不太懂时,回来看这里)

当n=1时,只需托盘A直接移到托盘C(这是递归问题的出口); 当n>1时,需要借助另一托盘来移动,将n个圆盘由A移到C上可以分解为以下几个步骤: (1) 将A上的n-1个圆盘,借助C,从A移到B上; (2) 把A上第n个圆盘,直接从A移到C上; (3) 将B上的n-1个圆盘,借助A,从B移到C上。

递归方式实现的汉诺塔(Java版):

public class Hanoi {
    // 阶数
    private static int n = 4;
    //验证汉诺塔移动次数
    private static int sum=0;
    public static void main(String[] args) {
        System.out.println(String.format("%s层汉诺塔的移动顺序:", n));
        move(n, 'A','B','C');
        System.out.println("汉诺塔移动次数:"+sum);
    }

    /**
     * (n-1) A -> B
     *   n   A -> C
     * (n-1) B -> C
     * 
     * 结束条件为:当n=1 时, A -> C
     */
    public static void move(int n,char A, char B, char C) {
        if(n==1) {
            System.out.println(A + " -> " + C);
            sum++;
        }
        else {
            move(n-1, A, C, B);//每次都是输出A->C,所以要看到A->B,就需要将B和C交换

            if(n==Hanoi.n)
                System.out.println("前面完成(n-1)层:从A移动到B");
            System.out.println(A + " -> " + C);
            sum++;
            if(n==Hanoi.n)
                System.out.println("完成第(n)层:从A移动到C");

            move(n-1, B, A, C);//每次都是输出A->C,所以要看到B->C,就需要将A和B交换
            if(n==Hanoi.n)
                System.out.println("前面完成(n-1)层:从B移动到C");
        }
    }

}

执行结果:

3层汉诺塔的移动顺序:

A -> C

A -> B

C -> B

前面完成(n-1)层:从A移动到B

A -> C

完成第(n)层:从A移动到C

B -> A

B -> C

A -> C

前面完成(n-1)层:从B移动到C

汉诺塔移动次数:7

先完成(n-1)层:从A移动到B,

再完成第(n)层:从A移动到C,

最后完成(n-1)层:从B移动到C。

通过数学推导汉诺塔移动次数

递归算法可以通过递归式的方式去推导证明,现在通过递归式推导汉诺塔移动次数。

假定n是盘子的数量,T(n)是移动n个圆盘的移动次数。

当n=1时,T(1)=1

当n=2时,T(2)=2T(1)+1

当n=3时,T(3)=2T(2)+1

汉诺塔递归式

由递归式求n阶汉诺塔移动次数:

由递归式可知:

image

又因当n=1时,T(1)=1,得:

解得n阶汉诺塔移动次数为: 次。

汉诺塔与二进制

公式

这就像是n位二进制的和,最终得到n位二进制的最大值(全1)

所以有,n阶汉诺塔移动次数等于n位二进制得最大值,如:4阶汉诺塔移动次数为

每个盘子的移动次数,观察下图:

image

如图可知,每个盘子移动总次数刚好相反,

所以,n阶汉诺塔的第i个盘子总的移动次数为:

3阶汉诺塔图解与二进制关系

image

汉诺塔与满二叉树

递归算法会有相对应的递归树,而汉诺塔的递归树刚好是满二叉树,即所有分支结点都有两个叶子结点。

调整汉诺塔对算法代码的输出信息后:

public class Hanoi {
    // 阶数
    private static int n = 3;
    public static void main(String[] args) {
        System.out.println(String.format("%s层汉诺塔的移动顺序:", n));
        int sum = moveTree(n, 'A','B','C');
        System.out.println("汉诺塔移动次数:"+sum);
    }

    /**
     * 汉诺塔与满二叉树
     * (n-1) A -> B
     *   n   A -> C
     * (n-1) B -> C
     * 
     * 结束条件为:当n=1 时, A -> C
     */
    public static int moveTree(int n,char A, char B, char C) {
        if(n==1)
            System.out.println(String.format("第 %s 层(叶子节点):%s -> %s",n, A, C));
        else {
            moveTree(n-1, A, C, B);//每次都是输出A->C,所以要看到A->B,就需要将B和C交换

            if(n==Hanoi.n)
                System.out.println(String.format("第 %s 层(根节点):%s -> %s", n, A, C));
            else
                System.out.println(String.format("第 %s 层(分支结点):%s -> %s", n, A, C));

            moveTree(n-1, B, A, C);//每次都是输出A->C,所以要看到B->C,就需要将A和B交换
        }
        //汉诺塔的移动次数为: 2^n-1
        return (int) Math.pow(2, n)-1;
    }

}

3层汉诺塔的移动顺序:

第 1 层(叶子节点):A -> C

第 2 层(分支结点):A -> B

第 1 层(叶子节点):C -> B

第 3 层(根节点):A -> C

第 1 层(叶子节点):B -> A

第 2 层(分支结点):B -> C

第 1 层(叶子节点):A -> C

汉诺塔移动次数:7

3阶汉诺塔对应的满二叉树:

image

3阶汉诺塔的移动步骤为满二叉树的中序遍历:AC、AB、CB、AC、BA、BC、AC

从输出结果可以看到,汉诺塔盘子编号对应满二叉树自底向上计算的层号,如:1号盘子的移动对应是叶子节点,最底层盘子对应根节点。

为了更好理解,可以写成这样:

    public static int moveTree(int n,char A, char B, char C) {
        if(n==1)
            System.out.println(String.format("第 %s 层(叶子节点):%s -> %s",Hanoi.n-n+1, A, C));
        else {
            moveTree(n-1, A, C, B);//每次都是输出A->C,所以要看到A->B,就需要将B和C交换

            if(n==Hanoi.n)
                System.out.println(String.format("第 %s 层(根节点):%s -> %s", Hanoi.n-n+1, A, C));
            else
                System.out.println(String.format("第 %s 层(根节点):%s -> %s", Hanoi.n-n+1, A, C));

            moveTree(n-1, B, A, C);//每次都是输出A->C,所以要看到B->C,就需要将A和B交换
        }
        //汉诺塔的移动次数为: 2^n-1
        return (int) Math.pow(2, n)-1;
    }

汉诺塔递归实现与二叉树中序遍历的递归实现,在代码实现上很类似

public static void inorder(TreeNode root) {
    if (root == null)
        return;
    inorder(root.left);
    System.out.print(root.val);
    inorder(root.right);
}

汉诺塔的移动步骤可以用满二叉树的中序遍历来表示,反过来,我们可以通过满二叉树的特性推导出汉诺塔的一些特性:

  • 满二叉树总的结点数为,所以汉诺塔移动次数为

  • 满二叉树第n层的节点数为,所以n阶汉诺塔第i个盘子被移动的次数为

  • 满二叉树叶子节点数为,所以汉诺塔第一个盘子被移动的次数为

  • 满二叉树是二进制的一种表现形式,所以汉诺塔也是二进制的一种表现形式,其中汉诺塔的移动过程就是二进制的累加过程。

最后附上三者的关系图

image

总结

如果这些结论都是自己推导发现的话,你会发现充满惊喜。其推导过程非常有意思,好像冥冥之中万物都和二进制相关。文章想表达的不仅仅是得出汉诺塔有哪些特性,更重要的是希望能在学习中,发现学习本身的乐趣,从而滋养内在的好奇心、探索精神,不断地自我推进,让学习越来越有趣越有动力。

image

自己编写平滑加权轮询算法,实现反向代理集群服务的平滑分配

Java实现平滑加权轮询算法--降权和提权

Java实现负载均衡算法--轮询和加权轮询

Java全栈学习路线、学习资源和面试题一条龙

更多优质文章,请关注WX公众号: Java全栈布道师

image

原创不易,觉得写得还不错的,三联支持↓

标签:二进制,System,汉诺塔,二叉树,println,移动,out
From: https://www.cnblogs.com/dennyLee2025/p/17288191.html

相关文章

  • 222. 完全二叉树的节点个数
    给你一棵完全二叉树的根节点root,求出该树的节点个数。完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第h层,则该层包含1~2h个节点。classSolution{public:......
  • 111. 二叉树的最小深度
    给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。说明:叶子节点是指没有子节点的节点。classSolution{public:intminDepth(TreeNode*root){if(root==nullptr)return0;if(!root->left&&!root->right......
  • 104.二叉树的最大深度
    给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明:叶子节点是指没有子节点的节点。示例:给定二叉树[3,9,20,null,null,15,7],classSolution{public:intgetdepth(TreeNode*node){if(node==NULL)return0;......
  • #yyds干货盘点# LeetCode面试题:二进制求和
    1.简述:给你两个二进制字符串a和b,以二进制字符串的形式返回它们的和。 示例 1:输入:a="11",b="1"输出:"100"示例 2:输入:a="1010",b="1011"输出:"10101"2.代码实现:classSolution{publicStringaddBinary(Stringa,Stringb){......
  • 代码随想录Day20-Leetcode654.最大二叉树,617.合并二叉树,700.二叉搜索树中的搜索,98.验
    654.最大二叉树题目链接:https://leetcode.cn/problems/maximum-binary-tree/基本的模拟思路很快/***Definitionforabinarytreenode.*functionTreeNode(val,left,right){*this.val=(val===undefined?0:val)*this.left=(left===undefined......
  • 树:剑指 Offer 37. 序列化二叉树
    题目描述:请实现两个函数,分别用来序列化和反序列化二叉树。你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列/反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。提示:输入输出格式与LeetCo......
  • 最小覆盖子串(哈希表、字符串)、两数之和(数组、哈希表)、二叉树的层序遍历 II(树、广
    最小覆盖子串(哈希表、字符串)给你一个字符串s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串""。**注意:**如果s中存在这样的子串,我们保证它是唯一的答案。示例1:输入:s="ADOBECODEBANC",t="ABC"输出:"B......
  • 【数据结构】二叉树先序、中序、后序及层次遍历(C语言版)
    一、图示展示1.先序遍历先序遍历可以想象为,一个小人从一棵二叉树根节点为起点,沿着二叉树外沿,逆时针走一圈回到根节点,路上遇到的元素顺序,就是先序遍历的结果先序遍历结果为:ABDHIEJCFKG动画演示:记住小人沿着外围跑一圈(直到跑回根节点),多看几次动图便能理解2.......
  • 二叉树
    树的结构一棵二叉树又三个部分组成:根节点左子树右子树我们将树的结构定义如下:classTreeNode{public: intval; TreeNode*left; TreeNode*right; intheight; TreeNode(intx):val(x),left(nullptr),right(nullptr),height(1){}; TreeNode():TreeNode(0){};};......
  • POJ 2773 Happy 2006 二分+容斥原理(二进制枚举或dfs)
    Happy2006TimeLimit: 3000MS MemoryLimit: 65536KTotalSubmissions: 14003 Accepted: 4946DescriptionTwopositiveintegersaresaidtoberelativelyprimetoeachotheriftheGreatCommonDivisor(GCD)is1.Forinstance,1,3,5,7,9...areallrelativel......