二叉树中的深搜

目录

二叉树中的深搜:

一、计算布尔二叉树的值

1.题目链接:2331. 计算布尔二叉树的值

2.题目描述:

3.解法(递归)

🍒算法思路:

🍒算法流程:

🍒算法代码:

二、求根节点到叶节点数字之和

1.题目链接:129. 求根节点到叶节点数字之和

2.题目描述:

3.解法(dfs - 前序遍历)

🍒算法思路:

🍒算法流程:

🍒算法代码:

三、二叉树剪枝

1.题目链接:814. 二叉树剪枝

2.题目描述:

3.解法(dfs - 后序遍历)

🍒算法思路:

🍒算法流程:

🍒算法代码:

四、验证二叉搜索树

1.题目链接:98. 验证二叉搜索树

2.题目描述:

3.解法(利用中序遍历)

🍒算法思路:

🍒算法流程:

🍒算法代码:

五、二叉搜索树中第 k 小的元素 

1.题目链接:230. 二叉搜索树中第K小的元素

2.题目描述:

3.解法(中序遍历 + 计数器剪枝)

🍒算法思路:

🍒算法流程:

🍒算法代码:

六、二叉树的所有路径

1.题目链接:257. 二叉树的所有路径

2.题目描述:

3.解法(回溯)

🍒算法思路:

🍒算法代码:


二叉树中的深搜:

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

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

        因为树的定义本⾝就是递归定义,因此采⽤递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。并且前中后序三种遍历的唯⼀区别就是访问根节点的时机不同,在做题的时候,选择⼀个适当的遍历顺序,对于算法的理解是⾮常有帮助的。

一、计算布尔二叉树的值

1.题目链接:2331. 计算布尔二叉树的值

2.题目描述:

给你一棵 完整二叉树 的根,这棵树有以下特征:

  • 叶子节点 要么值为 0 要么值为 1 ,其中 0 表示 False ,1 表示 True 。
  • 非叶子节点 要么值为 2 要么值为 3 ,其中 2 表示逻辑或 OR ,3 表示逻辑与 AND 。

计算 一个节点的值方式如下:

  • 如果节点是个叶子节点,那么节点的  为它本身,即 True 或者 False 。
  • 否则,计算 两个孩子的节点值,然后将该节点的运算符对两个孩子值进行 运算 。

返回根节点 root 的布尔运算值。

完整二叉树 是每个节点有 0 个或者 2 个孩子的二叉树。

叶子节点 是没有孩子的节点。

示例 1:

输入:root = [2,1,3,null,null,0,1]
输出:true
解释:上图展示了计算过程。
AND 与运算节点的值为 False AND True = False 。
OR 运算节点的值为 True OR False = True 。
根节点的值为 True ,所以我们返回 true 。

示例 2:

输入:root = [0]
输出:false
解释:根节点是叶子节点,且值为 false,所以我们返回 false 。

提示:

  • 树中节点数目在 [1, 1000] 之间。
  • 0 <= Node.val <= 3
  • 每个节点的孩子数为 0 或 2 。
  • 叶子节点的值为 0 或 1 。
  • 非叶子节点的值为 2 或 3 。

3.解法(递归

🍒算法思路:

本题可以被解释为:

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

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

  • 所有子节点的值;
  • 通过子节点的值运算出当前节点值。

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

🍒算法流程:

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

  1. 返回值:当前节点值;
  2. 参数:当前节点指针。
  3. 函数作用:求得当前节点通过逻辑运算符得出的值。

递归函数流程:

  1. 当前问题规模为 n=1 时,即叶⼦节点,直接返回当前节点值;
  2. 递归求得左右子节点的值;
  3. 通过判断当前节点的逻辑运算符,计算左右子节点值运算得出的结果

🍒算法代码:

class Solution 
{
public:
    bool evaluateTree(TreeNode* root) 
    {
        if(root->left == nullptr) return root->val == 0 ? false : true;

        bool left =  evaluateTree(root->left);
        bool right = evaluateTree(root->right);
        
        return root->val == 2 ? left || right : left && right;
    }

};

二、求根节点到叶节点数字之和

1.题目链接:129. 求根节点到叶节点数字之和

2.题目描述:

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。

每条从根节点到叶节点的路径都代表一个数字:

  • 例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。

计算从根节点到叶节点生成的 所有数字之和 。

叶节点 是指没有子节点的节点。

示例 1:

输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12

从根到叶子节点路径 1->3 代表数字 13

因此,数字总和 = 12 + 13 = 25

示例 2:

输入:root = [4,9,0,5,1]
输出:1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495
从根到叶子节点路径 4->9->1 代表数字 491
从根到叶子节点路径 4->0 代表数字 40
因此,数字总和 = 495 + 491 + 40 = 1026

提示:

  • 树中节点的数目在范围 [1, 1000] 内
  • 0 <= Node.val <= 9
  • 树的深度不超过 10

3.解法(dfs - 前序遍历

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

🍒算法思路:

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

  1. 将父节点的数字与当前节点的信息整合到⼀起,计算出当前节点的数字,然后传递到下一层进行递归;
  2. 当遇到叶子节点的时候,就不再向下传递信息,而是将整合的结果向上一直回溯到根节点。在递归结束时,根节点需要返回的值也就被更新为了整棵树的数字和。

🍒算法流程:

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

  1. 返回值:当前子树计算的结果(数字和);
  2. 参数 num:递归过程中往下传递的信息(父节点的数字);
  3. 函数作用:整合父节点的信息与当前节点的信息计算当前节点数字,并向下传递,在回溯时返回当前子树(当前节点作为⼦树根节点)数字和。

递归函数流程:

  1. 当遇到空节点的时候,说明这条路从根节点开始没有分支,返回 0;
  2. 结合父节点传下的信息以及当前节点的 val,计算出当前节点数字 sum;
  3. 如果当前结点是叶子节点,直接返回整合后的结果 sum;
  4. 如果当前结点不是叶子节点,将 sum 传到左右子树中去,得到左右子树中节点路径的数字和,然后相加后返回结果。

🍒算法代码:

class Solution 
{
public:
    int sumNumbers(TreeNode* root) 
    {
        return dfs(root, 0);
    }

    int dfs(TreeNode* root, int presum)
    {
        presum = presum * 10 + root->val;
        if(root->left == nullptr && root->right == nullptr)
            return presum;
        int ret = 0;
        if(root->left) ret += dfs(root->left, presum);
        if(root->right) ret += dfs(root->right, presum);

        return ret;
    }
};

三、二叉树剪枝

1.题目链接:814. 二叉树剪枝

2.题目描述:

给你二叉树的根结点 root ,此外树的每个结点的值要么是 0 ,要么是 1 。

返回移除了所有不包含 1 的子树的原二叉树。

节点 node 的子树为 node 本身加上所有 node 的后代。

示例 1:

输入:root = [1,null,0,0,1]
输出:[1,null,0,null,1]
解释:
只有红色节点满足条件“所有不包含 1 的子树”。 右图为返回的答案。

示例 2:

输入:root = [1,0,1,0,0,0,1]
输出:[1,null,1,null,1]

示例 3:

输入:root = [1,1,0,1,1,0,1,0]
输出:[1,1,0,1,1,null,1]

提示:

  • 树中节点的数目在范围 [1, 200] 内
  • Node.val 为 0 或 1

3.解法(dfs - 后序遍历

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

🍒算法思路:

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

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

  • 需要注意的是,在删除叶子节点时,其父节点很可能会成为新的叶子节点。因此,在处理完子节点后,我们仍然需要处理当前节点。这也是为什么选择后序遍历的原因(后序遍历首先遍历到的一定是叶子节点)。
  • 通过使用后序遍历,我们可以逐步删除叶子节点,并且保证删除后的节点仍然满足删除操作的要求。这样,我们可以较为方便地实现删除操作,而不会影响最终的结果。
  • 若在处理结束后所有叶子节点的值均为 1,则所有子树均包含 1,此时可以返回。

🍒算法流程:

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

1. 返回值:无;

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

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

后序遍历的主要流程:

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

2. 递归处理左子树;

3. 递归处理右子树;

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

  • 如果是,就删除掉;
  • 如果不是,就不做任何处理。

🍒算法代码:

class Solution 
{
public:
    TreeNode* pruneTree(TreeNode* root) 
    {
        if(root == nullptr) return nullptr;

        root->left = pruneTree(root->left);
        root->right = pruneTree(root->right);
        if(root->left == nullptr && root->right == nullptr && root->val == 0)
        {
            delete root;// 防止内存泄漏
            root = nullptr;
        }

        return root; 
    }
};

四、验证二叉搜索树

1.题目链接:98. 验证二叉搜索树

2.题目描述:

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入:root = [2,1,3]
输出:true

示例 2:

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

提示:

  • 树中节点数目范围在[1, 104] 内
  • -231 <= Node.val <= 231 - 1

3.解法(利用中序遍历

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

🍒算法思路:

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

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

🍒算法流程:

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

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

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

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

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

  • 如果当前结点的 val 大于 prev,说明满足条件,retcur 改为 true;
  • 如果当前结点的 val 小于等于 prev,说明不满足条件,retcur 改为 false;

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

3. 只有当 retleft、 retcur 和 retright 都是 true 的时候,才返回 true。

🍒算法代码:

class Solution 
{
    long prev = LONG_MIN;
public:
    bool isValidBST(TreeNode* root) 
    {
        if(root == nullptr) return true;
        bool left = isValidBST(root->left);
        // 剪枝
        if(left == false) return false;
        bool cur = false;
        if(root->val > prev)
            cur = true;
        // 剪枝(如果当前节点为false,那后面的节点都不用去看了)
        if(cur == false) return false;
        prev = root->val;
        bool right = isValidBST(root->right);

        return left && cur && right;
    }
};

五、二叉搜索树中第 k 小的元素 

1.题目链接:230. 二叉搜索树中第K小的元素

2.题目描述:

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

示例 1:

输入:root = [3,1,4,null,2], k = 1
输出:1

示例 2:

输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3

提示:

  • 树中的节点数为 n 。
  • 1 <= k <= n <= 104
  • 0 <= Node.val <= 104

3.解法(中序遍历 + 计数器剪枝

🍒算法思路:

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

🍒算法流程:

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

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

  • 返回值为第 k 个结点;

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

1. 递归出口:空节点直接返回 -1,说明没有找到;

2. 去左子树上查找结果,记为 retleft:

  • 如果 retleft == -1,说明没找到,继续执行下面逻辑;
  • 如果 retleft != -1,说明找到了,直接返回结果,无需执行下面代码(剪枝);

3. 如果左子树没找到,判断当前结点是否符合:

  • 如果符合,直接返回结果

4. 如果当前结点不符合,去右子树上寻找结果

🍒算法代码:

class Solution 
{
    int count;
    int ret;;
public:
    int kthSmallest(TreeNode* root, int k) 
    {
        count = k;
        dfs(root);
        return ret;
    }
    void dfs(TreeNode* root)
    {
        if(root == nullptr || count == 0) return;
        dfs(root->left);
        count--;
        if(count == 0) ret = root->val;
        dfs(root->right);
    }
};

六、二叉树的所有路径

1.题目链接:257. 二叉树的所有路径

2.题目描述:

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

 

示例 1:

输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

示例 2:

输入:root = [1]
输出:["1"]

提示:

  • 树中节点的数目在范围 [1, 100] 内
  • -100 <= Node.val <= 100

3.解法(回溯

🍒算法思路:

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

        路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加入到路径中,如果该节点为叶子节点,将路径存储到结果中。否则,将 "->" 加入到路径中并递归遍历该节点的左右子树。 定义一个结果数组,进行递归。递归具体实现方法如下:

  1. 如果当前节点不为空,就将当前节点的值加入路径 path 中,否则直接返回;
  2. 判断当前节点是否为叶子节点,如果是,则将当前路径加入到所有路径的存储数组 paths 中;
  3. 否则,将当前节点值加上 "->" 作为路径的分隔符,继续递归遍历当前节点的左右子节点。
  4. 返回结果数组。

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

        具体实现方法如下:

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

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

  1. 如果当前节点为空,返回。
  2. 将当前节点的值加入到路径数组中。
  3. 如果当前节点为叶子节点,将路径数组中的所有元素拼接成字符串,并将该字符串存储到结果数组中。
  4. 递归遍历当前节点的左子树。
  5. 递归遍历当前节点的右子树。
  6. 回溯,将路径数组中的最后⼀个元素移除,以返回到上一个节点。

3. 返回结果数组。

🍒算法代码:

class Solution 
{
public:
    vector<string> ret;// 记录结果
    vector<string> binaryTreePaths(TreeNode* root) 
    {
        string path;
        dfs(root, path);
        return ret;
    }
    void dfs(TreeNode* root, string path)
    {
        path += to_string(root->val);
        if(root->left == nullptr && root->right == nullptr)
        {
            ret.push_back(path);
            return;
        }
        path += "->";
        if(root->left) dfs(root->left, path);
        if(root->right) dfs(root->right, path);
    }
};
  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南风与鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值