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

题目描述

在经典汉诺塔问题中,有 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 变形与延伸
题目变形
  • (练习)如果规则变为大盘必须放在小盘上该如何实现?
延伸扩展
  • 递归是一种基本的编程思维
    • 快速排序、归并排序、二分查找、树……
  • 实际工作中应用很广泛
    • 按照组织架构统计公司某部门人数
    • 按职级关系统计销售人员提成
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容