文章目录

  • 一、最具启发性的汉诺塔问题
    • 1、汉诺塔问题描述
    • 2、从最初开始
    • 3、优化
  • 二、打印一个字符串的全部子序列
    • 1、子序列定义
    • 2、代码
  • 三、打印一个字符串的全部子序列,没有重复值
  • 四、打印一个字符串的全部全排列
    • 1、全排列定义
    • 2、采用舍弃添加的方式
      • (1)递归过程如下
      • (2)代码
    • 3、一直在原始字符串上以交换的方式进行递归
      • (1)递归过程如下
      • (2)代码
  • 五、打印一个字符串的全部全排列,没有重复值
    • 1、代码
    • 2、为啥我们不采用Set的方式来去重?
  • 六、栈的逆序
    • 1、得到栈底元素
      • (1)代码
      • (2)过程
    • 2、递归逆序
      • (1)代码
      • (2)过程

本文篇幅较长,建议仔细耐心看完,相信会有极大的收获。

一、最具启发性的汉诺塔问题

1、汉诺塔问题描述

有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:每次只能移动一个圆盘;大盘不能叠在小盘上面。

问:最少要移动多少次?并打印每次移动的情况。

2、从最初开始

我们的目标是将1、2、3圆盘从左杆移到右杆

可以拆解为以下三步

1)(大步)将1、2圆盘从左移到中杆

2)将3圆盘从左移到右杆

3)(大步)将1、2圆盘从中移到右杆

代码如下:

/*** @author Java和算法学习:周一*/
public static void leftToRight(int n) {if (n == 1) { // base caseSystem.out.println("Move 1 from left to right");return;}// 1、1到n-1个圆盘从左到中leftToMid(n - 1);// 2、从左到右System.out.println("Move " + n + " from left to right");// 3、1到n-1个圆盘从中到右midToRight(n - 1);
}

整个左到右方法,你会发现第一大步依赖左到中子方法,第三大步依赖中到右子方法,然后你去补起左到中方法、中到右方法;此时又会发现左到中依赖左到右、右到中方法,中到右依赖中到左、左到右方法……

/*** @author Java和算法学习:周一*/
private static void leftToMid(int n) {if (n == 1) {System.out.println("Move 1 from left to mid");return;}leftToRight(n - 1);System.out.println("Move " + n + " from left to mid");rightToMid(n - 1);
}private static void midToRight(int n) {if (n == 1) {System.out.println("Move 1 from mid to right");return;}midToLeft(n - 1);System.out.println("Move " + n + " from mid to right");leftToRight(n - 1);
}private static void rightToMid(int n) {if (n == 1) {System.out.println("Move 1 from right to mid");return;}rightToLeft(n - 1);System.out.println("Move " + n + " from right to mid");leftToMid(n - 1);
}private static void midToLeft(int n) {if (n == 1) {System.out.println("Move 1 from mid to left");return;}midToRight(n - 1);System.out.println("Move " + n + " from mid to left");rightToLeft(n - 1);
}private static void rightToLeft(int n) {if (n == 1) {System.out.println("Move 1 from right to left");return;}rightToMid(n - 1);System.out.println("Move " + n + " from right to left");midToLeft(n - 1);
}

最后你会发现左到中、左到右、中到左、中到右、右到左、右到中这6个方法相互依赖完成了汉诺塔问题。

是不是有点神奇,递归有时候是有点玄学,但是只要把子过程想明白,base case写对,就跑出来了(同时,需要有一点宏观思维)。

3、优化

这6个过程,是不是有点麻烦,同时细心的伙伴可能也发现了,这6个方法是及其相似的,那么我们是不是可以定义from、to、other三个变量,他们都可以表示左、中、右。当我左到右时,from=左、to=右、other=中;当左到中时,from=左、to=中、other=右……是不是就能六合一召唤神龙了。

同样拆解为以下三步

1)(大步)将1、2圆盘(即上面n-1个圆盘)从左移到中杆

2)将3圆盘(即最大的圆盘)从左移到右杆

3)(大步)将1、2圆盘(即上面n-1个圆盘)从中移到右杆

/*** 第一次调用function时:from=左、to=右、other=中* 表示借助other=中,将圆盘从from=左移动到to=右杆上** @author Java和算法学习:周一** @param n     总共的圆盘数量* @param from  起始位置* @param to    目标位置* @param other 剩余杆子*/
public static void function(int n, String from, String to, String other) {if (n == 1) {System.out.println("Move 1 from " + from + " to " + to);return;}// 1.将上面n-1个圆盘从左移到中杆上,// 即起始位置为左,当前from=左;目标位置中,当前other=中function(n - 1, from, other, to);// 2.将最大的圆盘从左移到右杆上System.out.println("Move " + n + " from " + from + " to " + to);// 3.将上面n-1个圆盘从中移到右杆上// 即起始位置为中,当前other=中;目标位置右,当前to=右function(n - 1, other, to, from);
}

这时候,我们就学会了一个技巧,一个递归函数可以通过增加参数的方式表达更多的可能性,听着跟废话一样,但是现在你再品品。

但是,若没有前面启发性的过程,直接看这个是不是有点难懂;有了启发性的过程,再看是不是豁然开朗

是不是发现上课时听老师讲汉诺塔时一头雾水,要是老师当时能这么讲绝对听的明明白白。此处应有掌声(和点赞)

一气呵成,再来看看几个递归过程。

二、打印一个字符串的全部子序列

1、子序列定义

对于字符串"12345",任意取其中0个、1个、2个、3个、4个、5个都是它的子序列,同时相对顺序不能改变。

对于字符串“123”,我们可以很容易分析出以下递归过程

2、代码

/*** @author Java和算法学习:周一*/
public static List<String> getAllSubSequences(String s) {char[] str = s.toCharArray();List<String> answer = new ArrayList<>();String path = "";process1(str, 0, answer, path);return answer;
}/*** 当前来到了str[index]字符* str[0..index-1]已经走过了,之前的选择都在path上,之前的选择已经不能改变了,就是path。* 但是str[index....]还能自由选择,把str[index....]所有生成的子序列,放入到answer里** @param str    指定的字符串(固定)* @param index  当前所处的位置* @param answer 之前决策依据产生的答案* @param path   之前已经做的选择*/
public static void process1(char[] str, int index, List<String> answer, String path) {// 当前来到了字符串的最后位置,已经不能再做决策了,answer只能放入之前的决策if (index == str.length) {answer.add(path);return;}// 当前没有要index位置的字符process1(str, index + 1, answer, path);// 当前要index位置的字符process1(str, index + 1, answer, path + str[index]);
}

process1方法就是上面分析出来的递归过程。

三、打印一个字符串的全部子序列,没有重复值

打印一个字符串的全部子序列,要求没有重复字面值的子序列,直接将上面的List换成Set即可去重。

/*** 打印一个字符串的全部子序列,要求没有重复字面值的子序列** @author Java和算法学习:周一*/
public static Set<String> getAllSubSequencesNoRepeat(String s) {char[] str = s.toCharArray();Set<String> answer = new HashSet<>();String path = "";process2(str, 0, answer, path);return answer;
}public static void process2(char[] str, int index, Set<String> answer, String path) {if (index == str.length) {answer.add(path);return;}process2(str, index + 1, answer, path);process2(str, index + 1, answer, path + str[index]);
}

四、打印一个字符串的全部全排列

1、全排列定义

所有字符都要,只是顺序不同。

2、采用舍弃添加的方式

(1)递归过程如下

在第一大列选择a时,形成了一些结果,当我进行第二大列递归时,得把a添加回去,保持最开始的abc,在第二大列选择b时进行递归得到的结果才是正确的,对于每一列的每一位选择都是如此,递归结束后要恢复现场

(2)代码

/*** 1.添加删除的方式** @author Java和算法学习:周一*/
public static List<String> permutation1(String s) {List<String> answer = new ArrayList<>();if (s == null || s.length() == 0) {return answer;}ArrayList<Character> strList = new ArrayList<>();for (char c : s.toCharArray()) {strList.add(c);}String path = "";process1(strList, path, answer);return answer;
}/*** 递归获取全排列** @param strList 当前参与选择的所有字符* @param path 之前所做的选择* @param answer 最终结果*/
private static void process1(ArrayList<Character> strList, String path, List<String> answer) {// 当前没有可以选择的字符了,answer只能放入之前的选择if (strList.isEmpty()) {answer.add(path);return;}for (int i = 0; i < strList.size(); i++) {// 当前选择的字符char cur = strList.get(i);// 舍弃已经选择的字符strList.remove(i);// 剩余字符再进行选择process1(strList, path + cur, answer);// 恢复现场strList.add(i, cur);}
}

3、一直在原始字符串上以交换的方式进行递归

在第一大列形成结果acb时,如果不恢复现场,当我进行第二大列递归时,是从acb开始进行0、1位交换形成cab,和后面的重复了,所以每一步交换递归结束后要恢复现场

(1)递归过程如下

(2)代码

/*** 2.交换的方式** @author Java和算法学习:周一*/
public static List<String> permutation2(String s) {List<String> answer = new ArrayList<>();if (s == null || s.length() == 0) {return answer;}char[] str = s.toCharArray();process2(str, 0, answer);return answer;
}/*** 递归获取全排列** @param str 当前经历过交换后的字符* @param index 当前交换到哪个位置了* @param answer 结果*/
private static void process2(char[] str, int index, List<String> answer) {// 当前来到了字符串的最后位置,已经不能再交换了,answer只能放入之前交换后的字符if (index == str.length) {answer.add(String.valueOf(str));return;}// index之前的已经交换过不能再变了,所以从index往后还可以再交换for (int i = index; i < str.length; i++) {// index、i位置交换swap(str, index, i);// index后面的继续交换process2(str, index + 1, answer);// index、i位置 恢复现场swap(str, index, i);}
}

五、打印一个字符串的全部全排列,没有重复值

1、代码

/*** 交换的方式,去重** @author Java和算法学习:周一*/
public static List<String> permutation3(String s) {List<String> answer = new ArrayList<>();if (s == null || s.length() == 0) {return answer;}char[] str = s.toCharArray();process3(str, 0, answer);return answer;
}/*** 递归获取全排列,没有重复的字符串** @param str 当前经历过交换后的字符* @param index 当前交换到哪个位置了* @param answer 结果*/
private static void process3(char[] str, int index, List<String> answer) {// 当前来到了字符串的最后位置,已经不能再交换了,answer只能放入之前交换后的字符if (index == str.length) {answer.add(String.valueOf(str));return;}boolean[] visited = new boolean[256];// index之前的已经交换过不能再变了,所以从index往后还可以再交换for (int i = index; i < str.length; i++) {// str[i]位置对应字符没有出现过才递归交换,否则忽略if (!visited[str[i]]) {visited[str[i]] = true;// index、i位置交换swap(str, index, i);// index后面的继续交换process3(str, index + 1, answer);// index、i位置 恢复现场swap(str, index, i);}}
}

2、为啥我们不采用Set的方式来去重?

仔细看以上代码,发现没有再用Set的方法来去重,为啥?

因为采用Set去重,程序会重复的做很多相同字符的递归操作,将产生的相同字符串放到Set中由Set去重;而采用以上方式,对于相同的字符就不会再重复的递归了,有效减少了重复分支的递归操作,俗称剪枝

相当于Set是从结果中过滤去重,而以上方式是在中途的过程就已经去重了

六、栈的逆序

给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?

1、得到栈底元素

(1)代码

/*** 得到栈底元素,剩余元素直接下沉** 例如,从栈顶到栈底元素为1、2、3、4、5* 此方法返回5,剩余从栈顶到栈底元素为1、2、3、4** @author Java和算法学习:周一*/
private static int getDown(Stack<Integer> stack) {int result = stack.pop();if (stack.isEmpty()) {return result;} else {int last = getDown(stack);stack.push(result);return last;}
}

(2)过程

2、递归逆序

(1)代码

/*** @author Java和算法学习:周一*/
public static void reverse(Stack<Integer> stack) {if (stack.isEmpty()) {return;}int down = getDown(stack);reverse(stack);stack.push(down);
}

(2)过程

本文所有代码

Github地址:https://github.com/monday-pro/algorithm-study/tree/master/src/basic/dynamicprogramming/recursion

Gitee地址:https://gitee.com/monday-pro/algorithm-study/tree/master/src/basic/dynamicprogramming/recursion

怎么样,是不是如沐春风般醍醐灌顶。

从 最具启发性的汉诺塔问题开始 聊递归相关推荐

  1. 【汉诺塔】C语言递归解法,深层次地带你理解汉诺塔公式

    目录 汉诺塔公式 汉诺塔问题在数学层面的公式: C语言递归公式 两层汉诺塔 三层汉诺塔 递归问题可谓是学习C语言以来的第一个拦路虎,而汉诺塔问题更是递归中对新手很不友好的一道经典题,我们接下来从公式角 ...

  2. java实现求解汉诺塔问题(提示, 使用递归)

    实现代码: 求解汉诺塔问题(提示, 使用递归) 汉诺塔问题是一个经典的问题.汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说. 大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从 ...

  3. 个盘子的汉诺塔需要移动几步_图解汉诺塔问题( Java 递归实现)

    汉诺塔简介 最近在看数据结构和算法,遇到了一个非常有意思的问题--汉诺塔问题. 先看下百度百科是怎么定义汉诺塔的规则的: 汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候 ...

  4. 用java编写汉诺塔问题_数据结构与算法之汉诺塔问题(Java递归)

    汉诺塔问题: 有三根柱子,源杆A,暂存杆temp,目的杆C A上有n层盘子,由小到大向下排列,现需要将A杆的盘子移到C杆中 要求:1)大的盘在下面,小的盘在上面 2)一次只能移动一个盘子 个人思路:先 ...

  5. c语言函数汉诺塔不用move,C语言——汉诺塔问题(函数递归)

    问题概述:古代有一个梵塔,塔内有3个座A,B,C.开始时A座上有64个盘子,盘子大小不等,大的在下,小的在上,有一个老和尚想把64个盘子从A座移动到C座,但是规定每次只允许移动一个盘,且在移动过程中在 ...

  6. 经典汉诺塔(Java初学递归篇)

    大一学C的时候已经接触到汉诺塔递归的问题,当时只是简单了解过方法,最近开了算法课,打算重新捋一捋. 题目描述:         有三根柱子分别为A.B.C,柱子A上从下到上按金字塔状叠放着n个不同大小 ...

  7. 借汉诺塔理解栈与递归

    我们先说,在一个函数中,调用另一个函数. 首先,要意识到,函数中的代码和平常所写代码一样,也都是要执行完的,只有执行完代码,或者遇到return,才会停止. 那么,我们在函数中调用函数,执行完了,就会 ...

  8. 由汉诺塔引起的对递归的思考

    对递归的理解在于放弃,放弃对于全程的理解与跟踪,只理解递归两层之间相互的联系,以及递归终结的条件. 汉诺塔永远只有两层,最底层和上层,上层放到中间,底层放好,再把中间的放到底层上面!!! 就这样,在乱 ...

  9. c语言递归汉诺塔次数,c语言递归解决汉诺塔参数变化的疑惑

    c语言递归解决汉诺塔参数变化的疑惑 答案:3  信息版本:手机版 解决时间 2020-04-05 14:20 已解决 2020-04-05 10:49 #include void main() {vo ...

最新文章

  1. UNL/EVE关联putty和wireshark
  2. 魅族大数据之流平台设计部署实践--转
  3. flatMap()和事件顺序– RxJava常见问题解答
  4. Oracle Service Bus简介
  5. Jenkins持续集成实践之java项目自动化部署
  6. Spring Boot2 集成 jasypt 3.0.4 配置文件敏感信息加密
  7. 跨平台桌面应用开发工具Electron v11.0.4
  8. Ubuntu环境下sublime3 nodejs安装与插件配置
  9. [转]Handler学习笔记(一)
  10. 蓝牙4.0BLE 芯片cc2540和cc2541 128x64 oled显示屏驱动
  11. 笔记本选购指南2020双十一
  12. 老牌安全公司CYBER ARK眼中的RPA部署安全问题
  13. Spring实训 个人博客二 详情页
  14. 鸿蒙系统的用途,华为高级副总裁谈鸿蒙系统:主要为工业用途
  15. 数字调制BPSK/QPSK/QAM/ASK/FSK/PSK
  16. 开源Cortex-M模拟器QEMU的使用方法
  17. linux查看硬件PCI设备,Linux系统查看硬件信息神器,比pci设备好用100倍!
  18. 根据年月以及月中周次,获取该周开始,结束日期
  19. Everyone Piano v2.2.10.16 WiN 电脑键盘钢琴模拟软件
  20. Windows无法连接到打印机,拒绝访问。

热门文章

  1. 计算机房维修保养记录表,机房设备、系统运行及维护记录表
  2. 【mysql5.7】组内分组排序
  3. java map合并_详解Java8合并两个Map中元素的正确姿势
  4. Android3彩蛋,Android升级gradle5的坑+Androidstudio3.4小彩蛋
  5. linux c语言俄罗斯方块 头文件,C语言写的俄罗斯方块程序
  6. Nginx 反向代理、负载均衡、页面缓存、URL重写及读写分离详解
  7. 原来,这才是项目管理的真相
  8. 《统计学习方法》第10章 隐马尔科夫模型 HMM算法 纯Python代码实现 + 前后向算法矩阵形式 + 课后习题答案
  9. 渗透测试之路:ThinkPHP漏洞复现
  10. vulhub-thinkphp漏洞复现