• 数据结构与算法之对于递归的理解


    对于递归的理解

    前言

    视频观看更得劲:【小小福讲算法】硅谷工程师十五分钟带你深入理解 Recursion (递归)算法,及其衍生出的算法(分治算法 Divide and Conquer, 回溯 Ba_哔哩哔哩_bilibili


    简介

    递归是什么

    “Recursion is an approach to solving problems using a function that calls itself as a subroutine.” ---- LeetCode

    • 为什么 function 需要 call 自己?

      • 因为大问题(Big problem)可以分解成小问题(sub problems).
      • 小问题通常很容易解答!
      • 例如:跑步 10km 很难,但是跑步 10km =跑步 9km+ 先跑 1km,后者不再是一个艰难的任

    递归函数的结构

    递归函数由终止条件(Base)和递归关系(Recursion Relation)组成

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-et323Fer-1663382898115)(assets/image-20220913105939-9cnti01.png)]

    举个例子

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c8tNiQVj-1663382898117)(assets/image-20220913110041-eetulq6.png)]​

    递归在计算机中的实现方法

    • 任何一个函数调用,计算机会在内存中生成一块区域,用于存放函数的参数,返回地址等,这块区域叫做“栈(stack)”
    • 递归函数也是一种函数调用 → 因而也会生成一系列的 stack。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y46CpV1o-1663382898117)(assets/GIF 2022-9-13 11-03-29-20220913110354-og1s27e.gif)]

    例题

    在二叉搜索树中搜索某一结点

    算法步骤可以抽象为:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHN9YOey-1663382898118)(assets/image-20220913110642-14obazj.png)]

    递归的几种常见形式

    Memorization 缓存

    如果递归问题会产生重复的子问题,那么可以使用 cache 记住子问题的答案,避免重复计算。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WnBo5o3b-1663382898118)(assets/image-20220913110948-nnwtlle.png)]

    例题
    1. 斐波那契数列的计算:剑指 Offer 10- I. 斐波那契数列 - 力扣(LeetCode)
    2. 爬楼梯:70. 爬楼梯 - 力扣(LeetCode)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfPynYef-1663382898119)(assets/image-20220913111235-qt7hkd0.png)]

    Divide And conquer 分治

    Divide and Conquer 分而治之,几乎等同于标准的 Recursion,唯一的区别是,最后需要将子问题的结果进行合并!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uRmHxDbk-1663382898119)(assets/image-20220913111327-zm7cajb.png)]

    例题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLkzYdoV-1663382898120)(assets/image-20220913111508-mjnplxz.png)]

    算法模板

    分治算法的核心是寻找子问题的解 ,解题步骤遵循「三步走」:

    1. 找到子问题并分解
    2. 解决子问题(递归)
    3. 合并子问题的解

    Backtracking 回溯

    • 回溯的问题的形式:通常要求寻找满足所有 xx 条件的结果,并且问题可以使用递归实现。
    • 个人理解:一步一步先前探索,每一步尝试所有的可能,一旦发现当前不是可行解,立刻停止(知错就改)
    例题

    找出所有由 n 个 1 左右括号组成的有效的表达式:22. 括号生成 - 力扣(LeetCode)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d4ZWdDg-1663382898120)(assets/image-20220913112536-bt4o7ro.png)]

    具体的思路可以通过观看视频理解:【小小福讲算法】硅谷工程师十五分钟带你深入理解 Recursion (递归)算法,及其衍生出的算法(分治算法 Divide and Conquer, 回溯 Ba_哔哩哔哩_bilibili

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LUNYwIbp-1663382898120)(assets/image-20220913112812-f8z5yh4.png)]

    回溯问题的模板

    可以参考这篇文章:回溯法套路模板 刷通 leetcode - 知乎 (zhihu.com)

    设计思路

    1. 全局变量 : 保存结果
    2. 参数设计 : 递归函数的参数,是将上一次操作的合法状态当作下一次操作的初始位置。这里的参数,我理解为两种参数:状态变量条件变量 。(1)状态变量(state)就是最后结果(result)要保存的值;(2)条件变量就是决定搜索是否完毕或者合法的值。
    3. 完成条件 : 完成条件是决定 状态变量和条件变量 在取什么值时可以判定整个搜索流程结束。搜索流程结束有两种含义: 搜索成功并保存结果 和 搜索失败并返回上一次状态。
    4. 递归过程 : 传递当前状态给下一次递归进行搜索。

    套路模板

    res = []    # 定义全局变量保存最终结果
    state = []  # 定义状态变量保存当前状态
    p,q,r       # 定义条件变量(一般条件变量就是题目直接给的参数)
    def back(状态,条件1,条件2,……):
        if # 不满足合法条件(可以说是剪枝)
            return
        elif # 状态满足最终要求
            res.append(state)   # 加入结果
            return 
        # 主要递归过程,一般是带有 循环体 或者 条件体
        for # 满足执行条件
        if  # 满足执行条件
            back(状态,条件1,条件2,……)
    back(状态,条件1,条件2,……)
    return res
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用回溯法的明显标志

    1. 排列、组合(子集、幂集、字符全排列)。 在传值时,对于排列问题,是要删掉单个用过的元素;组合问题,是删掉前面所有的元素。
    2. 数组、字符串,给定一个特定的规则,尝试搜索迭代找到某个解。
    3. 二维数组下的 DFS 搜索(八皇后、黄金矿工、数独)

    总结

    • 递归是一种非常直观的解决复杂问题的方法。(重点就在于你怎么去把这个问题抽象出简单的几步)
    • 递归分两步:Base Case+Recursion Relation

    简化概念

    1. 第一、 明确递归函数的作用和参数的含义,然后坚定的相信它的作用

      1. 再次强调不要试图在脑中重演递归过程,当然你也做不到。你要做的便是:明确递归函数的作用,包括参数是什么,是否有返回值,有的话返回值是什么,然后相信它就行了。
    2. 第二、找到递归基

      1. 所谓递归基,就是递归结束的条件。比如 n == 0 的时候之类的情况。
    3. 第三、明确递归函数返回后,该做点啥

      1. 里层的递归函数返回后,需要与当前的层做一些互动,然后才能将彼此联系起来。

    思考技巧

    就是将问题拆分成两个部分, 即 1剩余整体,其中 剩余整体 又可以用 1剩余整体 的思想来考虑.如此思考,那么 剩余整体 完全可以用递归的方法去解决.

    重中之重的点如下

    联系到第一点技巧的提示,即相信剩余整体已经处理完毕之后如何与 1 的部分衔接问题.

    算法选择

    • 递归遇到重复计算的情况 → 使用 memorization
    • 递归遇到子问题结果组合的情况 → 使用 divide-and-conquer
    • 递归要求返?所有满足条件的解答 → 使用 backtracking


    1. 获取二叉树所有双分支结点个数

      题目

      假设二叉树采用二叉链表存储结构存储,试设计一个算法,计算一棵给定二叉树的所有双分支结点个数。

      image

      想法

      1. 按照层次遍历算法2逻辑,修改判断结点入队时逻辑即可。
      2. 前/中/后序遍历皆可

      代码实现

      非递归形式

      /**
       * 统计二叉树的所有双分支结点个数。
       *
       * @param root 根节点
       * @return 二叉树的所有双分支结点个数
       */
      public int numberOfFullTreeNode(TreeNode root) {
          int count = 0;
          Queue<TreeNode> queue = new ArrayDeque<>();
          queue.add(root);
          while (!queue.isEmpty()) {
              int size = queue.size();
              while (size-- > 0) {
                  TreeNode poll = queue.poll();
                  assert poll != null;
                  if (poll.left != null) {
                      queue.offer(poll.left);
                  }
                  if (poll.right != null) {
                      queue.offer(poll.right);
                  }
                  if (poll.left != null && poll.right != null) {
                      count++;
                  }
              }
          }
          return -1;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28

      递归形式

      需要重点去理解

      public int numberOfFullTreeNode(TreeNode root){
          if(root!=null){
              if(root.left!=null&&root.right!=null){
                  //双分支结点
                  return numberOfFullTreeNode(root.left)+numberOfFullTreeNode(root.right)+1;
              }else{
                  return numberOfFullTreeNode(root.left)+numberOfFullTreeNode(root.right);
              }
          }
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      ↩︎

    2. 层次遍历算法

      层次遍历算法

      题目

      原题:102. 二叉树的层序遍历 - 力扣(LeetCode)

      给你二叉树的根节点root,返回其节点值的层序遍历。(即逐层地,从左到右访问所有节点)。

      逻辑思路

      • 建立一个queue
      • 先把根节点放进去,这时候找根节点的左右两个子节点
      • 去掉根节点,此时queue里的元素就是下一层的所有节点
      • 用for循环遍历,将结果存到一个一维向量里
      • 遍历完之后再把这个一维向量存到二维向量里
      • 以此类推,可以完成层序遍历

      代码

      这个题目其实就是考察BFS求最短路径问题,🤣写这篇文章的时候还不会这招。见谅啦

      下列代码可优化

      /**
       * Definition for a binary tree node.
       * public class TreeNode {
       *     int val;
       *     TreeNode left;
       *     TreeNode right;
       *     TreeNode() {}
       *     TreeNode(int val) { this.val = val; }
       *     TreeNode(int val, TreeNode left, TreeNode right) {
       *         this.val = val;
       *         this.left = left;
       *         this.right = right;
       *     }
       * }
       */
      class Solution {
          public List> levelOrder(TreeNode root) {
              List> lists = new ArrayList<>();
              if (root == null) {
                  return lists;
              }
              Queue treeNodeQueue = new ArrayDeque<>();
              treeNodeQueue.add(root);
              List sameStorey = new ArrayList<>();
              while (!treeNodeQueue.isEmpty()) {
                  //每当队列中弹出一个元素时,就需要将弹出元素子元素(左右)进行入队操作
                  List temp = new ArrayList<>();
                  while (!treeNodeQueue.isEmpty()) {
                      TreeNode poll = treeNodeQueue.poll();
                      temp.add(poll.val);
                      if (poll.left != null) {
                          sameStorey.add(poll.left);
                      }
                      if (poll.right != null) {
                          sameStorey.add(poll.right);
                      }
                  }
                  //将子节点加入到队列中,并清空集合
                  treeNodeQueue.addAll(sameStorey);
                  sameStorey.clear();
                  lists.add(temp);
              }
              return lists;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      ↩︎
  • 相关阅读:
    【NodeJs-5天学习】第三天实战篇③ ——基于MQTT的环境温度检测
    【web-5】HTTP/HTTPS
    SpringBoot2基础篇(三)—— 整合第三方技术
    Scala下载和配置
    【TcaplusDB知识库】TcaplusDB技术支持介绍
    应用安装
    Kubernetes(K8s):容器编排的未来是什么?
    下载安装包,platform的含义
    微信小程序的资源引用方式
    win11无法打开文件资源管理器
  • 原文地址:https://blog.csdn.net/qq_45074341/article/details/126902505