【递归,搜索与回溯算法 & 递归算法】递归算法入门详解:递归算法小专题


汉诺塔问题


题目解析



什么是汉诺塔问题


  1. 通过三根柱子,把最左边柱子上的圆环移动到最右边;
  2. 每次只能移动一个圆环;
  3. 小圆环必须在大圆环之上 ;

汉诺塔问题的简单过程模拟



算法原理


为什么汉诺塔问题可以使用递归来解决呢?(Why?)

1. 如何解决汉诺塔问题?


要想知道为什么能使用递归来解决汉诺塔问题,我们先来解决:如何解决汉诺塔问题?


总结规律


解决两层汉诺塔的基本流程


对于2层汉诺塔的解决步骤:


解决三层汉诺塔的基本流程


对于3层汉诺塔的解决步骤:

所以,当胖子有 n 个时,先把底下最大的盘子移动到 poleC 上,那么就需要把上面 n-1 个盘子移动到 poleB 上;那当盘子有 n-1 个,也是同样的操作;

规律

当 N=n 时,我们只需要把第 n 个盘子先移动到 poleC 即可,此时就需要把 n -1 个盘子先移动到辅助杆 poleB 上,那么这 n-1 个盘子怎么移动呢?

在 N=n-1 的时候,我们已经解决过这个问题了,直接拿来用即可;


解法:递归 (How?)



重复子问题 (函数头)


对于上图列出的情况,无论 N 等于多少,都要重复上面的 1,2,3 步:


重复子问题就是把某一根柱子x上的盘子,借助某一根柱子y,放到另一个柱子z上:

  • 设置函数头:

void dfs( poleX , poleY, poleZ, 盘子数量) ;


  • 作用:

给 dfs()传入三个柱子和盘子的数量,dfs()就可以把 x 上的 N 个盘子,借助 y 的帮助,传入到 z 上;


设置函数体


设计函数体,只需要关心某一个子问题在做什么,也就是 N 为某一个值时,盘子的移动流程;


  • 第一步:借助 z 柱子,把 x 上的 n-1 个盘子移动到 y 上
  • 函数体的第一步: dfs( x , z , y , n-1)

  • 第二步:把 x 底下最大的盘子,从 x 移动到 z :
  • 函数体的第二步:x.back(),这个方法的作用是,把 x 最底下的盘子移动到 z 上;

  • 第三步:把 y 上的 n-1 个盘子,借助 x 移动到 z 上:
  • 函数体的第三步 dfs( y, x, z, n-1);

递归出口


我们发现,当 N=1 时的处理过程,和 N>1 的处理过程完全不一样:

当 N=1,就不需要借助辅助柱子,直接移动即可;

所以N=1时处理盘子的方法,就是汉诺塔问题的递归出口; x.back();


编写代码


方法一:递归



递归细节展开图


方法二:不讲武德

如果是笔试题,可以直接这样 ac,因为:

但是如果是面试题这样写,就回家等通知吧哈哈哈!!!


合并两个有序链表


题目解析



算法原理


解法:递归


(1)寻找重复子问题 (设计函数头)


我们定义两个指针 left1,left2, 返回两个链表合并后的有序链表的头指针

因为这两个指针都是升序的,在刚开始遍历链表时,比较两条链表头节点,以较小节点作为最终合并链表后,返回的头节点;

接下来,就比较剩下的两条链表,找到两个新头节点中较小的节点,作为接下来新链表的下一个节点;

所以重复的子问题:是合并链表,返回新链表的下一个节点;

所以函数头为 Node dfs(Node l1, Node l2),就是合并完链表,返回每次合并链表的末尾节点;


(2)某一个子问题的具体流程 (函数体)


  • 第一步,比较大小;假设较小值为 l1;

  • 第二步,让较小节点连接两条链表剩余部分合并后的结果即可;l1.next = dfs(l1.next , l2);

  • 返回合并链表的新加入节点 return l1;(假设 l1 ,l2 两个指针,l1指向较小的节点)

(3)递归出口


什么时候子问题不能再细分了呢?当两个指针其中一个指向对应链表的尾节点,就可以停止递归,返回另一个指针即可;此时另一条链表的剩余部分,会和合并链表的尾节点连接;


(4)编写代码


注意:无论当前节点是子问题还是递归出口,在拼接好剩余链表的合并部分后,都要返回当前节点;


拓展


(1) 递归问题和循环问题可以互相转换


递归和循环都是方法处理一个子问题,两者可以通过修改互相转换


(2) 使用递归&循环的时机


而如果通过递归解决的问题,要通过循环来解决,我们就需要借助栈,因为在递归时的一个代码块方法,在递归后该方法节点并没有完全执行完,等下层节点回溯之后,还会继续执行方法剩余部分;


但是如果使用循环,我们就需要使用栈来保存方法节点未执行完的信息,如果是解决汉诺塔问题,递归代码会非常简洁,而使用循环,那么代码成本就会大幅度提高;

但是如果递归展开图不是树形结构,那么把递归修改成循环也是比较容易的(不绝对),如遍历数组;对于使用递归遍历数组,子问题就是,给一个数组元素,把剩余的数组元素打印出来;

所以对于大多数情况下,如果递归图越复杂,越适合使用递归,反之则适合使用循环;


(3) 修改递归顺序可以改变遍历顺序



我们修改这两条代码的先后顺序,虽然递归图相同,但是遍历数组的顺序会被修改;


(4) 通过打印日志调试



反转链表


题目解析



算法原理


视角一: 从宏观角度看待问题



我们可以先让 head.next.next = head:


再让 head.next = null 即可翻转前两个节点:

但是此时会出现一个问题,就是第三个节点丢失了;如果先修改,会出现丢失节点信息的情况;

既然先修改前面的节点指向会出现节点信息丢失,我们就调整修改顺序:

算法思路:

  1. 递归函数的含义: 交给你一个链表的头指针,你帮我逆序之后,返回逆序后的头结点;(想到递归函数的含义是这道题中最难的)
  2. 函数体: 先把当前结点之后的链表逆序,逆序完之后,把当前结点添加到逆序后的链表后面即可;
  3. 递归出口: 当前结点为空或者当前只有一个结点的时候,不用逆序,直接返回。
    1. 让当前节点后面的链表先逆置,并且把头节点返回(怎么返回,怎么逆置都不关心)
    1. 让当前节点添加到逆置的链表之后即可;
    1. 如果当前节点为 null,结束递归

视角二:把链表看成一棵树


链表其实就是一棵树形结构,如果要把链表逆置,只需要对这棵树进行一次后序遍历,并且返回头节点即可;


动态模拟过程


编写代码


给这个 dfs 传入节点指针,dfs自身就能把这个节点所连的链表逆置,并且把逆置链表的头节点返回:


两两交换链表中的节点


题目解析



算法原理


解法:递归




为什么 dfs 不是从节点2 往后开始逆置呢?我们的大问题是:把链表前两个节点和剩余链表全部逆置,前面两个节点是一个组合,所以 dfs 从第三个节点开始;怎么逆置我们不关心:


接下来,我们只需要把前两个节点进行交换;把交换后的结构,和交换好的链表连起来:

递归出口:如果是空节点,或者只剩下一个节点,那么就不需要交换,直接返回即可;


处理细节问题


需要记录一下原链表的第二个节点作为返回值,因为新链表的头节点位原链表的第二个节点;


编写代码




Pow(x,n) - 快速幂


题目解析



算法原理


解法一:暴力循环


如果直接使用暴力循环,会超时:


如果当 n 非常大时,比如 x^(2^31-1),计算这个数会导致超时问题;


解法二:使用递归实现快速幂


思考:为什么暴力循环慢呢?

通过上图,我们可以类比出,当 n 非常大时,计算次数也会异常庞大,从而导致计算超时;


但是,我们可以使用快速幂的方式,来减小了计算的次数:

上面的计算次数,和时间复杂度相等;


递归过程



编写代码


实现快速幂:递归


相关推荐
凡人的AI工具箱1 分钟前
每天40分玩转Django:实操 Todo List应用
数据库·后端·python·算法·django
唐骁虎24 分钟前
Spring Boot 项目的默认推荐目录结构详解
java·springboot
liuyunshengsir26 分钟前
crictl和ctr与docker的命令的对比
java·docker·eureka
Ttang2333 分钟前
Tomcat原理(5)——tomcat最终实现
java·开发语言·servlet·tomcat·intellij-idea
忘梓.39 分钟前
解锁动态规划的奥秘:从零到精通的创新思维解析(1)
算法·动态规划
7 :)1 小时前
第三月(下)第六章:反射+设计模式
java·jvm·设计模式
B站计算机毕业设计超人1 小时前
计算机毕业设计Python+Django农产品推荐系统 农产品爬虫 农产品商城 农产品大数据 农产品数据分析可视化 PySpark Hadoop
大数据·python·机器学习·网络爬虫·课程设计·数据可视化·推荐算法
OldGj_1 小时前
「LangChain4j入门 — JavaAI程序」
java·ai·langchain·langchain4j
就爱学编程1 小时前
重生之我在异世界学编程之C语言:深入预处理篇(下)
android·java·c语言
杰哥技术分享1 小时前
Public Key Retrieval is not allowed
java