图解数据结构与算法-搜索与回溯

2023-11-05

前言

本博客是leetcode上图解算法数据结构 - LeetBook - 力扣(LeetCode)全球极客挚爱的技术成长平台的刷题记录,可能存在错误,仅供参考。

主要记录刷题过程的思路,错误,代码以及总结,更详细的解答可以直接看上面这本书。

如发现错误,欢迎在评论区指正,我会及时修改。

同时也希望大家能在评论区多和我讨论,或者私信我,讨论可以让我们学习效率更高。

现在的版本不是最终版本,我会在学习过程中不断地更新。

这是我的gitee账号

shumoke/Leetcode-brushing-record - 码云 - 开源中国 (gitee.com)

里面有更加齐全的代码

搜索和回溯

剑指 Offer 27. 二叉树的镜像

题目

image-20220921213240533

方法1:回溯

思路

遍历每个根节点,然后交换其左右子数,然后再访问其左右子数继续交换。这是从上到下进行交换。

代码

 public TreeNode mirrorTree(TreeNode root) {

        reverse(root);
        return root;
    }
    void reverse(TreeNode root){
        if(root==null) return ;
            TreeNode tmp;
            tmp=root.left;
            root.left=root.right;
            root.right=tmp;
            //遍历根节点的左右节点
            reverse(root.left);
            reverse(root.right);
        
       
    }

注意

  1. 要注意函数的返回值,此函数的返回值是TreeNode类型,递归函数中只有在上层的解需要下层的解时,递归终止条件中才会返回值,不然只需要返回执行权就可。

  2.  如果下面的代码被放在if(root.left!=null&&root.right!=null)中,会导致叶子节点和非叶子节点不进行交换,对于一些特例,无法形成镜像。
     TreeNode tmp;
     tmp=root.left;
     root.left=root.right;
     root.right=tmp;
    

方法2:递归

时间复杂度:O(n)

空间复杂度:O(n)

//递归法
    public TreeNode mirrorTree(TreeNode root) {

        //递归终止
        if(root==null) return null;
        TreeNode node=root.left;
        root.left=mirrorTree(root.right);
        root.right=mirrorTree(node);
        return root;
    }

剑指 Offer 28. 对称的二叉树

思路
对称二叉树定义: 对于树中 任意两个对称节点 L 和 R ,一定有:

L.val = R.val :即此两对称节点值相等。
L.left.val = R.right.val :即 LL 的 左子节点 和 RR 的 右子节点 对称;
L.right.val = R.left.val :即 LL 的 右子节点 和 RR 的 左子节点 对称。

根据以上规律,考虑从顶至底递归,判断每对左右节点是否对称,从而判断树是否为对称二叉树。

代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIMbL6f8-1666100552960)(算法刷题笔记.assets/image-20220923214911263.png)]

总结

将判断一个树是否是对称二叉树,分解成是否每一对对称的左右节点的对称情况。

疑问

什么时候用,什么时候用从底到顶的递归?

剑指 Offer 12. 矩阵中的路径

题目

image-20220925212614060

思路

深度优先搜索:遍历矩阵中每个元素,在当前元素通过DFS递归,先往一个方向一直搜索,如果遇到不匹配选项,就返回到该元素,然后换一个方向去搜索。

剪枝:如果搜索一个方向,发现该方向有不匹配项,则立即返回,称为可行性剪枝。

DFS解析:

  • 递归参数:

    1. 矩阵,字符串,矩阵的坐标(i,j),字符串word元素索引k
  • 返回值:

    1.返回res,代表是否搜索到了目标字符串

  • 终止条件:

    1. 返回false:遍历四个方向,下上右左,如果遇到矩阵索引越界,矩阵对应元素和字符串中元素值不一致,或者重复访问矩阵元素
    2. 返回true:如果字符串word元素索引k=length-1,则成功匹配了所有的字符,递归结束
  • 递推:

    1. 标记当前矩阵元素:将 board[i][j] 修改为 空字符 '' ,代表此元素已访问过,防止之后搜索时重复访问。
    2. 搜索下一元素:朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res
    3. 还原当前矩阵元素:将 board[i][j] 元素还原至初始值,即 word[k] ,这个得在递归结束后,才会一个个还原。

时间复杂度:矩阵中所有元素都可以作为起点,共有NM个起点,所以时间复杂度为O(NM),对于每个起点,长度为K的字符串,搜索方案数为3的K次方(这个对应了回溯中,每一步面对的不同选项数),所有总的时间复杂度为***O*(3*K*M*N*)

空间复杂度:搜索过程中的递归深度不超过 K,因此系统因函数调用累计使用的栈空间占用 O(K) (因为函数返回后,系统调用的栈空间会释放)。最坏情况下 字符串K 的长度为=MN ,递归深度为 MN ,此时系统栈使用 **O(MN)**的额外空间。

代码

package com.回溯;

class Solution4 {
    public boolean exist(char[][] board, String word) {
        //将字符串转变为字符数组
        char[] words = word.toCharArray();
//        遍历字符串
        for(int i=0;i<board.length;i++){
            for (int j = 0; j < board[0].length; j++) {
                if(dfs(board,words,i,j,0)){
                    return true;
                }
            }
        }
        return false;
    }
    boolean dfs(char[][] board,char[] words,int i,int j,int k){
        //终止条件
        if(i<0||i>=board.length||j<0||j>board[0].length||board[i][j]!=words[k]) return false;
        if(k==words.length-1) return true;
        //递推
        board[i][j]='\0';//将当前元素设置为空字符串(Python: '' , Java/C++: '\0'),防止重复访问
        //递归当前元素的四个方向,下上右左,匹配下一个元素
        boolean res=dfs(board,words,i+1,j,k+1)||dfs(board,words,i-1,j,k+1)||
                dfs(board,words,i,j+1,k+1)||dfs(board,words,i,j-1,k+1);//有一个ture则整体为true
        //将矩阵元素还原
        board[i][j]=words[k];//因为上面有判断board[i][j]!=words[k],它不成立,则board[i][j]=words[k],所有这需要这样,就可以将矩阵还原。
        return res;
    }
}
public class 剑指Offer12矩阵中的路径 {
    public static void main(String[] args) {

    }
}

总结

  • 在终止条件中就会进行判断,或者保存结果

  • 空字符(Python: ‘’ , Java/C++: ‘\0’ )

  • 递归的时间复杂度为O(2^N)指数阶

剑指 Offer 13. 机器人的运动范围

总结

  • 在递归中使用全局变量时,要注意,递归的下一层的全局变量的值如果没有传回,目前层的其他递归入口的全局变量的值还是没有变的。
    • 所以在递归中要慎用全局变量,使用时,一定要注意把值返回,不然值无法传达到上层。
    • 疑问:这其中的具体底层原理是啥。

剑指 Offer 34. 二叉树中和为某一值的路径

代码

package com.回溯;

import java.util.LinkedList;
import java.util.List;

class Solution6 {
    LinkedList<List<Integer>> res=new LinkedList<>();// 存放所有满足条件的路径
    LinkedList<Integer> subset=new LinkedList<>();//存放正在寻找的路径
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        dfs(root,target,subset,res);
        return res;
    }
    void dfs(TreeNode root,int target,LinkedList<Integer> subset,LinkedList<List<Integer>> res){
        //终止条件
        if(root==null){
            return;
        }
        subset.add(root.val);
        // int sum=0;
        // for (int e:subset) {
        //     sum+=e;
        // }
        //改进上面发for循环
        target-=root.val;
        if(target==0&&root.left==null&&root.right==null){
            res.add(new LinkedList<>(subset));//找到一条满足条件的路径,将其添加到结果列表中,一定要new一个,不然后对subset进行操作,结果里面的值也会变。
        }
        //递推开始
        dfs(root.left,target,subset,res);
        dfs(root.right,target,subset,res);
        subset.removeLast();
    }
}
public class 剑指Offer34二叉树中和为某一值的路径 {
    public static void main(String[] args) {

    }
}

错误

  1. 审题错误,题目中是从根节点到叶子节点,我理解成从根节点到中间节点也行。
  2. 我访问完节点后,如果是根节点或者刚好满足条件就想从路径列表中删除该节点,这个会导致一个问题——叶子节点一直被存放在路径列表中,从来一直找不到正确的路径。

bug

1.java.lang.IndexOutOfBoundsException: Index: 7, Size: 0
at line 559, java.base/java.util.LinkedList.checkElementIndex
at line 529, java.base/java.util.LinkedList.remove
at line 46, Solution.dfs
at line 44, Solution.dfs
at line 44, Solution.dfs
at line 44, Solution.dfs
at line 21, Solution.pathSum
at line 54, DriverSolution.helper
at line 87, Driver.main

原因:应该分remove函数有关,在递归到下一层时,subset.remove删除了一些元素,但是在上层中这个元素还在,出现了索引异常,所以在递归中使用全局变量时一定要注意。可以用removeLast()来代替remove(object o),但要注意removeLast()是LinkedList特有的办法,remove(object o)是所有Collection集合都有的办法。

总结

  • 在先序遍历时记录的路径,在访问完当前节点的左右子数后,如果为空,就要进行回溯了,才回溯前需要回复路径,即将当前节点的值从路径列表中删除掉。
  • 求和时会用到for循环,可以转变一下思路,看看能不能做减法,避免for循环的时间开销。

剑指 Offer 36. 二叉搜索树与双向链表

题目

image-20221007194636461

思路

利用二叉树的中序遍历的元素顺序是递归的这一性质进行解题

中序遍历访问的第一个节点就是双向链表的第一个节点

时间复杂度:O(N)

空间复杂度:O(N)

代码

package com.回溯;

// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val,Node _left,Node _right) {
        val = _val;
        left = _left;
        right = _right;
    }
}
class Solution7 {
    //定义前结点和当前结点
    Node pre,head;
    public Node treeToDoublyList(Node root) {
        if(root==null) return null;
        //进行中序遍历
        dfs(root);
        //将链表头尾相连,形成循环链表
        head.left=pre;
        pre.right=head;
        return head;
    }
    void dfs(Node cur){
        if(cur==null) return;
        dfs(cur.left);
        //没有前驱结点则为第一个结点
        if(pre!=null) pre.right=cur;
        else head=cur;
        cur.left=pre;
        pre=cur;
        dfs(cur.right);
    }
}
public class 剑指Offer36二叉搜索树与双向链表 {
    public static void main(String[] args) {

    }
}

总结

  • 不用固定思维在先序遍历上,可以利用中序遍历的元素递增性质进行解题

  • 二叉树的深度遍历时间复杂度为O(N),在回溯中有些递归的复杂度是k的N次方。

疑问

回溯和搜索的区别是什么?

中序遍历属于回溯吗?

剑指 Offer 54. 二叉搜索树的第 k 大节点

1.题目

image-20221010200117804

2.思路

中序深度遍历,中序遍历二叉搜索树得到的序列是递增的

3.代码

package com.回溯;

import java.util.ArrayList;

class Solution8 {
    //法一:将root.val添加到ArrayList中
    ArrayList arr=new ArrayList();
    public int kthLargest(TreeNode root, int k) {
        dfs(root);
        int len=arr.size();
        int res= (int) arr.get(len-k);
        return res;
    }
    void dfs(TreeNode root){
        if(root==null) return;
        dfs(root.left);
        arr.add(root.val);
        dfs(root.right);
    }
    //法二:求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。
    int res,k;
    public int kthLargest2(TreeNode root, int k) {
        this.k=k;
        dfs2(root,this.k);
        return res;
    }
    void dfs2(TreeNode root,int k){
        if(root==null || k==0) return;
        dfs(root.right);
        if(--k==0) res=root.val;
        dfs(root.left);
    }
}

public class 剑指Offer54二叉搜索树的第k大节点 {
    public static void main(String[] args) {

    }
}

4.总结

  • 求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”,中序遍历倒序就是先遍历右子数(这个不容易想到)

  • 刚开始审题错误,以为中序遍历后不是自然数,所以需要注意审题

  • 要有转化的思想,当直接解决不好解决时,看看能不能换个角度去解决

  • –k和k–的区别

    • –k是先自减,再运算
    • k–是先运算,再自减

剑指 Offer 55 - I. 二叉树的深度

1.题目

image-20221018213133663

2.方法

法一:回溯法

  1. 思路:先dfs遍历所有的路径,然后把每一条路径都存到LinkedList中,然后比较路径的最大值,即为结果。

  2. 时间复杂度:O(n)

  3. 空间复杂度:O(n*2) #疑问

  4. 代码:

    package com.回溯;
    
    import java.util.LinkedList;
    import java.util.List;
    
    class Solution9 {
        LinkedList<List<Integer>> res=new LinkedList<>();
        LinkedList<Integer> path=new LinkedList<>();
        public int maxDepth(TreeNode root) {
            if(root==null) return 0;
            dfs(root);
            int maxdepth=0;
            for (List p:res) {
                if(p.size()>maxdepth) maxdepth=p.size();
            }
            return maxdepth;
        }
        void dfs(TreeNode root){
            //终止条件
            if(root==null) {
                res.add(new LinkedList<>(path));//将此条路径存入结果中,注意:要这样添加new LinkedList<>(path),然后path还原后res中的值也会变
                return;
            }
            //递推
            path.add(root.val);
            dfs(root.left);
            dfs(root.right);
            path.removeLast();//回溯前将路径还原,用于存放新的路径
        }
    }
    public class 剑指Offer55I二叉树的深度 {
        public static void main(String[] args) {
    
        }
    }
    

​ 5.总结:这种方法的效率很低,时间和空间都低

法二:后序遍历

  1. 思路:采用后序遍历(至下往上递归),该树的深度等于其左子树深度和右子数深度的最大值+1

  2. 代码

    //法二:后序遍历(至下往顶递归),该树的深度等于其左子树深度和右子数深度的最大值+1
    public int maxDepth2(TreeNode root) {
         	if(root==null) return 0;
            // int leftDepth=maxDepth(root.left);
            // int rightDepth=maxDepth(root.right);
            // return Math.max(leftDepth,rightDepth)+1;
            //上面3句合并后为
            return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
    }
    
  3. 时间复杂度:O(n)

  4. 空间复杂度:O(n),树退化成链表时

  5. 提交结果

    image-20221014215625147

  6. 总结:

    • 不要忘记至下往顶递归,它需要等下面的层返回值后,才能返回最终结果

法三:层序遍历

  1. 思路:层次遍历,累计层数

    1. 定义一个队列存放每一层元素
    2. 定义一个列表,存放当前队列中每个元素的左右结点
    3. 然后下层的列表赋给队列
    4. 将层数+1
  2. 代码:

    //法三:层次遍历,累计层数
    public int maxDepth3(TreeNode root) {
        if(root==null) return 0;
            //定义一个队列
    //        List<TreeNode> queue = new LinkedList<TreeNode>(){{add(root);}},tmp;//这句改写成下面两句
            List<TreeNode> queue = new LinkedList<TreeNode>(),tmp;
            queue.add(root);
            int res=0;
            while(!queue.isEmpty()){
                tmp=new LinkedList<TreeNode>();
                for (TreeNode e:queue
                     ) {
                    if(e.left!=null) tmp.add(e.left);
                    if(e.right!=null) tmp.add(e.right);
                }
                queue=tmp;
                res++;
            }
            return res;
    }
    
  3. 时间复杂度:O(n)

  4. 空间复杂度:O(n),最差情况下(当树平衡时),队列 queue 同时存储 N*/2 个节点

  5. 提交结果:

    image-20221014222942017

  6. 总结:

    • List queue = new LinkedList(){{add(root);}},tmp;双大括号初始化,利用了匿名内部类。

剑指 Offer 55 - II. 平衡二叉树

1.题目

image-20221016210611428

2.方法

1)法一:从底向顶递归

  • 思路:
  1. 递归判断每一个节点是否满足平衡二叉树的条件(只有当前节点和其左右节点都满足平衡二叉树的条件,整条树才是平衡二叉树
    1. 先求出每个节点的左右子数的深度,然后判断相差是否超过了1
  • 复杂度分析:#疑问

image-20221018192957312

2)法二:后序遍历+剪枝

  • 思路:

    • 后序遍历二叉树,计算根节点的左右子树的深度,如果左子树-右子树深度的绝对值<=1,就平衡,如果>1则不平衡。
    • 根节点在左右子树中可能存在不平衡,根节点自身也有可能不平衡
    • 剪枝的思想:就是如果已经有子树不平衡了,则这棵树就是不平衡的
  • 代码:

//法二:后序+剪枝判断(即在后序遍历中直接判断)
public boolean isBalanced(TreeNode root) {
    return recur(root)!=-1;
}
int recur(TreeNode root){
    //终止条件
    if(root==null) return 0;
    //递推
    int leftDepth=recur(root.left);
    if(leftDepth==-1) return -1;//如果左子数已经是不平衡的,则直接返回不平衡
    int rightDepth=recur(root.right);
    if(rightDepth==-1) return -1;//如果右子数已经是不平衡的,则直接返回不平衡
    //返回值
    //如果左子数-右子数的绝对值小于等于1,则返回该节点的深度,否则返回-1,代表不平衡。
    return Math.abs(leftDepth-rightDepth)<=1?Math.max(leftDepth,rightDepth)+1:-1;
}
  • 复杂度分析:

    • 时间复杂度:O(n)

    • 空间复杂度:O(n),二叉树退化成链表时

总结

1.递归的终止条件分类

  • return; 只返回执行权
  • return true/false ; 返回执行权和布尔值
  • return 值或者对象;返回执行权和值或者对象
  • 注意一些特例的情况可以和终止条件合并

2.用递归做判断时,要考虑多种可能情况,可能会用到|| 和&&逻辑运算法

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

图解数据结构与算法-搜索与回溯 的相关文章

随机推荐

  • 三种排序方法:冒泡排序,选择排序,sort()函数排序

    三种排序方法 冒泡排序 选择排序 sort函数排序 引言 为什么要写呢 因为我怕我忘了 一个寒假过去冒泡排序都不会手写了 其中的冒泡排序和选择排序为C语言实现 而sort函数排序则借助C 实现 并且还可以用sort函数对结构体进行排序 冒泡
  • 一文详细介绍什么是数据标注?

    机器学习和深度学习算法都依赖于数据 为构建可靠的人工智能模型 需要为算法提供结构良好且标注良好的数据 为了让机器学习算法学习如何完成特定任务 我们必须标注它们用于训练的数据 换句话说 标注数据很简单 但并不总是那么容易 幸运的是 我们将通过
  • 第一届世界区块链大会·三点钟峰会(W.B.C)在澳门开幕 万人峰会大咖云集明星嘉年华

    2018年4月23日晚 以 技术重构世界 为主题的第一届世界区块链大会 三点钟峰会 W B C 在中国澳门威尼斯人大酒店隆重开幕 为期三天 大会由世界区块链联合协会首倡 世界区块链大会组委会 三点钟 深创学院主办 深圳大学区块链研究院 银河
  • 解决org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.spring

    从svn上取下来的之前一直在开发的项目 因为项目我是中途接手的 所以同步下来 配置好tomcat 运行的时候报错 Cannot find class com lhzxt dhub MyExceptionHandler for bean wi
  • JavaScript基础(Dom操作)

    目录 一 BOM模型 1 1 BOM可实现功能 二 Window对象的常用属性 2 1 Window对象的常用方法 2 1 1 open 和close 方法 三 History对象 四 Location对象 五 Document对象的常用方
  • tab页过多,展现按钮可左右移动tab页

    1 结果如下图 tab太多 一行放不下 右侧展现左右按钮 鼠标移上tab页可左右移动 2 代码 功能按钮过多时 可左右移动 param id 按钮区域id param dom 按钮元素 param btnclass 按钮样式 param m
  • this指针详解

    什么是this指针 this是指向实例化对象本身时候的一个指针 里面存储的是对象本身的地址 通过该地址可以访问内部的成员函数和成员变量 一个对象的this指针并不是对象本身的一部分 其不会影响sizeof 对象 的结果 this指针的用处
  • 设计一个windows应用程序,定义一个Student类,包含学号和姓名两个字段,并定义一个班级类ClassList

    设计一个windows应用程序 定义一个Student类 包含学号和姓名两个字段 并定义一个班级类ClassList 该类包含一个Student集合 使用索引器访问该集合 1 创建一个Windows应用程序Myproject6 1 2 设计
  • vue自定义$confirm弹窗确认组件

    前言 1 Vue extend 使用基础 Vue 构造器 创建一个 子类 参数是一个包含组件选项的对象 vue单文件经过webpack打包之后是一个组件示例对象 因此可以传到Vue extend中生成一个包含此组件的类 2 new VueC
  • 关闭window10状态栏热点资讯

    前言 最近window10更新了 在桌面右下角的工具栏出现气温的小图标 占用了工具栏位置 挺不爽的 想关闭它 解决 1 在气温图标上 点击鼠标右键 然后选择资讯和兴趣 在弹出的下级菜单中选择 关闭
  • VC++6.0 没用atlstr.h 怎么办

    在VC 6 0中配置WTL9 0 提示没有atlstr h 这个文件 怎么办呢 于是把atlmisc h这个文件 复制一份 把名称改成atlstr h 不就OK了 又可使用CString 这个恶心的东西了 编绎一下试试 提示 error C
  • CSS选择器器练习之餐厅小游戏答案和解析(下)

    17 small last child 伪类选择器 last child选择最后一个子元素 18 plate nth child 3 伪类选择器 nth child 选择第n个子元素 19 bento nth last child 3 伪类
  • linux命令行将已有项目提交到github

    用git是在windows下用git的图形化界面进行操作的 这次有一个写了几天的小项目想提交到git上 linux命令行下面没有图形化的界面 所以全部需要git命令来操作 实践之后 主要是下面几个步骤 1 登陆github 创建一个repo
  • Layui之选项卡案例 详细易懂

    本期精彩 利用Layui框架实现动态选项卡 继上一篇已经实现了左边的树形菜单栏 这一关卡我们已通过 接下来就是实现右边的动态选项卡的关卡 上个关卡的效果及链接 链接 http t csdn cn tYccL 目录 本期精彩 利用Layui框
  • Android JNI开发从0到1,java调C,C调Java,保姆级教程详解

    前些天发现了一个蛮有意思的人工智能学习网站 8个字形容一下 通俗易懂 风趣幽默 感觉非常有意思 忍不住分享一下给大家 点击跳转到教程 第一步首先配置Android studio的NDK开发环境 首先在Android studio中下载NDK
  • 3.5.1 ASM规划及实现

    最后更新2021 08 14 AMS规划 规划涉及到几个参数 它们之间互相影响 如果需要修改其中一个 注意是否需要同时修改其它几个 下面是几个重要参数及其概念 Memory Pool size共享内存池的大小 使用同一共享内存池的分区数量
  • 贷后联动管控指标与差异化案件的分配逻辑

    在风控精细化运营的当下 贷后工作的开展 越来越需要精细化管理 如何做好相关的精细化管理工作 首先我们从这些贷后相关的名词如下开始熟悉 贷后基本催收名词解释 Flow Rate 迁移率就是在贷后资产评估里最重要的报表了 比如计算M0到M1的迁
  • shell脚本获取当前ip地址

    需求 shell脚本里我需要根据不同的ip地址做出不同的操作 因此我需要在shell脚本里获取当前主机的ip地址 我需要获取到192 168 1 111这个ip地址 方法1 ifconfig grep inet 地址 grep 192 16
  • (十五)视频处理、不用事先训练

    十五 视频处理 不用事先训练 本文的代码的功能是 可以对人物视频进行操作 不用预先耗时训练模型 效率极高 可进行视频处理 使用了人工智能的算法 注 请移步最新博文 十八 一 主要功能 以下的Python代码的功能 选择视频 主要包括 1 对
  • 图解数据结构与算法-搜索与回溯

    前言 本博客是leetcode上图解算法数据结构 LeetBook 力扣 LeetCode 全球极客挚爱的技术成长平台的刷题记录 可能存在错误 仅供参考 主要记录刷题过程的思路 错误 代码以及总结 更详细的解答可以直接看上面这本书 如发现错