#LeetCode每日一题【二叉树专题】
- 不同的二叉搜索树
https://leetcode-cn.com/problems/unique-binary-search-trees/ - 分析
二叉搜索树,根节点的值大于左节点小于右节点,且他的每一颗子树都是二叉搜索树。
所以确定根节点i之后,那左子树的值必然是是[1,i)之间,右子树的值必然是在(i,n]之间。
即能切分成子问题:左子树的根节点值j在[1,i)中,其左子树的值必然是在[i,j),其右子树的值必然是在(j,i)…以此类推。
即是一个递归问题,不断的寻找左子树和右子树的数量,最终结果是左子树的数目*右子树的数目;
- 递归结束条件,区间start大于end;
- 递归返回值:左子树有多种可能,右子树也有多种可能,记录可能的数目;
- 最小问题,本级递归应该做:左子树的数目*右子树的数目
- 代码实现
public int numTrees(int n) {
return numTrees(1, n);
}
private int numTrees(int start, int end) {
if (start > end) {
return 1;
}
int sum = 0;
for (int i = start; i <= end; i++) {
// 左子树的数目
int left = numTrees(start, i - 1);
// 右子树的数目
int right = numTrees(i + 1, end);
// i作为根节点,拼上左右子树之后,可能的数量
sum += left * right;
}
return sum;
}
因为这里面的递归存在大量的重复计算,所以当n=18的时候,LeetCode显示超时
如当n=18计算i=10的时候左子树需要计算1-9区间的值,再计算i=11的时候需要1-10区间的值其中包含了1-9,即有重复计算了一次。那么我们可以使用记忆化搜索的方式将1-9区间的值记录下来,再下次递归的时候进行判断,如果记忆中有则直接取,若没有则放入记忆中。
- 优化——递归+记忆搜索法
public int numTrees(int n) {
int[][] memory = new int[n + 2][n + 2];
return numTreesMemory(1, n, memory);
}
// 使用记忆搜索优化
// 如计算10的左右子树的数量的时候,左子树在1-9之间组合;当计算11的时候,左子树在1-10之间组合,必然包含了1-9等,即存在大量的重复计算。
// 用一个二维数组存储start、end的计算结果,当准备递归调用之前先判断是否在记忆存储,若有则直接取,若无则放入
private int numTreesMemory(int start, int end, int[][] memory) {
if (start > end) {
// 返回1不是0,表示该子树为null,也是一种可能
return 1;
}
int sum = 0;
for (int i = start; i <= end; i++) {
// 左子树的数目
int left = memory[start][i - 1] > 0 ? memory[start][i - 1] : numTreesMemory(start, i - 1, memory);
// 右子树的数目
int right = memory[i + 1][end] > 0 ? memory[i + 1][end] : numTreesMemory(i + 1, end, memory);
// i作为根节点,拼上左右子树之后,可能的数量
sum += left * right;
memory[start][i - 1] = left;
memory[i + 1][end] = right;
}
return sum;
}
在原来的代码基础上进行的记忆优化,LeetCode耗时:0ms
-
动态规划
看了评论和题解之后,说是一个典型的动态规划类型的题;动态规划关键于寻找递推公式:
dp(0)=1 dp(1)=dp(0)*dp(0)=1 dp(2)=dp(0)*dp(1)+dp(1)*dp(0)=2 dp(3)=dp(0)*dp(2)+dp(1)*dp(1)+dp(2)*dp(0)=5 ... dp(n)=dp(0)*dp(n-1)+dp(1)*dp(n-2)+...+dp(n-1)*dp(0) 即 n dp(n)= ∑ dp(i-1)*dp(n-i) i=1
dp(n)表示:从1-n个数,能组成的二叉搜索树的数量
其实该递推公式寻找的过程也是上面递归算法体现的逻辑,从1-n分别作为根,在[1,i)之间寻找左子树的数量(有i-1个节点),在(i,n]之间寻找右子树的数量(有n-i个节点),最后累计求和
具体实现:// 使用动态规划,递推公式:dp(n)= 求和(i=1~n) dp(i-1)*dp(n-i) public int numTreesDP(int n) { // 初始化dp int[] dp = new int[n + 1]; dp[0] = 1; // 根据递推公式 for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { // 计算每一步的结果,i =》 1~n dp[i] += dp[j - 1] * dp[i - j]; } } return dp[n]; }
LeetCode耗时:0ms
- 总结
该题通过最开始的递归——递归+记忆搜索优化——动态规划,算法思想层层递进,最重要的是如何分析问题,想到使用递归,在优化递归,再寻找递推公式使用动态规划。
- 总结