LeetCode 96,n个数构建BST的方法有多少种?
今天是LeetCode专题第62篇文章,我们一起来聊聊LeetCode的96题,Unique Binary Search Trees(唯一的二叉搜索树)。
这道题的官方难度是Medium,点赞3576,反对132,从这个点赞比上来看,这道题的质量还是很高的。也可以算得上是特别好评。通过率52.8%,从通过率上来看在Medium当中算是偏高的,实际上也的确如此,这道题没有什么诡计,也没有很刁钻的内容,考验的完全是我们对于数据结构最基本的理解。
题意
和上题的内容很相似,如果有没有看过上题的同学可以点击传送门回顾一下:
LeetCode 94 | 构造出所有二叉搜索树
给定一个整数n,代表1-n这n个整数。要求用这n个数生成二叉搜索树(BST)。请问可以构成多少种结构不同的BST?
样例
Input: 3
Output: 5
Explanation:
Given n = 3, there are a total of 5 unique BST's:
1 3 3 2 1
/ / /
3 2 1 1 3 2
/ /
2 1 2 3
题解
某种程度上来说这是上一题的简化版,我们当然可以复用上一题的代码,但这没有任何意义。如果只是复用上一题的代码是无法通过这题了,原因也很简单,因为n的范围变大了。
在上一题当中我们用递归的思路求解出了所有构建树的可能性,在这一题当中我们没必要完整地构建树了,只需要计算种类就可以了。我们虽然不能直接复用上一题的代码,但是思路却是可以借鉴的,我们一样可以采用分治和递归的思路。
在分治法当中我们把一个比较复杂的大问题转化成若干个相对容易解决的小问题,比如著名的归并排序就是利用的这个思路。而递归做的是什么事情呢,是函数自己调用自己,解决一个本质相同只是规模更小的问题。这两者并不是等价的,分治法并不一定需要递归,递归也不一定就是用来实现分治法。准确得说分治法是一种算法,而递归是一种解决问题的思想是算法的实现方式。这两者高度相关但是并不相同,这道题是这两者一个巧妙的结合。
我们回到问题本身,我们假设有一个函数f,可以求出包含n个大小不同的数的BST的数量。那么显然f(n)就是答案。所以我们要做的就是想办法得到这个f。但是也很明显,这个f是很难或者是没法直接得到的。针对这种情况我们需要对算法找一个开头,再构建出一种嵌套方法。这一点有些类似于数学归纳法,我们先证明n=1时成立,再证明如果n=k时成立可以证明得到n=k+1也成立,那么我们就可以循环嵌套得到所有的证明。
递归也是一样,我们先得到n=1时的解,再通过n=k得到n=k+1,那么我们就可以递归得到所有的解。所以问题的核心就是怎么构建出这个循环嵌套的过程。这一点需要我们对问题进行深入分析。
对于有n个元素的BST来说,它的根节点的组成有n种可能。假设根节点是i,那么我们可以得到它的左子树包含1-i-1这i-1个元素,右子树包含i+1到n这n-i个元素。那么以i为根节点的BST的数量就是,而根节点一共有n种可能,所以。
那么很明显,代码呼之欲出。
class Solution:
def numTrees(self, n: int) -> int:
def dfs(n):
if n <= 1:
return 1
ret = 0
for i in range(1, n+1):
ret += dfs(i-1) * dfs(n-i)
return ret
return dfs(n)
是不是很简单呢?但是很遗憾这段代码是无法通过的,原因也很简单,因为递归浪费了很多时间。我们在计算dfs(n)的时候,它将dfs(0)到dfs(n-1)都计算了一遍。而我们计算dfs(n+1)的时候,这些值又会重复再计算一遍。这样层层累加之下,会有很多的值是重复计算的,这会带来大量的消耗,是毫无意义的。
我们可以做一个很简单的优化,就是将所有dfs的值都存储起来。如果我们要求的dfs(n)之前已经计算过了,那么直接返回不在重复计算了。
class Solution:
def numTrees(self, n: int) -> int:
record = {}
record[0] = 1
record[1] = 1
def dfs(n):
if n in record:
return record[n]
ret = 0
for i in range(1, n+1):
ret += dfs(i-1) * dfs(n-i)
return ret
return dfs(n)
这种很简单的优化方法叫做记忆化搜索,说白了就是将可能会出现重复计算的中间值存储起来防止重复计算,从而优化运行速度。
如果我们换一种写法写成递推的形式,那么就成了动态规划了。所以某种程度上来说,动态规划和记忆化搜索是同一种算法,只是表现形式不同。所以很多课本上先从记忆化搜索开始介绍动态规划,就是这么个道理。
class Solution:
def numTrees(self, n: int) -> int:
dp = [0 for _ in range(n+2)]
dp[0] = 1
dp[1] = 1
for i in range(2, n+1):
for j in range(1, i+1):
dp[i] += dp[j-1] * dp[i-j]
return dp[n]
- END -
- Linux下修改系统编码的操作记录
- 微信公众平
- linq to xml复习
- web cache server方案比较:varnish、squid、nginx
- Nginx虚拟目录alias和root目录
- Nginx的https配置记录以及http强制跳转到https的方法梳理
- VPC下访问FTP的问题
- 分析车辆雨刮频次计算降雨量 大数据服务天气预报
- 干货:这里有一份小程序接入微信支付避雷指南
- Mysql占用过高CPU时的优化手段
- 服务器端加入自动运行的JS代码
- 数据中心发电机组的调试与验收要求
- mysql连接数设置操作(Too many connections)
- 做好容错才能确保服务器的不间断运行
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 剑指offer(51-60)题解
- mybatis逆向工程
- ssm之spring+springmvc+mybatis整合初探
- mybatis插件开发小例子
- java之如何在eclipse中新建对象时自动补全
- mybatis文件映射之当输入的参数不只一个时
- mybatis插件开发初探
- 剑指offer(25-30)题解
- 如何实时迁移MySQL到TcaplusDB
- 如何利用Terraform工具编排管理TcaplusDB
- 如何实时迁移AWS DynamoDB到TcaplusDB
- 腾讯云TcaplusDB基础能力介绍
- 游戏架构上云实战
- 【JUC】CyclicBarrier的了解和使用
- 完美解决-RuntimeError: CUDA error: device-side assert triggered