返回

算法深入浅出:LeetCode #96 不同的二叉搜索树

IOS

作为算法学习的必经之路,LeetCode 高频题始终备受关注,其中 #96 不同的二叉搜索树更是频频出现在面试中。本文将从递归到动态规划,深入浅出地解析这一经典问题,带你领略算法的魅力。

问题

给定一个整数 n,返回所有由 [1, n] 构成的不同的二叉搜索树的个数。

例如,当 n = 3 时,所有不同的二叉搜索树为:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 /
   2     1         2             1

因此,n = 3 时共有 5 种不同的二叉搜索树。

递归法

一种求解此问题的常见方法是递归。我们知道,对于一个包含 n 个结点的二叉搜索树,其根结点可以取 [1, n] 中的任意一个值。对于每个根结点,其左子树包含根结点左边的所有结点,而其右子树包含根结点右边的所有结点。

基于此,我们可以递归地构造所有的二叉搜索树。对于根结点为 i 的二叉搜索树,其左子树包含 i - 1 个结点,而其右子树包含 n - i 个结点。我们分别递归地构造左子树和右子树,并将其组合起来,即可得到以 i 为根结点的二叉搜索树。

def numTrees(n):
  if n == 0:
    return 1

  total = 0
  for i in range(1, n + 1):
    left_trees = numTrees(i - 1)
    right_trees = numTrees(n - i)
    total += left_trees * right_trees

  return total

动态规划法

除了递归法之外,我们还可以使用动态规划来解决此问题。动态规划是一种自底向上的方法,它通过将问题分解成一系列子问题,并存储子问题的解,从而避免重复计算。

对于此问题,我们可以定义一个二维数组 dp,其中 dp[i][j] 表示由 [i, j] 构成的二叉搜索树的个数。显然,当 i == j 时,dp[i][j] = 1,因为只有一个结点构成的二叉搜索树只有一个。

对于 i < j 的情况,我们可以遍历所有可能的根结点 k。对于根结点为 k 的二叉搜索树,其左子树包含 k - 1 个结点,而其右子树包含 j - k 个结点。我们已经知道了由 [i, k - 1] 构成的二叉搜索树的个数 dp[i][k - 1],以及由 [k + 1, j] 构成的二叉搜索树的个数 dp[k + 1][j]。因此,我们可以计算出以 k 为根结点的二叉搜索树的个数 dp[i][j] = dp[i][k - 1] * dp[k + 1][j]

def numTrees(n):
  dp = [[0 for _ in range(n + 1)] for _ in range(n + 1)]

  for i in range(1, n + 1):
    dp[i][i] = 1

  for i in range(2, n + 1):
    for j in range(1, i + 1):
      for k in range(j, i + 1):
        dp[j][i] += dp[j][k - 1] * dp[k + 1][i]

  return dp[1][n]

总结

通过递归和动态规划这两种方法,我们可以高效地计算出由 [1, n] 构成的不同的二叉搜索树的个数。在实际面试中,这道题的考察重点在于理解二叉搜索树的性质以及掌握动态规划的思想。希望本文的解析能够帮助你更深入地理解此算法问题。