【递归、搜索与回溯算法】(二叉树深搜模型拆解与经典题型全面突破)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习的进阶阶段,递归、搜索与回溯几乎是绕不开的核心主题,而其中二叉树深搜模型更是连接"树结构理解"和"搜索思想应用"的关键桥梁.很多同学在刷题时会发现:明明知道二叉树要用 DFS,明明也写过递归,但一到具体题目就容易出现思路混乱、边界不清、状态设计困难的问题.归根结底,不是题目太难,而是对"深搜模型"的底层逻辑还没有真正拆透.事实上,二叉树上的很多经典题目,无论是遍历、路径统计、树形 DP、最近公共祖先,还是回溯式搜索,本质上都可以归结为几个稳定且高频的 DFS 思维框架.只要抓住这些框架,建立起"从节点出发如何思考、从子树返回什么信息、递归过程如何组织状态"的统一认知,原本零散的题型就会逐渐串联起来,形成完整的方法体系.因此,本文将围绕"二叉树深搜模型拆解"这一主线,系统梳理递归、搜索与回溯在树结构中的应用方式,从基础思想入手,逐步分析常见模型与易错点,并结合经典题型进行全面突破.希望你读完之后,不只是会做几道题,而是真正掌握二叉树DFS背后的通用套路,建立起面对同类问题时能够快速识别模型、精准下手的能力.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.二叉树的深度优先搜索算法思想背景

二叉树的深度优先搜索,本质思想是:沿着一条分支尽可能往下走,走到不能再走时,再回退到上一个结点,改走另一条分支.这是一种很自然的"探索式"思维方式,背景可以从几个角度理解.
1.思想来源

它来源于人们处理"树状结构"问题的直觉.

把二叉树想成一棵真正的树,从根结点出发,你有两条路可选:左子树和右子树.深度优先搜索不是一层一层看,而是先选一条路一直走到底,再退回来找别的路.这和人在迷宫里"先一直往前探,走不通再回头"的策略很像.
2.为什么适合二叉树

二叉树本身就是一种递归定义的结构:

(1)一棵二叉树由根结点、左子树、右子树组成

(2)左子树和右子树本身又都是二叉树

所以处理整棵树的问题时,可以自然拆成:

(1)先处理当前结点

(2)再处理左子树

(3)再处理右子树

这正好对应深度优先搜索的递归思想.
3.核心算法思想

DFS 的核心是两点:

(1)深入:优先沿某个孩子结点一直向下

(2)回溯:当当前路径走完后,返回父结点继续搜索其他分支

在二叉树中,常见 DFS 访问方式有三种:

(1)前序遍历:根 → 左 → 右

(2)中序遍历:左 → 根 → 右

(3)后序遍历:左 → 右 → 根

它们本质上都是 DFS,只是"访问根结点"的时机不同.
4.数学与程序设计背景

DFS 特别适合递归,是因为它符合"分治"思想:

(1)大问题:遍历整棵树

(2)小问题:遍历左子树、遍历右子树

(3)终止条件:遇到空结点就返回

所以代码通常非常简洁,例如前序遍历思想可以写成:

cpp 复制代码
def dfs(root):
    if root is None:
        return
    print(root.val)   # 访问当前结点
    dfs(root.left)    # 深入左子树
    dfs(root.right)   # 深入右子树

5.这种思想的意义

DFS 在二叉树中很重要,因为它能解决很多问题,比如:

(1)遍历所有结点

(2)求树的高度/深度

(3)判断两棵树是否相同

(4)查找某条路径

(5)求最近公共祖先

(6)判断是否平衡树

因为这些问题都适合通过"先解决子树,再组合结果"的方式完成.
6.一句话概括

二叉树深度优先搜索的思想背景就是:利用二叉树天然的递归结构,按照"一条路走到底,再回退分支"的方式遍历和求解问题.


2.计算布尔二叉树的值(OJ题)


算法思路:解法(递归):

本题可以被解释为:

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

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

(1)所有子节点的值;

(2)通过子节点的值运算出当前节点值.

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

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

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

递归函数流程:

  1. 判断是否为叶子节点
    因为这棵树是完全二叉树,只要左孩子为空,就说明是叶子节点,直接返回对应布尔值.
  2. 递归拆解问题
    把整棵树的计算,拆成左子树计算+右子树计算,递归求解两个子问题.
  3. 合并子问题结果
    根据当前节点的运算符:
    2 = OR(或):只要左/右有一个为 true,结果就是true
    3 = AND(与):必须左/右都为true,结果才是true
  4. 返回最终结果
    自底向上完成整棵树的计算.


核心代码

cpp 复制代码
class Solution 
{
public:
    //核心递归函数:输入根节点,返回整棵树的布尔计算结果
    bool evaluateTree(TreeNode* root) 
    {
        //1.递归终止条件(叶子节点)
        //布尔二叉树是【完全二叉树】:非叶子节点一定有左右孩子
        //因此 左孩子=空 ⇨ 当前节点是叶子节点
        if (root->left == nullptr)
            //叶子节点:0 → false,1 → true(三元运算符简写)
            return root->val == 0 ? false : true;

        //2.递归计算左右子树
        //递归计算【左子树】的结果
        bool left = evaluateTree(root->left);
        //递归计算【右子树】的结果
        bool right = evaluateTree(root->right);

        //3.根据当前节点运算符计算结果
        //节点值=2 → 逻辑或 OR (left | right)
        //节点值=3 → 逻辑与 AND (left & right)
        return root->val == 2 ? left | right : left & right;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
using namespace std;

//二叉树结点定义
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 == 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;
    }
};

int main()
{
    Solution s;

    // 测试1:
    //      2
    //     / \
    //    1   3
    //       / \
    //      0   1
    //
    //含义:1 OR (0 AND 1)
    //结果:true OR false = true

    TreeNode* root1 = new TreeNode(2);
    root1->left = new TreeNode(1);
    root1->right = new TreeNode(3);
    root1->right->left = new TreeNode(0);
    root1->right->right = new TreeNode(1);

    cout << "Test 1 result: " << s.evaluateTree(root1) << endl;  // 1

    // 测试2:
    //      3
    //     / \
    //    1   0
    //
    //含义:1 AND 0
    //结果:false

    TreeNode* root2 = new TreeNode(3);
    root2->left = new TreeNode(1);
    root2->right = new TreeNode(0);

    cout << "Test 2 result: " << s.evaluateTree(root2) << endl;  // 0

    // 测试3:
    //      2
    //     / \
    //    0   1
    //
    //含义:0 OR 1
    //结果:true

    TreeNode* root3 = new TreeNode(2);
    root3->left = new TreeNode(0);
    root3->right = new TreeNode(1);

    cout << "Test 3 result: " << s.evaluateTree(root3) << endl;  // 1

    //测试4:
    //单个叶子结点 0
    TreeNode* root4 = new TreeNode(0);
    cout << "Test 4 result: " << s.evaluateTree(root4) << endl;  // 0

    //测试5:
    //单个叶子结点 1
    TreeNode* root5 = new TreeNode(1);
    cout << "Test 5 result: " << s.evaluateTree(root5) << endl;  // 1

    return 0;
}

3.求根节点到叶节点数字之和(OJ题)


算法思路:解法(dfs - 前序遍历):

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

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

  1. 将父节点的数字与当前节点的信息整合到一起,计算出当前节点的数字,然后传递到下一层进行递归;
  2. 当遇到叶子节点的时候,就不再向下传递信息,而是将整合的结果向上一直回溯到根节点.

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

算法流程:

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

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

递归函数流程:

  1. 当遇到空节点的时候,说明这条路从根节点开始没有分支,返回0;
  2. 结合父节点传下的信息以及当前节点的val,计算出当前节点数字 sum;
  3. 如果当前结点是叶子节点,直接返回整合后的结果 sum;
  4. 如果当前结点不是叶子节点,将 sum 传到左右子树中去,得到左右子树中节点路径的数字和,然后相加后返回结果.
  5. 向下传递:从根节点开始,每往下走一层,就把之前的数字×10加上当前节点值,生成新的路径数字;
    向上汇总:走到叶子节点时,得到一条完整路径的数字,回溯时把所有路径数字加起来,就是最终答案.

核心代码

cpp 复制代码
class Solution 
{
public:
    //主函数:程序入口,调用递归函数,初始前缀和为 0
    int sumNumbers(TreeNode* root) { 
        return dfs(root, 0); 
    }

    //核心递归函数:DFS深度优先遍历
    //root:当前遍历的节点
    //presum:从【根节点】到【当前节点的父节点】的路径数字(前缀和)
    int dfs(TreeNode* root, int presum) 
    {
        //1.计算当前节点的路径值
        //核心公式:前缀和 ×10(数字左移一位)+ 当前节点值 = 根到当前节点的完整数字
        //例:presum=1 → ×10=10 + 当前值2 → 12
        presum = presum * 10 + root->val;

        //2.递归终止条件:遇到叶子节点
        //叶子节点定义:左右孩子都为空
        if (root->left == nullptr && root->right == nullptr)
            //叶子节点:这条路径的数字计算完成,直接返回
            return presum;

        //3.递归遍历左右子树
        //初始化结果变量,用于累加左右子树的路径和
        int ret = 0;

        //如果左子树存在,递归计算左子树所有路径和,并累加
        if (root->left)
            ret += dfs(root->left, presum);

        //如果右子树存在,递归计算右子树所有路径和,并累加
        if (root->right)
            ret += dfs(root->right, presum);

        //返回当前节点【所有子树的路径总和】
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
using namespace std;

//二叉树结点定义
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)
    {
        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;
    }
};

int main()
{
    Solution s;

    //测试1
    //      1
    //     / \
    //    2   3
    //
    //路径:
    //1->2 = 12
    //1->3 = 13
    //总和 = 25

    TreeNode* root1 = new TreeNode(1);
    root1->left = new TreeNode(2);
    root1->right = new TreeNode(3);

    cout << "Test 1 result: " << s.sumNumbers(root1) << endl;  // 25


    //测试2
    //        4
    //       / \
    //      9   0
    //     / \
    //    5   1
    //
    //路径:
    //4->9->5 = 495
    //4->9->1 = 491
    //4->0   = 40
    //总和 = 495 + 491 + 40 = 1026

    TreeNode* root2 = new TreeNode(4);
    root2->left = new TreeNode(9);
    root2->right = new TreeNode(0);
    root2->left->left = new TreeNode(5);
    root2->left->right = new TreeNode(1);

    cout << "Test 2 result: " << s.sumNumbers(root2) << endl;  //1026


    //测试3
    //只有一个结点
    //    7
    //
    //路径:
    // 7
    //总和 = 7

    TreeNode* root3 = new TreeNode(7);

    cout << "Test 3 result: " << s.sumNumbers(root3) << endl;  // 7


    //测试4
    //        2
    //       /
    //      3
    //     /
    //    4
    //
    //路径:
    //2->3->4 = 234
    //总和 = 234

    TreeNode* root4 = new TreeNode(2);
    root4->left = new TreeNode(3);
    root4->left->left = new TreeNode(4);

    cout << "Test 4 result: " << s.sumNumbers(root4) << endl;  // 234


    //测试5
    //        1
    //       / \
    //      0   5
    //     /   / \
    //    3   6   7
    //
    //路径:
    // 1->0->3 = 103
    // 1->5->6 = 156
    // 1->5->7 = 157
    //总和 = 103 + 156 + 157 = 416

    TreeNode* root5 = new TreeNode(1);
    root5->left = new TreeNode(0);
    root5->right = new TreeNode(5);
    root5->left->left = new TreeNode(3);
    root5->right->left = new TreeNode(6);
    root5->right->right = new TreeNode(7);

    cout << "Test 5 result: " << s.sumNumbers(root5) << endl;  // 416

    return 0;
}

4.二叉树剪枝(OJ题)


算法思路:解法(dfs - 后序遍历):

后序遍历按照左子树、右子树、根节点的顺序遍历二叉树的所有节点,通常用于父节点的状态依赖于子节点状态的题目.如果我们选择从上往下删除,我们需要收集左右子树的信息,这可能导致代码编写相对困难.然而,通过观察我们可以发现,如果我们先删除最底部的叶子节点,然后再处理删除后的节点,最终的结果并不会受到影响.因此,我们可以采用后序遍历的方式来解决这个问题.在后序遍历中,我们先处理左子树,然后处理右子树,最后再处理当前节点.在处理当前节点时,我们可以判断其是否为叶子节点且其值是否为0,如果满足条件,我们可以删除当前节点.需要注意的是,在删除叶子节点时,其父节点很可能会成为新的叶子节点.因此,在处理完子节点后,我们仍然需要处理当前节点.这也是为什么选择后序遍历的原因(后序遍历首先遍历到的一定是叶子节点).通过使用后序遍历,我们可以逐步删除叶子节点,并且保证删除后的节点仍然满足删除操作的要求.这样,我们可以较为方便地实现删除操作,而不会影响最终的结果.若在处理结束后所有叶子节点的值均为1,则所有子树均包含1,此时可以返回.
算法流程:

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

  1. 返回值:无;
  2. 参数:当前需要处理的节点;
  3. 函数作用:判断当前节点是否需要删除,若需要删除,则删除当前节点.

后序遍历的主要流程:

  1. 递归出口:当传入节点为空时,不做任何处理;
  2. 递归处理左子树;
  3. 递归处理右子树;
  4. 处理当前节点:判断该节点是否为叶子节点(即左右子节点均被删除,当前节点成为叶子节点),并且节点的值为0:
    (1)如果是,就删除掉;
    (2)如果不是,就不做任何处理.

    核心代码
cpp 复制代码
class Solution 
{
public:
    //主函数:二叉树剪枝,输入根节点,返回剪枝后的根节点
    TreeNode* pruneTree(TreeNode* root) 
    {
        //1.递归终止条件
        //如果当前节点为空,直接返回空(没有节点需要剪)
        if (root == nullptr)
            return nullptr;

        //2.后序遍历:先递归处理左右子树
        //递归剪枝左子树,并把处理后的结果赋值给当前节点的左孩子
        //关键:必须先处理子节点,再处理父节点(后序遍历)
        root->left = pruneTree(root->left);
        //递归剪枝右子树,并把处理后的结果赋值给当前节点的右孩子
        root->right = pruneTree(root->right);

        //3.剪枝核心判断:是否删除当前节点 
        //条件:当前节点是【叶子节点】(左右都空) + 节点值为0 → 剪掉这个节点
        if (root->left == nullptr && root->right == nullptr && root->val == 0) 
        {
            //工程中建议加上 delete root; 释放内存,防止内存泄漏
            //将节点置为空,代表剪掉这个节点
            root = nullptr;
        }

        //4. 返回处理后的当前节点
        //要么返回原节点(没剪),要么返回空(已剪掉)
        return root;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <string>
using namespace std;

//二叉树结点定义
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 == 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;
            return nullptr;
        }

        return root;
    }
};

//根据层序数组建树,-1 表示 null
TreeNode* buildTree(const vector<int>& nums)
{
    if (nums.empty() || nums[0] == -1)
        return nullptr;

    TreeNode* root = new TreeNode(nums[0]);
    queue<TreeNode*> q;
    q.push(root);

    int i = 1;
    while (!q.empty() && i < (int)nums.size())
    {
        TreeNode* cur = q.front();
        q.pop();

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->left = new TreeNode(nums[i]);
            q.push(cur->left);
        }
        i++;

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->right = new TreeNode(nums[i]);
            q.push(cur->right);
        }
        i++;
    }

    return root;
}

//二叉树转成 LeetCode 风格层序数组字符串
vector<string> treeToVector(TreeNode* root)
{
    vector<string> res;
    if (root == nullptr)
        return res;

    queue<TreeNode*> q;
    q.push(root);

    while (!q.empty())
    {
        TreeNode* cur = q.front();
        q.pop();

        if (cur)
        {
            res.push_back(to_string(cur->val));
            q.push(cur->left);
            q.push(cur->right);
        }
        else
        {
            res.push_back("null");
        }
    }

    //删除末尾多余的 null
    while (!res.empty() && res.back() == "null")
        res.pop_back();

    return res;
}

// 打印数组
void printVector(const vector<string>& vec)
{
    cout << "[";
    for (int i = 0; i < (int)vec.size(); i++)
    {
        cout << vec[i];
        if (i != (int)vec.size() - 1)
            cout << ",";
    }
    cout << "]" << endl;
}

//释放整棵树
void deleteTree(TreeNode* root)
{
    if (root == nullptr)
        return;
    deleteTree(root->left);
    deleteTree(root->right);
    delete root;
}

int main()
{
    Solution s;

    //测试1: [1,0,1,0,0,0,1]
    vector<int> nums1 = {1, 0, 1, 0, 0, 0, 1};
    TreeNode* root1 = buildTree(nums1);

    cout << "Test 1 before prune: ";
    printVector(treeToVector(root1));

    root1 = s.pruneTree(root1);

    cout << "Test 1 after prune:  ";
    printVector(treeToVector(root1));

    deleteTree(root1);
    cout << "---------------------" << endl;

    //测试2: [1,0,0]
    vector<int> nums2 = {1, 0, 0};
    TreeNode* root2 = buildTree(nums2);

    cout << "Test 2 before prune: ";
    printVector(treeToVector(root2));

    root2 = s.pruneTree(root2);

    cout << "Test 2 after prune:  ";
    printVector(treeToVector(root2));

    deleteTree(root2);
    cout << "---------------------" << endl;

    //测试3: [0]
    vector<int> nums3 = {0};
    TreeNode* root3 = buildTree(nums3);

    cout << "Test 3 before prune: ";
    printVector(treeToVector(root3));

    root3 = s.pruneTree(root3);

    cout << "Test 3 after prune:  ";
    printVector(treeToVector(root3));

    deleteTree(root3);
    cout << "---------------------" << endl;

    //测试4: [1,1,0,null,null,1,0]
    //这里用 -1 表示 null
    vector<int> nums4 = {1, 1, 0, -1, -1, 1, 0};
    TreeNode* root4 = buildTree(nums4);

    cout << "Test 4 before prune: ";
    printVector(treeToVector(root4));

    root4 = s.pruneTree(root4);

    cout << "Test 4 after prune:  ";
    printVector(treeToVector(root4));

    deleteTree(root4);

    return 0;
}

5.验证二叉搜索树(OJ题)


算法思路:解法(利用中序遍历):

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

如果一棵树是二叉搜索树,那么它的中序遍历的结果一定是一个严格递增的序列.因此,我们可以初始化一个无穷小的全区变量,用来记录中序遍历过程中的前驱结点.那么就可以在中序遍历的过程中,先判断是否和前驱结点构成递增序列,然后修改前驱结点为当前结点,传入下一层的递归中.
算法流程:

  1. 初始化一个全局的变量 prev,用来记录中序遍历过程中的前驱结点的 val;
  2. 中序遍历的递归函数中:
    (1)设置递归出口:root == nullptr 的时候,返回 true;
    (2)先递归判断左子树是否是二叉搜索树,用 retleft 标记;
    (3)然后判断当前结点是否满足二叉搜索树的性质,用 retcur 标记:
    • 如果当前结点的 val 大于 prev,说明满足条件,retcur 改为 true;
    • 如果当前结点的 val 小于等于 prev,说明不满足条件,retcur 改为 false;
      (4)最后递归判断右子树是否是二叉搜索树,用 retright 标记;
  3. 只有当 retleftretcurretright 都是 true 的时候,才返回 true.

核心代码

cpp 复制代码
class Solution 
{
    //核心:记录中序遍历中【上一个节点的值】
    //初始化为 long 类型的最小值,避免边界值冲突(如节点值为 int 最小值)
    long prev = LONG_MIN;

public:
    //主函数:递归验证是否为有效BST
    bool isValidBST(TreeNode* root) 
    {
        //1.递归终止条件 
        //空节点默认是有效的二叉搜索树
        if (root == nullptr)
            return true;

        //2.中序遍历:递归遍历左子树 
        //中序顺序:左 → 根 → 右,先校验左子树
        bool left = isValidBST(root->left);

        //3.剪枝优化 
        //左子树已经无效,直接返回 false,无需继续遍历
        if (left == false)
            return false;

        //4.验证当前节点:严格大于前驱节点 
        bool cur = false;
        //BST 中序必须严格递增:当前节点值 > 上一个节点值
        if (root->val > prev)
            cur = true;

        //剪枝:当前节点不满足递增,直接返回 false
        if (cur == false)
            return false;

        //5.更新前驱节点
        //把当前节点值,作为下一个节点的「前驱值」
        prev = root->val;

        //6.中序遍历:递归遍历右子树 
        bool right = isValidBST(root->right);

        //7.最终结果 
        //左子树、当前节点、右子树 全部有效,才是真BST
        return left && right && cur;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;

//二叉树结点定义
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
{
    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;

        //剪枝
        if (cur == false)
            return false;

        prev = root->val;

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

//根据层序数组建树,-1 表示 null
TreeNode* buildTree(const vector<int>& nums)
{
    if (nums.empty() || nums[0] == -1)
        return nullptr;

    TreeNode* root = new TreeNode(nums[0]);
    queue<TreeNode*> q;
    q.push(root);

    int i = 1;
    while (!q.empty() && i < (int)nums.size())
    {
        TreeNode* cur = q.front();
        q.pop();

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->left = new TreeNode(nums[i]);
            q.push(cur->left);
        }
        i++;

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->right = new TreeNode(nums[i]);
            q.push(cur->right);
        }
        i++;
    }

    return root;
}

//中序遍历输出,便于观察
void inorder(TreeNode* root)
{
    if (root == nullptr) return;
    inorder(root->left);
    cout << root->val << " ";
    inorder(root->right);
}

//释放整棵树
void deleteTree(TreeNode* root)
{
    if (root == nullptr) return;
    deleteTree(root->left);
    deleteTree(root->right);
    delete root;
}

int main()
{
    cout << boolalpha;

    //测试1: [2,1,3]
    vector<int> nums1 = {2, 1, 3};
    TreeNode* root1 = buildTree(nums1);

    cout << "Test 1 inorder: ";
    inorder(root1);
    cout << endl;

    Solution s1;
    cout << "Test 1 result: " << s1.isValidBST(root1) << endl;
    cout << "------------------------" << endl;
    deleteTree(root1);

    //测试2: [5,1,4,null,null,3,6]
    //用 -1 表示 null
    vector<int> nums2 = {5, 1, 4, -1, -1, 3, 6};
    TreeNode* root2 = buildTree(nums2);

    cout << "Test 2 inorder: ";
    inorder(root2);
    cout << endl;

    Solution s2;
    cout << "Test 2 result: " << s2.isValidBST(root2) << endl;
    cout << "------------------------" << endl;
    deleteTree(root2);

    //测试3: [1]
    vector<int> nums3 = {1};
    TreeNode* root3 = buildTree(nums3);

    cout << "Test 3 inorder: ";
    inorder(root3);
    cout << endl;

    Solution s3;
    cout << "Test 3 result: " << s3.isValidBST(root3) << endl;
    cout << "------------------------" << endl;
    deleteTree(root3);

    //测试4: [2,2,3]
    vector<int> nums4 = {2, 2, 3};
    TreeNode* root4 = buildTree(nums4);

    cout << "Test 4 inorder: ";
    inorder(root4);
    cout << endl;

    Solution s4;
    cout << "Test 4 result: " << s4.isValidBST(root4) << endl;
    cout << "------------------------" << endl;
    deleteTree(root4);

    //测试5: [10,5,15,2,12]
    vector<int> nums5 = {10, 5, 15, 2, 12};
    TreeNode* root5 = buildTree(nums5);

    cout << "Test 5 inorder: ";
    inorder(root5);
    cout << endl;

    Solution s5;
    cout << "Test 5 result: " << s5.isValidBST(root5) << endl;
    cout << "------------------------" << endl;
    deleteTree(root5);

    return 0;
}

6.二叉搜索树中的第k小的元素(OJ题)


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

上述解法不仅使用大量额外空间存储数据,并且会将所有的结点都遍历一遍.但是,我们可以根据中序遍历的过程,只需扫描前k个结点即可.因此,我们可以创建一个全局的计数器 count,将其初始化为 k,每遍历一个节点就将 count--.直到某次递归的时候,count 的值等于1,说明此时的结点就是我们要找的结果.
算法流程:

  1. 定义一个全局的变量 count,在主函数中初始化为 k 的值(不用全局也可以,当成参数传入递归过程中);
  2. 递归函数的设计:int dfs(TreeNode* root): 返回值为第k个结点;

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

  1. 递归出口:空节点直接返回 -1,说明没有找到;
  2. 去左子树上查找结果,记为 retleft:
    (1)如果 retleft == -1,说明没找到,继续执行下面逻辑;
    (2)如果 retleft != -1,说明找到了,直接返回结果,无需执行下面代码(剪枝);
  3. 如果左子树没找到,判断当前结点是否符合:如果符合,直接返回结果
  4. 如果当前结点不符合,去右子树上寻找结果.

核心代码

cpp 复制代码
class Solution 
{
    //成员变量1:计数器,记录还需要找到第几个节点
    int count;
    //成员变量2:存储最终结果(第k小的节点值)
    int ret;

public:
    //主函数:入口,输入根节点和k,返回第k小的值
    int kthSmallest(TreeNode* root, int k) 
    {
        //初始化计数器:从k开始倒数
        count = k;
        //调用DFS中序遍历
        dfs(root);
        //返回找到的结果
        return ret;
    }

    //核心DFS函数:中序遍历(左→根→右)
    void dfs(TreeNode* root) 
    {
        //1.递归终止+剪枝条件
        //节点为空  || 已经找到答案(count=0),直接退出,不继续遍历
        if (root == nullptr || count == 0)
            return;

        //2.中序遍历:递归左子树
        //BST最小值在最左侧,优先遍历左子树
        dfs(root->left);

        //3.处理当前节点(核心计数)
        //遍历到当前节点,计数器-1
        count--;
        //计数器减到0 → 当前节点就是【第k小的节点】
        if (count == 0)
            ret = root->val;  // 把答案存入ret

        //4.中序遍历:递归右子树
        dfs(root->right);
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

//二叉树结点定义
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 count;
    int ret;

public:
    int kthSmallest(TreeNode* root, int k)
    {
        count = k;
        ret = -1;   //防止未初始化
        dfs(root);
        return ret;
    }

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

        dfs(root->left);

        count--;
        if (count == 0)
        {
            ret = root->val;
            return;
        }

        dfs(root->right);
    }
};

//根据层序数组建树,-1 表示 null
TreeNode* buildTree(const vector<int>& nums)
{
    if (nums.empty() || nums[0] == -1)
        return nullptr;

    TreeNode* root = new TreeNode(nums[0]);
    queue<TreeNode*> q;
    q.push(root);

    int i = 1;
    while (!q.empty() && i < (int)nums.size())
    {
        TreeNode* cur = q.front();
        q.pop();

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->left = new TreeNode(nums[i]);
            q.push(cur->left);
        }
        i++;

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->right = new TreeNode(nums[i]);
            q.push(cur->right);
        }
        i++;
    }

    return root;
}

//中序遍历输出
void inorder(TreeNode* root)
{
    if (root == nullptr) return;
    inorder(root->left);
    cout << root->val << " ";
    inorder(root->right);
}

//释放整棵树
void deleteTree(TreeNode* root)
{
    if (root == nullptr) return;
    deleteTree(root->left);
    deleteTree(root->right);
    delete root;
}

int main()
{
    // ---------------- 测试1 ----------------
    // [3,1,4,null,2]
    // 中序:1 2 3 4
    vector<int> nums1 = {3, 1, 4, -1, 2};
    TreeNode* root1 = buildTree(nums1);

    cout << "Test 1 inorder: ";
    inorder(root1);
    cout << endl;

    Solution s1;
    cout << "k = 1, result = " << s1.kthSmallest(root1, 1) << endl;

    Solution s2;
    cout << "k = 2, result = " << s2.kthSmallest(root1, 2) << endl;

    Solution s3;
    cout << "k = 3, result = " << s3.kthSmallest(root1, 3) << endl;

    Solution s4;
    cout << "k = 4, result = " << s4.kthSmallest(root1, 4) << endl;

    cout << "------------------------" << endl;
    deleteTree(root1);

    // ---------------- 测试2 ----------------
    // [5,3,6,2,4,null,null,1]
    // 中序:1 2 3 4 5 6
    vector<int> nums2 = {5, 3, 6, 2, 4, -1, -1, 1};
    TreeNode* root2 = buildTree(nums2);

    cout << "Test 2 inorder: ";
    inorder(root2);
    cout << endl;

    Solution s5;
    cout << "k = 3, result = " << s5.kthSmallest(root2, 3) << endl;

    Solution s6;
    cout << "k = 5, result = " << s6.kthSmallest(root2, 5) << endl;

    cout << "------------------------" << endl;
    deleteTree(root2);

    // ---------------- 测试3 ----------------
    // [7]
    vector<int> nums3 = {7};
    TreeNode* root3 = buildTree(nums3);

    cout << "Test 3 inorder: ";
    inorder(root3);
    cout << endl;

    Solution s7;
    cout << "k = 1, result = " << s7.kthSmallest(root3, 1) << endl;

    cout << "------------------------" << endl;
    deleteTree(root3);

    return 0;
}

7.二叉树的所有路径(OJ题)


算法思路:解法(回溯):

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

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

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

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

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

具体实现方法如下:

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

核心代码

cpp 复制代码
class Solution 
{
public:
    //成员变量:存储最终所有路径的结果集
    vector<string> ret; 

    //主函数:程序入口,返回所有路径
    vector<string> binaryTreePaths(TreeNode* root) 
    {
        string path; //初始化空路径字符串
        //特殊情况:根节点为空,直接返回空结果
        if (root == nullptr)
            return ret;
        //调用DFS递归遍历,开始拼接路径
        dfs(root, path);
        //返回所有路径结果
        return ret;
    }

    //核心DFS递归函数:root=当前节点,path=从根到当前节点的路径(值传递)
    void dfs(TreeNode* root, string path) 
    {
        //1.将当前节点的值转为字符串,拼接到路径中
        path += to_string(root->val);

        //2.递归终止条件:当前节点是【叶子节点】(左右孩子都为空)
        if (root->left == nullptr && root->right == nullptr) 
        {
            //一条完整路径生成,加入结果集
            ret.push_back(path);
            return;
        }

        //3.非叶子节点:拼接路径分隔符 "->"
        path += "->";

        //4.递归遍历左子树(存在才遍历)
        if (root->left)
            dfs(root->left, path);
        //5.递归遍历右子树(存在才遍历)
        dfs(root->right, path);
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <string>
using namespace std;

//二叉树结点定义
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)
    {
        ret.clear();   //防止同一个对象重复调用时结果叠加
        string path;

        if (root == nullptr)
            return ret;

        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);
    }
};

//根据层序数组建树,-1 表示 null
TreeNode* buildTree(const vector<int>& nums)
{
    if (nums.empty() || nums[0] == -1)
        return nullptr;

    TreeNode* root = new TreeNode(nums[0]);
    queue<TreeNode*> q;
    q.push(root);

    int i = 1;
    while (!q.empty() && i < (int)nums.size())
    {
        TreeNode* cur = q.front();
        q.pop();

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->left = new TreeNode(nums[i]);
            q.push(cur->left);
        }
        i++;

        if (i < (int)nums.size() && nums[i] != -1)
        {
            cur->right = new TreeNode(nums[i]);
            q.push(cur->right);
        }
        i++;
    }

    return root;
}

//打印 vector<string>
void printPaths(const vector<string>& paths)
{
    cout << "[";
    for (int i = 0; i < (int)paths.size(); i++)
    {
        cout << "\"" << paths[i] << "\"";
        if (i != (int)paths.size() - 1)
            cout << ", ";
    }
    cout << "]" << endl;
}

//释放整棵树
void deleteTree(TreeNode* root)
{
    if (root == nullptr) return;
    deleteTree(root->left);
    deleteTree(root->right);
    delete root;
}

int main()
{
    // ---------------- 测试1 ----------------
    // [1,2,3,-1,5]
    // 对应 [1,2,3,null,5]
    // 输出: ["1->2->5", "1->3"]
    vector<int> nums1 = {1, 2, 3, -1, 5};
    TreeNode* root1 = buildTree(nums1);

    Solution s1;
    vector<string> ans1 = s1.binaryTreePaths(root1);

    cout << "Test 1 result: ";
    printPaths(ans1);

    deleteTree(root1);
    cout << "------------------------" << endl;

    // ---------------- 测试2 ----------------
    // [1]
    // 输出: ["1"]
    vector<int> nums2 = {1};
    TreeNode* root2 = buildTree(nums2);

    Solution s2;
    vector<string> ans2 = s2.binaryTreePaths(root2);

    cout << "Test 2 result: ";
    printPaths(ans2);

    deleteTree(root2);
    cout << "------------------------" << endl;

    // ---------------- 测试3 ----------------
    // [1,2,3,4,-1,5,6]
    // 输出: ["1->2->4", "1->3->5", "1->3->6"]
    vector<int> nums3 = {1, 2, 3, 4, -1, 5, 6};
    TreeNode* root3 = buildTree(nums3);

    Solution s3;
    vector<string> ans3 = s3.binaryTreePaths(root3);

    cout << "Test 3 result: ";
    printPaths(ans3);

    deleteTree(root3);
    cout << "------------------------" << endl;

    // ---------------- 测试4 ----------------
    // 空树 []
    // 输出: []
    vector<int> nums4 = {};
    TreeNode* root4 = buildTree(nums4);

    Solution s4;
    vector<string> ans4 = s4.binaryTreePaths(root4);

    cout << "Test 4 result: ";
    printPaths(ans4);

    deleteTree(root4);
    cout << "------------------------" << endl;

    // ---------------- 测试5 ----------------
    // [10,20,-1,30]
    // 这其实会按层序构造成:
    //      10
    //     /
    //    20
    //   /
    //  30
    // 输出: ["10->20->30"]
    vector<int> nums5 = {10, 20, -1, 30};
    TreeNode* root5 = buildTree(nums5);

    Solution s5;
    vector<string> ans5 = s5.binaryTreePaths(root5);

    cout << "Test 5 result: ";
    printPaths(ans5);

    deleteTree(root5);

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【递归、搜索与回溯算法】(穷举vs暴搜vs深搜vs回溯vs剪枝:一文讲清概念与用法)


每日心灵鸡汤:"日子总归是向前走的,人要学会和不属于自己的东西说再见"
学会放弃是件挺厉害的事情,放弃内耗自己,放下某个不自信的执念,放弃不切实际的幻想,放弃紧握着不撒手的过去,放弃不适合自己的生活.放弃这个词看着挺丧气,但想放弃就意味着新的开始,意味着你将收获新的东西.如果你总是抓住过去的事情,不放过那些受过的委屈和伤害耿耿于怀,以及那些根本无法改变的人和事上,无畏的消耗自己,会对任何事情提不起兴趣,没办法投入新生活.
日子总归是向前的,我们总要学会和不属于你的东西说再见.

相关推荐
鱼鳞_1 小时前
Java学习笔记_Day32(IO流字符集字符流)
java·笔记·学习
handler011 小时前
Linux基础知识(1)
linux·服务器·c语言·开发语言·数据结构·c++
良木生香1 小时前
【C++ 初阶】:内存管理的迭代革新——从malloc/free 到 new/delete 的时代更迭
c语言·开发语言·c++
小则又沐风a2 小时前
深剖string内部结构 手撕string
java·前端·数据库·c++
泽02022 小时前
OJBalancer ----- 基于负载均衡仿leetcode的刷题界面
linux·leetcode·负载均衡
会编程的土豆2 小时前
常用算法里的细节
数据结构·c++·算法·图论
skilllite作者2 小时前
为什么我认为 Hermes 需要说明 self-evolution 的设计来源
人工智能·算法·rust·openclaw·agentskills
月诸清酒2 小时前
33-260416 AI 科技日报 (Gemini桌面应用登陆Mac,快捷键唤醒)
人工智能·macos
CHANG_THE_WORLD2 小时前
C 语言的 `fread` 与 C++ 的 `ifstream::read` 区别及设计哲学
java·c语言·c++