递归、搜索与回溯——二叉树中的深搜

💁‍♂️个人主页:进击的荆棘

👇作者其它专栏:

《数据结构与算法》《算法》《C++起始之路》


目录

1.深搜的概念

2.相关题解


1.深搜的概念

深度优先遍历(DFS, 全称为Depth First Traversal ),是我们树或图这样的数据结构中常用的一种遍历算法。这个算法会尽可能深的搜索树或图的分支,直到一条路径上的所有路径节点都被遍历完毕,然后再回溯到上一层,继续找一条路遍历。

在二叉树中,常见的深度优先遍历为:前序遍历、中序遍历 以及后续遍历

因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。并且前中后三种遍历的唯一区别就是访问根节点的时机不同

2.相关题解

2.1计算布尔二叉树的值

算法思路:

本题可以被解释为:

1.对于规模为n的问题,需要求得当前节点值。

2.节点值不为0或1是,规模为n的问题可以被拆分为规模为n-1的子问题:

a.所有子节点的值;

b.通过子节点的值运算出当前节点值。

3.当问题的规模变为n=1时,即叶子节点的值为0或1,可以直接获取当前节点值

算法流程:

递归函数设计 bool evaluateTree(TreeNode* root)

1.返回值:当前节点值;

2.参数:当前节点指针;

3.函数作用:求得当前节点通过逻辑运算得到的值。

递归函数流程:

1.当前问题规模为n=1时,即叶子节点,直接返回当前节点值;

2.递归求得左右子结点的值;

3.通过判断当前节点的逻辑运算符,计算左右子结点值运算得出的结果。

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool evaluateTree(TreeNode* root) {
        if(!root->left) return root->val==1?true:false;

        //左遍历
        bool l=evaluateTree(root->left);
        //右遍历
        bool r=evaluateTree(root->right);
        //返回节点值
        return root->val==2?l|r:l&r;
    }
};

2.2求根节点到叶节点数字之和

解法(dfs-前序遍历):

前序遍历按照根节点、左子树、右子树的顺序遍历二叉树的所有结点,通常用于子结点的状态依赖于父节点状态的题目。

算法思路:

前序 遍历的过程中,我们可以往左右子树传递信息,并且在回溯时得到左右子树的返回值。递归函数可以帮我们完成两件事:

1.将父节点的数字于当前节点的信息整合到一起,计算出当前节点的数字,然后传递到下一层进行递归;

2.当遇到叶子节点的时候,就不再往下传递信息,而是将整合的结果向上一直回溯到根节点。

在递归结束时,根节点需要返回的值也就被更新为了整棵树的数字和。

算法流程:

递归函数设计:int dfs(TreeNode* root,int num)

1.返回值:当前子树计算的结果(数字和);

2.参数num:递归过程中往下传递的信息(父节点的数字);

3.函数作用:整合父节点的信息与当前节点 的信息计算当前节点数字,并向下传递,在回溯时返回当前**子树(当前节点作为子树根节点)**数字和。

递归函数流程:

1.当遇到空节点的时候,说明这条路从根节点开始没有分支,返回0

2.结合父节点传下的信息以及当前节点的val ,计算出当前节点数字sum

3.若当前节点是叶子节点,直接返回整合后的结果sum

4.若当前节点不是叶子节点,将sum传到左右子树中去,得到左右子树中节点路径的数字和,然后相加后返回结果。

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int sumNumbers(TreeNode* root) {
        return dfs(root,0);
    }
    int dfs(TreeNode* root,int presum){
        //1.先加上当前节点的值,判断是否为叶子节点
        presum=presum*10+root->val;
        if(!root->left&&!root->right) return presum;
        int ret=0;
        //2.若左子树存在
        if(root->left) ret+=dfs(root->left,presum);
        //3.若右子树存在
        if(root->right) ret+=dfs(root->right,presum);
        //4.返回
        return ret;
    }
};

2.3二叉树剪枝

解法(dfs-后序遍历):

后序遍历按照左子树、右子树、根节点的顺序遍历二叉树的所有结点,通常用于父节点的状态依赖于子结点状态的题目。

算法思路:

若我们选择从上晚霞删除,我们需要收集左右子树的信息,这可能导致代码编写相对困难。然而,通过观察我们可以发现,若我们先删除最底部的叶子节点,然后再处理删除后节点,最终的结果并不会受到影响。

因此,我们可以采用后序遍历的方式来解决这个问题。在后序遍历中,我们先处理左子树,然后处理右子树,最后再处理当前节点。再处理当前节点时,我们可以判断其是否为叶子节点且其值是否为0,若满足条件,我们可以删除当前节点。

●需要注意的是,在删除叶子节点时,其父节点很可能会成为新的叶子节点。因此,在处理完子节点后,我们任然需要处理当前节点。这也是为什么选择后续遍历的原因(后序遍历首先遍历到的一定是叶子节点)。

●通过使用后序遍历,我们可以逐步删除叶子节点,并且保证删除后的节点仍然满足删除操作的要求。这样,我们可以较为方便地实现删除操作,而不会影响最终的结果。

●若在处理结束后所有叶子节点的值均为1,则所有子树均包含1,此时可以返回。

算法流程:

递归函数设计:void dfs(TreeNode* root)

1.返回值:无;

2.参数:当前需要处理的节点;

3.函数作用:判断当前节点是否需要删除,若需要删除,则删除当前节点。

后序遍历的主要流程:

1.递归出口:当传入节点为空时,不做任何处理;

2.递归处理左子树;

3.递归处理右子树;

4.处理当前节点:判断改节点是否为叶子节点(即左右子结点均被删除,当前节点成为叶子节点),并且节点的值为0;

a.若是,就删除掉;

b.若不是,就不做任何处理。

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* pruneTree(TreeNode* root) {
        if(!root) return nullptr;
        //若左子树存在
        if(root->left) root->left=pruneTree(root->left);
        //若右子树存在
        if(root->right) root->right=pruneTree(root->right);
        
        if(!root->left&&!root->right&&root->val==0){
            //delete root;  new出来的节点需删除
            root=nullptr;
        }
        return root;
    }
};

2.4验证二叉搜索树

解法(利用中序遍历):

后序遍历按照左子树、根节点、右子树的顺序遍历二叉树的所有结点,通常用于二叉搜索树相关题目。

算法思路:

若一颗树是二叉搜索树,那么它的中序遍历的结果一定是一个严格递增的序列。

因此,我们可以初始化一个无穷小 的全局变量,用来记录中序遍历过程中的前驱节点。那么就可以在中序遍历的过程中,先判断是否和前驱节点构成递增序列,然后修改前驱节点为当前节点,传入下一层的递归中。

算法思路:

1.初始化一个全局的变量prev ,用来记录中序遍历过程中的前驱节点的val

2.中序遍历的递归函数中:

a.设置递归出口:root==nullptr 的时候,返回true

b.先递归判断左子树是否是二叉搜索树,用left标记;

c.然后判断当前节点是否满足二叉搜索树的性质,用cur标记:

●若当前节点的val 大于prev ,说明满足条件,cur 改为true;

●若当前节点的val 小于等于prev ,说明不满足条件,cur 改为false

d.最后递归判断右子树是否是二叉搜索树,用right标记;

3.只有当left、cur、right 都是true 的时候,才返回true

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
//使用数组的思想,将一棵树中序遍历结果放进数组,则它是一个升序数组,但这样内存消耗太大,所以借助这个思想
class Solution {
    //防止根节点值为INT_MIN
    long prev=LONG_MIN;
public:
    bool isValidBST(TreeNode* root) {
        if(!root) return true;

        bool left=isValidBST(root->left);
        //剪枝,若数据很多,可以大大优化时间
        if(!left) return false;

        bool cur=false;
        if(root->val>prev) 
            cur=true;
        //剪枝
        if(!cur) return false;
        prev=root->val;

        bool right=isValidBST(root->right);
        return left&&cur&&right;
    }
};

2.5二叉搜索树中第 K 小的元素

解法二(中序遍历+计数器剪枝):

算法思路:

上述解法不仅使用大量额外空间存储数据,并且会将所有的节点都遍历一遍。

但是,我们可以根据中序遍历的过程,只需扫描前k个节点即可。

因此,我们可以创建一个全局的计数器cnt ,将其初始化为k ,每遍历一个节点就将cnt-- 。直到某次递归的时候,cnt 的值等于1,说明此时的节点就是我们要找到的结果。

算法流程:

1.定义一个全局的变量cnt,在主函数中初始化为k的值;

递归函数的设计:int dfs(TreeNode* root):

●返回值为第k个结点;

递归函数流程(中序遍历):

1.递归出口:空节点或cnt<0直接返回;

2.去左子树上查找结果,然后cnt--;

3.若找到,此时cnt==0,直接返回当前节点的值;

4.若没有找到,去右子树找。

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    //中序遍历
    int cnt=0,ret=0;
    int kthSmallest(TreeNode* root, int k) {
        cnt=k;
        dfs(root);
        return ret;
    }
    void dfs(TreeNode* root){
        //出口+剪枝
        if(!root||cnt<0) return;
        
        dfs(root->left);
        cnt--;

        if(cnt==0) ret=root->val;

        dfs(root->right);
    }
};

2.6二叉树的所有路径

算法思路(回溯):

使用深度优先遍历(DFS)求解

路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加入到路径中,若该节点为叶子节点,将路径存储到结果中。否则,将"->"加入到路径中并递归遍历该节点的左右子树。

定义一个结果数组,进行递归。递归具体实现方法如下:

1.若当前节点不为空,就将当前节点的值加入路径path中,否则直接返回;

2.判断当前节点是否为叶子节点,若是,则将当前路径加入到所有路径的存储数组path中;

3.否则,将当前节点值加上"->"作为路径的分隔符,继续递归遍历当前节点的左右子节点;

4.返回结果数组。

●特别地,我们可以只使用一个字符串储存每个状态的字符串,在递归回溯的过程中,需要将路径中的当前节点移除,以回到上一个节点。

具体实现方法:

1.定义一个结果数组和一个路径数组。

2.从根节点开始递归,递归函数的参数为当前节点、结果数组和路径数组。

a.若当前节点为空,返回。

b.将当前节点的值加入到路径数组中。

c.若当前节点为叶子节点,将路径数组中的所有元素拼接成字符串,并将该字符串存储到结果数组中。

d.递归遍历当前节点的左子树。

e.递归遍历当前节点的右子树。

f.回溯,将路径数组中的最后一个元素移除,以返回到上一个节点。

3.返回结果数组。

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<string> ret;
    vector<string> binaryTreePaths(TreeNode* root) {
        string path;
        dfs(root,path);
        return ret;
    }
    //若将string设为全局变量,因为到达叶节点后,恢复现场会很麻烦(将添加的节点删除),若将string设为参数,则很简单
    void dfs(TreeNode* root,string path){
        path+=to_string(root->val);
        //判断此节点是否为叶子节点
        if(!root->left&&!root->right) {
            ret.push_back(path);
        }
        path+="->";
        //剪枝+回溯,剪枝后就不用加出口了
        if(root->left) dfs(root->left,path);
        if(root->right) dfs(root->right,path);
    }
};
相关推荐
人道领域2 小时前
【LeetCode刷题日记】:151翻转字符串的单词(两种解法)
java·开发语言·算法·leetcode·面试
会编程的土豆2 小时前
【日常做题】栈 中缀前缀后缀
开发语言·数据结构·算法
进击的荆棘2 小时前
递归、搜索与回溯——回溯
数据结构·c++·算法·leetcode·dfs
励志的小陈2 小时前
数据结构--二叉树(链式结构、C语言实现、层序遍历)
c语言·数据结构
郝学胜-神的一滴2 小时前
[简化版 Games 101] 计算机图形学 05:二维变换下
c++·unity·图形渲染·three.js·opengl·unreal
自我意识的多元宇宙9 小时前
树与二叉树--二叉树的存储结构
数据结构
BestOrNothing_201511 小时前
C++零基础到工程实战(4.3.3):vector数组访问与遍历
c++·迭代器·stl·vector·动态数组
charlie11451419111 小时前
通用GUI编程技术——图形渲染实战(三十三)——Direct2D与Win32/GDI互操作:渐进迁移实战
c++·图形渲染·gui·win32
文祐11 小时前
C++类之虚函数表及其内存布局(一个子类继承一个父类)
开发语言·c++