算法沉淀——二叉树中的深搜(leetcode真题剖析)

在这里插入图片描述


二叉树的深度优先搜索是一种遍历二叉树的方法,它通过深度递归的方式探索树的结构。有两种主要形式:前序遍历、中序遍历、和后序遍历。在二叉树上执行深度优先搜索,意味着首先探索到树的深度,然后回溯到更浅的层次。

以下是三种常见的二叉树深度优先搜索方式的定义:

  1. 前序遍历(Preorder Traversal)

    • 访问根节点
    • 递归地前序遍历左子树
    • 递归地前序遍历右子树
    根 -> 左子树 -> 右子树
    
  2. 中序遍历(Inorder Traversal)

    • 递归地中序遍历左子树
    • 访问根节点
    • 递归地中序遍历右子树
    左子树 -> 根 -> 右子树
    
  3. 后序遍历(Postorder Traversal)

    • 递归地后序遍历左子树
    • 递归地后序遍历右子树
    • 访问根节点
    左子树 -> 右子树 -> 根
    

在实现深度优先搜索时,通常使用递归的方法,递归函数的调用栈会自动帮助我们在深度上进行探索。对于每个节点,首先处理当前节点,然后递归处理其左子树和右子树。

示例代码(C++):

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

void preorderTraversal(TreeNode* root) {
    if (root == nullptr) return;
    // 处理当前节点
    cout << root->val << " ";
    // 递归处理左子树
    preorderTraversal(root->left);
    // 递归处理右子树
    preorderTraversal(root->right);
}

void inorderTraversal(TreeNode* root) {
    if (root == nullptr) return;
    // 递归处理左子树
    inorderTraversal(root->left);
    // 处理当前节点
    cout << root->val << " ";
    // 递归处理右子树
    inorderTraversal(root->right);
}

void postorderTraversal(TreeNode* root) {
    if (root == nullptr) return;
    // 递归处理左子树
    postorderTraversal(root->left);
    // 递归处理右子树
    postorderTraversal(root->right);
    // 处理当前节点
    cout << root->val << " ";
}

这些函数分别实现了二叉树的前序、中序和后序深度优先搜索。在实际应用中,可以根据问题的要求选择不同的遍历方式。

01.计算布尔二叉树的值

题目链接:https://leetcode.cn/problems/evaluate-boolean-binary-tree/

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

  • 叶子节点 要么值为 0 要么值为 1 ,其中 0 表示 False1 表示 True
  • 非叶子节点 要么值为 2 要么值为 3 ,其中 2 表示逻辑或 OR3 表示逻辑与 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
  • 每个节点的孩子数为 02
  • 叶子节点的值为 01
  • 非叶子节点的值为 23

思路

二叉树的大部分题目都可划分为子问题,所以一般我们都使用递归来解决,这里我们要先访问左右节点再访问根节点进行运算,所以这里使用的应该是后序遍历来写递归函数。

代码

class Solution {
public:
    bool evaluateTree(TreeNode* root) {
        if(!root->left) return root->val;

        int left = evaluateTree(root->left);
        int right = evaluateTree(root->right);

        return root->val==2?left|right:left&right;
    }
};

02.求根节点到叶节点数字之和

题目链接:https://leetcode.cn/problems/sum-root-to-leaf-numbers/

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

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

  • 例如,从根节点到叶节点的路径 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

思路

在前序遍历过程中,我们能够通过向左右子树传递信息,并在回溯时获取左右子树的返回值。递归函数的作用主要体现在两个方面:

  1. 整合父节点的数字与当前节点的信息,计算出当前节点的数字,并将其传递到下一层进行递归;
  2. 当遇到叶子节点时,停止向下传递信息,而是将整合的结果一直回溯到根节点。递归结束时,根节点需要返回的值即为整棵树的数字和。

代码

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

    int dfs(TreeNode* root,int pre){
        int presum = pre*10+root->val;

        if(!root->left&&!root->right) return presum;

        int ret=0;
        if(root->left) ret+=dfs(root->left,presum);
        if(root->right) ret+=dfs(root->right,presum);

        return ret;
    }
};
  1. int sumNumbers(TreeNode* root): 这是主要的函数,用于计算二叉树中所有从根节点到叶子节点的路径和。它调用了辅助函数dfs,并将初始的前缀和设为0。
  2. int dfs(TreeNode* root, int pre): 这是递归的辅助函数,用于深度优先搜索计算路径和。它接收当前节点和前一个节点的路径和,然后计算当前节点的路径和。
    • int presum = pre * 10 + root->val;: 计算当前节点的路径和,乘以10表示将前一个节点的值左移一位,然后加上当前节点的值。
    • if (!root->left && !root->right) return presum;: 如果当前节点是叶子节点,直接返回当前节点的路径和,表示找到了一条完整的路径。
    • int ret = 0;: 初始化一个变量ret,用于累加左子树和右子树的路径和。
    • if (root->left) ret += dfs(root->left, presum);: 如果左子树存在,递归计算左子树的路径和,并累加到ret中。
    • if (root->right) ret += dfs(root->right, presum);: 如果右子树存在,递归计算右子树的路径和,并累加到ret中。
    • return ret;: 返回累加的结果,表示以当前节点为起点的所有路径和。

03.二叉树剪枝

题目链接:https://leetcode.cn/problems/binary-tree-pruning/

给你二叉树的根结点 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.val01

思路

这里我们可以通过题目可以,我们要先考虑左右子树才能判断当前节点是否能够删除,所以这里我们采用后序遍历方式来使用递归解决此问题

代码

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

        root->left=pruneTree(root->left);
        root->right=pruneTree(root->right);

        if(!root->left&&!root->right&&root->val==0){
            delete root;
            root=nullptr;
        }

        return root;
    }
};
  1. TreeNode* pruneTree(TreeNode* root): 这是主要的函数,用于修剪二叉树。它通过递归调用pruneTree函数来修剪左子树和右子树,并在修剪后检查当前节点是否需要删除。
  2. if (!root) return nullptr;: 如果当前节点为空,直接返回nullptr,表示空节点。
  3. root->left = pruneTree(root->left);root->right = pruneTree(root->right);: 递归调用pruneTree函数,分别修剪左子树和右子树。
  4. if (!root->left && !root->right && root->val == 0) {: 如果当前节点是叶子节点且值为0,表示可以删除该节点。
    • delete root;: 释放当前节点的内存。
    • root = nullptr;: 将当前节点指针置为空,确保不再指向已删除的节点。
  5. return root;: 返回修剪后的当前节点,包括修剪后的左子树和右子树。

04.验证二叉搜索树

题目链接:https://leetcode.cn/problems/validate-binary-search-tree/

给你一个二叉树的根节点 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

思路

这里我们可以采用二叉搜索树的一个性质,即中序遍历后为一个有序序列,所以我们只需要中序遍历二叉树时判断当前节点是否比前一个节点大即可,在遍历左子树和当前节点时,不符合直接返回false可以剪去不必要遍历的枝。

代码

class Solution {
    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&&right&&cur;
    }
};
  1. long prev = LONG_MIN;: 这个类成员变量用于追踪上一个访问的节点的值,初始值设置为LONG_MIN,即负无穷,以确保对根节点的验证不会出现问题。

  2. bool isValidBST(TreeNode* root): 这是主要的函数,用于验证二叉搜索树是否合法。

    • if (!root) return true;: 如果当前节点为空,表示是合法的BST,直接返回true
    • bool left = isValidBST(root->left);: 递归验证左子树是否合法。
    • if (!left) return false;: 如果左子树不合法,直接返回false,因为整个树已经不合法了。
    • bool cur = false;: 初始化一个变量cur,用于表示当前节点是否满足BST的条件。
    • if (root->val > prev) cur = true;: 如果当前节点的值大于上一个节点的值,说明当前节点满足BST的条件。
    • if (!cur) return false;: 如果当前节点不满足BST的条件,直接返回false
    • prev = root->val;: 更新prev为当前节点的值,以备下次验证使用。
    • bool right = isValidBST(root->right);: 递归验证右子树是否合法。
    • return left && right && cur;: 返回左子树、右子树和当前节点是否都满足BST的条件。

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

题目链接:https://leetcode.cn/problems/kth-smallest-element-in-a-bst/

给定一个二叉搜索树的根节点 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

**进阶:**如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

思路

根据上面的题目我们不难想到使用中序遍历和计数的方式来找到第k小的值,并加上计数器的剪枝优化。

代码

class Solution {
    int count,ret;
public:
    int kthSmallest(TreeNode* root, int k) {
        count=k;
        dfs(root);
        return ret;
    }

    void dfs(TreeNode* root){
        if(root==nullptr||!count) return;

        dfs(root->left);
        count--;
        if(!count) ret=root->val;
        dfs(root->right);
    }
};
  1. int count, ret;: 这两个类成员变量用于追踪当前要找的第 k 小的元素的数量(count)和最终找到的结果(ret)。
  2. int kthSmallest(TreeNode* root, int k): 这是主要的函数,用于调用 DFS 函数并返回结果。首先将 count 初始化为 k,然后调用 dfs(root)
  3. void dfs(TreeNode* root): 这是深度优先搜索(DFS)函数,用于在二叉搜索树中找到第 k 小的元素。
    • if (root == nullptr || !count) return;: 如果当前节点为空或者已经找到了第 k 小的元素,直接返回。
    • dfs(root->left);: 递归调用 DFS 函数,遍历左子树。
    • count--;: 每次访问一个节点,将 count 减一,表示已经找到了一个较小的元素。
    • if (!count) ret = root->val;: 如果 count 变为零,表示找到了第 k 小的元素,将结果记录在 ret 中。
    • dfs(root->right);: 递归调用 DFS 函数,遍历右子树。
  4. return ret;: 返回最终找到的第 k 小的元素。

06.二叉树的所有路径

题目链接:https://leetcode.cn/problems/binary-tree-paths/

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

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

示例 1:

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

示例 2:

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

提示:

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

思路

使用深度优先遍历(DFS)来解决问题。路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加入到路径中。如果该节点为叶子节点,将路径存储到结果中。否则,将"->"加入到路径中并递归遍历该节点的左右子树。定义一个结果数组,在递归过程中进行路径的构建和存储。具体递归实现方法如下:

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

需要注意的是这里我们传入path时,不传引用,否则回溯需要另外处理。

代码

class Solution {
    vector<string> ret;
public:
    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&&!root->right){
            ret.push_back(path);
            return;
        }
        path+="->";

        if(root->left) dfs(root->left,path);
        if(root->right) dfs(root->right,path);
    }
};
  1. vector<string> ret;: 用于存储所有找到的路径。
  2. vector<string> binaryTreePaths(TreeNode* root): 主函数,初始化路径字符串为空,然后调用 DFS 函数。
  3. void dfs(TreeNode* root, string path): 深度优先搜索函数,递归生成从根节点到叶子节点的路径。
    • path += to_string(root->val);: 将当前节点的值加入到路径中。
    • if (!root->left && !root->right) {: 如果当前节点是叶子节点,将当前路径加入结果集,并返回。
    • path += "->";: 如果当前节点不是叶子节点,将箭头符号加入路径,表示连接到下一个节点。
    • if (root->left) dfs(root->left, path);: 递归调用 DFS 函数,搜索左子树。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱学习的鱼佬

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

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

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

打赏作者

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

抵扣说明:

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

余额充值