美文网首页
算法 2.1 递归:求汉诺塔问题

算法 2.1 递归:求汉诺塔问题

作者: 珺王不早朝 | 来源:发表于2021-01-22 21:25 被阅读0次

题目描述

在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。
一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。
移动圆盘时受到以下限制:
  (1) 每次只能移动一个盘子;
  (2) 盘子只能从柱子顶端滑出移到下一根柱子;
  (3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
你需要原地修改栈
A中盘子的数目不大于14个。

输入:A = [2, 1, 0], B = [], C = []
输出:C = [2, 1, 0]

输入:A = [1, 0], B = [], C = []
输出:C = [1, 0]

数据结构

  • 链表、栈

算法思维

  • 递归

解题要点

  • 递归思维的灵活运用

关键知识点:递归

  • 概念
    数学与计算机科学范畴,是指在函数的定义中使用函数自身的方法
    递归算法是一种直接或者间接调用自身函数的算法
  • 本质
    递归,去的过程叫"递",回来的过程叫"归"
    递是调用,归是结束后回来
    是一种循环,而且在循环中执行的就是调用自己
  • 三要素
    结束条件
    函数主功能
    函数的等价关系式(参数、返回值、关系)
  • 递归方法模板
public int recursion(int n) {
    //1.结束条件
    if (n == 1) {
        return 1; 
    }
    
    //2.函数主功能 do something
    System.out.println(n);

    //3.等价关系式 f(n)=n+f(n-1) 转换为简单问题
    return n + recursion(n - 1);
}

解题步骤


一. Comprehend 理解题意
题目主干
  • 通过程序解决汉诺塔问题
  • 圆盘数量不固定
细节问题
  • 严格遵守汉诺塔规则
附加限制
  • 原地修改
宽松限制
  • A中盘子的数目不大于14个

二. Choose 选择数据结构与算法
数据结构选择
  • 输入和输出的参数类型已经限定为 List
  • 其中 “盘子只能从柱子顶端滑出移到下一根柱子”
  • 与栈的后进先出的特点一致
  • 但输入输出参数均为 List
  • 每次都在 List 尾部添加或者移除(模拟栈的方式)
算法思维选择

美国学者提出的一种两步操作法:
将柱子摆成品字型,摆放顺序由圆盘数量奇偶性决定
  思考1层汉诺塔、2层汉诺塔如何移动
  • 若n为奇数,按顺时针方向依次摆放 A C B
  • 若n为偶数,按顺时针方向依次摆放 A B C

解题思路
  1. 按顺时针方向把圆盘1从现在的柱子移动到下一根柱子
  2. 把另外两根柱子上可以移动的圆盘移动到新的柱子上
    • 把非空柱子上的圆盘移动到空柱子上
    • 当两根柱子都非空时,移动较小的圆盘
  3. 反复进行1、2两步操作,最后就能按规定完成汉诺塔的移动

三. Code 编码实现基本解法
解题思路剖析

判断盘子总数奇偶性,例如为偶数,则顺序为 ABC
1.最小盘子移动到下一个柱子
2.另两个柱子较小的盘子移动到另一个
不断重复1、2两步

class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        int size = A.size();
        List<Integer>[] lists = new List[3];
        lists[0] = A;
        if (size % 2 == 0) {//若n为偶数,按顺时针方向依次摆放 A B C;
            lists[1] = B;
            lists[2] = C;
        } else { // 若n为奇数,按顺时针方向依次摆放 A C B
            lists[1] = C;
            lists[2] = B;
        }
        int currentIndex = 0; //记录最小盘子所在的柱子下标
        while (C.size() < size) {
            List<Integer> current = lists[currentIndex];
            currentIndex = (currentIndex + 1) % 3; //编号最小盘子所在柱子的下一个柱子
            List<Integer> next = lists[currentIndex];
            List<Integer> pre = lists[(currentIndex + 1) % 3]; //编号最小盘子所在柱子的上一个柱子
            int preSize = pre.size();
            int curSize = current.size();
            next.add(current.remove(--curSize)); //最小的圆盘 移动到下一个柱子
            //另外两根柱子上可以移动的圆盘移动到新的柱子上 当两根柱子都非空时,移动较小的圆盘
            int plateToMove1 = preSize == 0 ? Integer.MAX_VALUE : pre.get(preSize - 1);
            int plateToMove2 = curSize == 0 ? Integer.MAX_VALUE : current.get(curSize - 1);
            if (plateToMove1 < plateToMove2) {
                current.add(pre.remove(preSize - 1));
            } else if (plateToMove2 < plateToMove1) {
                pre.add(current.remove(curSize - 1));
            }
        }
    }
}

时间复杂度:O(2n)
  • 需要多次比较盘子大小以及移动盘子,移动次数为 2n-1
  • 在每次移动前还需要进行比较,比较次数也为 2n-1

空间复杂度:O(1)
  • 常数级变量空间 O(1)

执行耗时:6 ms,击败了 7.21% 的Java用户
内存消耗:37.4 MB,击败了 77.97% 的Java用户

四. Consider 思考更优解
寻找更好的算法思维 -- 递归

N层汉诺塔 该如何思考呢?

  • 如果我们能将上面的 N-1 层移动到B上
  • 把N层移动到C,再把B上N-1层移动到C上就可以解决问题了
  • 问题变为如何解决N-1层汉诺塔的移动问题
  • 继续思考一直到N-1等于1时,我们可以直接将1层汉诺塔移动目的位置

五. Code 编码实现最优解
解题思路剖析
  • 递归结束条件:移动一个盘子
  • 递归函数主功能
      • 移动 N-1 个盘子到中间柱子
      • 移动第 N 个盘子到目标柱子
      • 将 N-1 个盘子从中间柱子移动到目标柱子
  • 函数的等价关系式
      • 参数:本轮盘子数量、三个柱子(List)
      • 返回值:无返回值,使用List中本地删除的方式
      • 等价关系:f(n,A,B,C) = f(n-1,A,C,B) + M(A,C) + f(n-1,B,A,C)
class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        movePlate(A.size(), A, B, C);
    }

    /**
     * @param size      需要移动盘子的数量
     * @param start     起始柱子
     * @param auxiliary 辅助柱子
     * @param target    目标柱子
     */
    private void movePlate(int size, List<Integer> start, List<Integer>
            auxiliary, List<Integer> target) {
        if (size == 1) { // 只剩一个盘子时,直接从第一个柱子移动到第三个柱子 即可
            target.add(start.remove(start.size() - 1));
            return;
        }
        // 将 n-1 个盘子,从 第一个柱子 移动到 第二个柱子
        movePlate(size - 1, start, target, auxiliary);
        // 将第 n个盘子,从 第一个柱子 移动到 第三个柱子
        target.add(start.remove(start.size() - 1));
        // 再将 n-1 个盘子,从 第二个柱子 移动到 第三个柱子
        movePlate(size - 1, auxiliary, start, target);
    }
}

时间复杂度:O(2n)
使用迭代法分析
  • 设递归函数的运行时间 T(n)
  • 每轮递归调用两次递归函数,每调用一次问题规模减少 1,T(n) = 2×T(n - 1)
  • 所以最终的复杂度为 O(2n),其中比较次数为 0,省略了比较大小的步骤

空间复杂度:O(n)
  • 函数中我们不需要主动创建额外存储
  • 但递归调用本身需要进行堆栈的存储
  • 空间复杂度和递归的深度有关系
  • 递归深度为 n,所以空间复杂度为 O(n)

执行耗时:1 ms,击败了 84.31% 的Java用户
内存消耗:37.7 MB,击败了 13.28% 的Java用

六. Change 变形与延伸
题目变形
  • (练习)如果规则变为大盘必须放在小盘上该如何实现?
延伸扩展
  • 递归是一种基本的编程思维
    • 快速排序、归并排序、二分查找、树……
  • 实际工作中应用很广泛
    • 按照组织架构统计公司某部门人数
    • 按职级关系统计销售人员提成

相关文章

网友评论

      本文标题:算法 2.1 递归:求汉诺塔问题

      本文链接:https://www.haomeiwen.com/subject/nsuezktx.html