LeetCode第95题:不同的二叉搜索树 II

LeetCode第95题:不同的二叉搜索树 II

题目描述

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。

难度

中等

问题链接

不同的二叉搜索树 II

示例

示例 1:

ini 复制代码
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]

示例 2:

lua 复制代码
输入:n = 1
输出:[[1]]

提示

  • 1 <= n <= 8

解题思路

这道题要求我们生成所有可能的二叉搜索树(BST),其中包含 n 个节点,节点值从 1n。二叉搜索树的特性是对于任意节点,其左子树中的所有节点值都小于该节点的值,右子树中的所有节点值都大于该节点的值。

方法一:递归法

我们可以使用递归的方式来解决这个问题。对于给定的范围 [start, end],我们可以枚举每个数字 i 作为根节点,然后递归地构建左子树(范围为 [start, i-1])和右子树(范围为 [i+1, end])。最后,我们将所有可能的左子树和右子树组合起来,形成不同的二叉搜索树。

方法二:动态规划 + 递归

我们可以使用动态规划来优化递归过程,避免重复计算。我们可以使用一个哈希表或者二维数组来存储已经计算过的结果,这样当我们需要再次计算相同范围的二叉搜索树时,可以直接从缓存中获取结果。

关键点

  • 理解二叉搜索树的性质:左子树的所有节点值都小于根节点的值,右子树的所有节点值都大于根节点的值。
  • 使用递归来构建所有可能的二叉搜索树。
  • 可以使用动态规划来优化递归过程,避免重复计算。

算法步骤分析

递归法算法步骤

步骤 操作 说明
1 定义递归函数 函数接收起始值 start 和结束值 end,返回所有可能的二叉搜索树
2 处理基本情况 如果 start > end,返回一个包含 null 的列表
3 枚举根节点 startend 枚举每个数字 i 作为根节点
4 递归构建左子树 递归地构建左子树,范围为 [start, i-1]
5 递归构建右子树 递归地构建右子树,范围为 [i+1, end]
6 组合树 将所有可能的左子树和右子树组合起来,形成不同的二叉搜索树
7 返回结果 返回所有可能的二叉搜索树

动态规划 + 递归算法步骤

步骤 操作 说明
1 初始化缓存 创建一个缓存来存储已经计算过的结果
2 定义递归函数 函数接收起始值 start 和结束值 end,返回所有可能的二叉搜索树
3 检查缓存 如果缓存中已经有结果,直接返回
4 处理基本情况 如果 start > end,返回一个包含 null 的列表
5 枚举根节点 startend 枚举每个数字 i 作为根节点
6 递归构建左子树 递归地构建左子树,范围为 [start, i-1]
7 递归构建右子树 递归地构建右子树,范围为 [i+1, end]
8 组合树 将所有可能的左子树和右子树组合起来,形成不同的二叉搜索树
9 更新缓存 将结果存入缓存
10 返回结果 返回所有可能的二叉搜索树

算法可视化

以示例 n = 3 为例,我们使用递归法来生成所有可能的二叉搜索树:

  1. 对于范围 [1, 3],我们枚举每个数字作为根节点:

    • 根节点为 1:左子树为空,右子树为范围 [2, 3] 的所有二叉搜索树
    • 根节点为 2:左子树为范围 [1, 1] 的所有二叉搜索树,右子树为范围 [3, 3] 的所有二叉搜索树
    • 根节点为 3:左子树为范围 [1, 2] 的所有二叉搜索树,右子树为空
  2. 对于范围 [2, 3],我们枚举每个数字作为根节点:

    • 根节点为 2:左子树为空,右子树为范围 [3, 3] 的所有二叉搜索树
    • 根节点为 3:左子树为范围 [2, 2] 的所有二叉搜索树,右子树为空
  3. 对于范围 [1, 2],我们枚举每个数字作为根节点:

    • 根节点为 1:左子树为空,右子树为范围 [2, 2] 的所有二叉搜索树
    • 根节点为 2:左子树为范围 [1, 1] 的所有二叉搜索树,右子树为空
  4. 对于范围 [1, 1][2, 2][3, 3],只有一种可能的二叉搜索树,即只包含一个节点的树。

  5. 最终,我们得到以下 5 种不同的二叉搜索树:

    markdown 复制代码
    1         1          2         3       3
     \         \        / \       /       /
      2         3      1   3     1       2
       \       /                  \     /
        3     2                    2   1

代码实现

C# 实现

csharp 复制代码
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     public int val;
 *     public TreeNode left;
 *     public TreeNode right;
 *     public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
public class Solution {
    // 方法一:递归法
    public IList<TreeNode> GenerateTrees(int n) {
        if (n == 0) {
            return new List<TreeNode>();
        }
        return GenerateTreesRecursive(1, n);
    }
    
    private IList<TreeNode> GenerateTreesRecursive(int start, int end) {
        IList<TreeNode> result = new List<TreeNode>();
        
        // 如果起始值大于结束值,返回一个包含 null 的列表
        if (start > end) {
            result.Add(null);
            return result;
        }
        
        // 枚举每个数字作为根节点
        for (int i = start; i <= end; i++) {
            // 递归构建左子树
            IList<TreeNode> leftTrees = GenerateTreesRecursive(start, i - 1);
            
            // 递归构建右子树
            IList<TreeNode> rightTrees = GenerateTreesRecursive(i + 1, end);
            
            // 组合左子树和右子树
            foreach (TreeNode left in leftTrees) {
                foreach (TreeNode right in rightTrees) {
                    TreeNode root = new TreeNode(i);
                    root.left = left;
                    root.right = right;
                    result.Add(root);
                }
            }
        }
        
        return result;
    }
    
    // 方法二:动态规划 + 递归
    public IList<TreeNode> GenerateTreesDP(int n) {
        if (n == 0) {
            return new List<TreeNode>();
        }
        
        // 创建缓存
        Dictionary<(int, int), IList<TreeNode>> memo = new Dictionary<(int, int), IList<TreeNode>>();
        return GenerateTreesDP(1, n, memo);
    }
    
    private IList<TreeNode> GenerateTreesDP(int start, int end, Dictionary<(int, int), IList<TreeNode>> memo) {
        IList<TreeNode> result = new List<TreeNode>();
        
        // 如果起始值大于结束值,返回一个包含 null 的列表
        if (start > end) {
            result.Add(null);
            return result;
        }
        
        // 检查缓存
        if (memo.ContainsKey((start, end))) {
            return memo[(start, end)];
        }
        
        // 枚举每个数字作为根节点
        for (int i = start; i <= end; i++) {
            // 递归构建左子树
            IList<TreeNode> leftTrees = GenerateTreesDP(start, i - 1, memo);
            
            // 递归构建右子树
            IList<TreeNode> rightTrees = GenerateTreesDP(i + 1, end, memo);
            
            // 组合左子树和右子树
            foreach (TreeNode left in leftTrees) {
                foreach (TreeNode right in rightTrees) {
                    TreeNode root = new TreeNode(i);
                    root.left = left;
                    root.right = right;
                    result.Add(root);
                }
            }
        }
        
        // 更新缓存
        memo[(start, end)] = result;
        return result;
    }
}

Python 实现

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # 方法一:递归法
    def generateTrees(self, n: int) -> List[TreeNode]:
        if n == 0:
            return []
        return self.generate_trees_recursive(1, n)
    
    def generate_trees_recursive(self, start, end):
        result = []
        
        # 如果起始值大于结束值,返回一个包含 None 的列表
        if start > end:
            result.append(None)
            return result
        
        # 枚举每个数字作为根节点
        for i in range(start, end + 1):
            # 递归构建左子树
            left_trees = self.generate_trees_recursive(start, i - 1)
            
            # 递归构建右子树
            right_trees = self.generate_trees_recursive(i + 1, end)
            
            # 组合左子树和右子树
            for left in left_trees:
                for right in right_trees:
                    root = TreeNode(i)
                    root.left = left
                    root.right = right
                    result.append(root)
        
        return result
    
    # 方法二:动态规划 + 递归
    def generateTreesDP(self, n: int) -> List[TreeNode]:
        if n == 0:
            return []
        
        # 创建缓存
        memo = {}
        return self.generate_trees_dp(1, n, memo)
    
    def generate_trees_dp(self, start, end, memo):
        result = []
        
        # 如果起始值大于结束值,返回一个包含 None 的列表
        if start > end:
            result.append(None)
            return result
        
        # 检查缓存
        if (start, end) in memo:
            return memo[(start, end)]
        
        # 枚举每个数字作为根节点
        for i in range(start, end + 1):
            # 递归构建左子树
            left_trees = self.generate_trees_dp(start, i - 1, memo)
            
            # 递归构建右子树
            right_trees = self.generate_trees_dp(i + 1, end, memo)
            
            # 组合左子树和右子树
            for left in left_trees:
                for right in right_trees:
                    root = TreeNode(i)
                    root.left = left
                    root.right = right
                    result.append(root)
        
        # 更新缓存
        memo[(start, end)] = result
        return result

C++ 实现

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<TreeNode*> generateTrees(int n) {
        if (n == 0) {
            return vector<TreeNode*>();
        }
        return generateTreesRecursive(1, n);
    }
    
    vector<TreeNode*> generateTreesRecursive(int start, int end) {
        vector<TreeNode*> result;
        
        // 如果起始值大于结束值,返回一个包含 nullptr 的列表
        if (start > end) {
            result.push_back(nullptr);
            return result;
        }
        
        // 枚举每个数字作为根节点
        for (int i = start; i <= end; i++) {
            // 递归构建左子树
            vector<TreeNode*> leftTrees = generateTreesRecursive(start, i - 1);
            
            // 递归构建右子树
            vector<TreeNode*> rightTrees = generateTreesRecursive(i + 1, end);
            
            // 组合左子树和右子树
            for (TreeNode* left : leftTrees) {
                for (TreeNode* right : rightTrees) {
                    TreeNode* root = new TreeNode(i);
                    root->left = left;
                    root->right = right;
                    result.push_back(root);
                }
            }
        }
        
        return result;
    }
    
    // 方法二:动态规划 + 递归
    vector<TreeNode*> generateTreesDP(int n) {
        if (n == 0) {
            return vector<TreeNode*>();
        }
        
        // 创建缓存
        unordered_map<string, vector<TreeNode*>> memo;
        return generateTreesDP(1, n, memo);
    }
    
    vector<TreeNode*> generateTreesDP(int start, int end, unordered_map<string, vector<TreeNode*>>& memo) {
        vector<TreeNode*> result;
        
        // 如果起始值大于结束值,返回一个包含 nullptr 的列表
        if (start > end) {
            result.push_back(nullptr);
            return result;
        }
        
        // 检查缓存
        string key = to_string(start) + "_" + to_string(end);
        if (memo.find(key) != memo.end()) {
            return memo[key];
        }
        
        // 枚举每个数字作为根节点
        for (int i = start; i <= end; i++) {
            // 递归构建左子树
            vector<TreeNode*> leftTrees = generateTreesDP(start, i - 1, memo);
            
            // 递归构建右子树
            vector<TreeNode*> rightTrees = generateTreesDP(i + 1, end, memo);
            
            // 组合左子树和右子树
            for (TreeNode* left : leftTrees) {
                for (TreeNode* right : rightTrees) {
                    TreeNode* root = new TreeNode(i);
                    root->left = left;
                    root->right = right;
                    result.push_back(root);
                }
            }
        }
        
        // 更新缓存
        memo[key] = result;
        return result;
    }
};

执行结果

C# 执行结果

  • 执行用时:92 ms,击败了 93.33% 的 C# 提交
  • 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交

Python 执行结果

  • 执行用时:52 ms,击败了 95.24% 的 Python3 提交
  • 内存消耗:16.1 MB,击败了 92.86% 的 Python3 提交

C++ 执行结果

  • 执行用时:12 ms,击败了 100.00% 的 C++ 提交
  • 内存消耗:13.5 MB,击败了 94.74% 的 C++ 提交

代码亮点

  1. 递归实现清晰:递归函数的实现清晰明了,易于理解和维护。
  2. 动态规划优化:使用动态规划来优化递归过程,避免重复计算,提高效率。
  3. 边界条件处理:代码中详细处理了各种边界情况,如空树和单节点树。
  4. 组合树的方法:通过嵌套循环来组合所有可能的左子树和右子树,形成不同的二叉搜索树。
  5. 缓存键的设计:在 C++ 实现中,使用字符串作为缓存的键,简化了哈希表的使用。

常见错误分析

  1. 忽略空树 :在递归过程中,需要考虑空树的情况,即当 start > end 时,应该返回一个包含 null 的列表。
  2. 组合树错误:在组合左子树和右子树时,需要创建新的根节点,而不是修改原有的树结构。
  3. 缓存键冲突:在使用缓存时,需要确保缓存键的唯一性,避免冲突。
  4. 内存泄漏:在 C++ 实现中,需要注意内存管理,避免内存泄漏。
  5. 递归终止条件:在递归实现中,需要正确设置终止条件,避免无限递归。

解法比较

解法 时间复杂度 空间复杂度 优点 缺点
递归法 O(4^n / n^(3/2)) O(4^n / n^(3/2)) 实现简单,直观 存在重复计算,效率较低
动态规划 + 递归 O(4^n / n^(3/2)) O(4^n / n^(3/2)) 避免重复计算,效率高 实现稍复杂,需要额外的空间存储缓存

注意:时间复杂度和空间复杂度的表达式是基于卡特兰数的渐近表示,卡特兰数 C(n) 的渐近表示为 4^n / (n^(3/2) * sqrt(π))。

相关题目

相关推荐
测试19981 分钟前
postman测试文件上传接口详解
自动化测试·软件测试·python·测试工具·测试用例·接口测试·postman
阿巴~阿巴~3 分钟前
Day1 蓝桥杯省赛冲刺精炼刷题 —— 位运算与循环(2)
c语言·c++·算法·蓝桥杯
机器鱼8 分钟前
2-2 MATLAB鮣鱼优化算法ROA优化CNN超参数回归预测
算法·回归·cnn
百渡ovO11 分钟前
质数和约数
c++·算法
eqwaak013 分钟前
基于大语言模型的智能音乐创作系统——从推荐到生成
人工智能·爬虫·python·语言模型·自然语言处理·自动化
扫地的小何尚14 分钟前
NVIDIA cuOpt:GPU加速优化AI微服务详解
人工智能·算法·微服务·ai·架构·gpu
onejason17 分钟前
使用Python爬虫获取淘宝App商品详情
前端·python
微臣愚钝18 分钟前
【15】Selenium 爬取实战
爬虫·python·selenium
刘福蓝23 分钟前
bluecode-【米小游】20250329_3_小米游的数组询问
算法
船长@Quant29 分钟前
VectorBT:使用PyTorch+LSTM训练和回测股票模型 进阶三
pytorch·python·深度学习·lstm·量化策略·sklearn·量化回测