题目
解答
解法一:递归
递归直接超时。。
但是本题的最基本思路有了:
- 对于每一个结点都尝试作为根节点进行计算不同的左子树的可能数和右子树的可能数。
- 最后将所有的可能累加起来即可。
思路明了,剩下的就是优化的事了。
代码
class Solution {
public int numTrees(int n) {
return numTrees(1, n);
}
private int numTrees(int start, int end) {
if(start > end) {
return 1;
}
int res = 0;
for(int i = start; i <= end; i ++) {
int left = numTrees(start, i - 1);
int right = numTrees(i + 1, end);
res += left * right;
}
return res;
}
}
结果
备忘录优化的代码一
简单的对递归操作加一个备忘录记录已经计算的结果。
复杂度:O(n^2) 时间,O(n^2) 空间。
class Solution {
public int numTrees(int n) {
int[][] memo = new int[n + 1][n + 1];
return numTrees(1, n, memo);
}
private int numTrees(int start, int end, int[][] memo) {
if(start > end) return 1;
if(memo[start][end] != 0) return memo[start][end];
int res = 0;
for(int i = start; i <= end; i ++) {
int left = numTrees(start, i - 1, memo);
int right = numTrees(i + 1, end, memo);
res += left * right;
}
memo[start][end] = res;
return res;
}
}
结果
备忘录优化的代码二
可以预见:1 … 3 与 4 … 6 其实构造的树的结构是一样的,只不过值不同而已。
如果只计算数量的话就可以将他们统一化。将空间复杂度降为 O(n)。
复杂度:O(n^2) 时间,O(n) 空间。
class Solution {
public int numTrees(int n) {
int[] memo = new int[n + 1];
return numTrees(n, memo);
}
private int numTrees(int n, int[] memo) {
if(n == 0) return 1;
if(memo[n] != 0) return memo[n];
int res = 0;
for(int i = 1; i <= n; i ++) {
int left = numTrees(i - 1, memo);
int right = numTrees(n - i, memo);
res += left * right;
}
memo[n] = res;
return res;
}
}
结果
解法二:动态规划
将备忘录优化二的代码转换成自底向上的方式就是本题动态规划的解。
下述代码的解释:
- 第一层 for 循环确定组成二叉树结点的个数。
- 第二层 for 循环确定由哪一个结点作为根节点。
复杂度:O(n^2) 时间,O(n) 空间。
代码
class Solution {
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i ++) {
for(int j = 1; j <= i; j ++) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
结果
解法三:卡特兰数
分析一下本题:
- 假设由 n 个结点组成的不同的二叉搜索树数量为 h(n)。
- 那么现在再假设将第 k 个结点作为根节点,可以得到 f(k)=h(k-1)*h(n-k)。
- 而每个结点都可能会被作为根节点,也就是说 k 的取值范围是从 1 到 n 的。
- 所以可以推出公式:h(n) = f(1) + f(2) + … + f(n)。
- 进而满足了卡特兰数递推通式:h(n)= h(0)*h(n-1)+h(1)*h(n-2) + … + h(n-1)h(0) (n>=2)。
所以本题可以使用卡特兰数进行计算!
补充:卡特兰数其前几项为(从第 0 项开始): 1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, …
为了计算的方便,我们可以选用以下几个公式任意一个:
- C0 = 1,Cn+1 = Cn * (4n + 2) / (n + 2)
- Cn = (2n)! / ((n + 1)! * n !)
- Cn = C(2n,n) / (n+1) ,此处的 C(2n,n) 是组合数。
此题我选用了第一个公式。
复杂度:O(n) 时间,O(1) 空间。
代码
class Solution {
public int numTrees(int n) {
long C = 1;
for(int i = 0; i < n; i ++) {
C = C * (4 * i + 2) / (i + 2);
}
return (int)C;
}
}