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

发布于:2024-12-18 ⋅ 阅读:(63) ⋅ 点赞:(0)

  

 


汉诺塔问题


     题目解析    



     什么是汉诺塔问题     


  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. 让当前节点后面的链表先逆置,并且把头节点返回(怎么返回,怎么逆置都不关心)
  • 2. 让当前节点添加到逆置的链表之后即可; 
  • 3. 如果当前节点为 null,结束递归

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


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


     动态模拟过程     


    编写代码    


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


两两交换链表中的节点


     题目解析    



    算法原理    


     解法:递归    




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


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

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


    处理细节问题    


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


    编写代码    




 Pow(x,n) - 快速幂


     题目解析    



    算法原理    


      解法一:暴力循环     


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


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


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


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

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


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

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


    递归过程    



    编写代码    


    实现快速幂:递归