这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。
73. 验证二叉搜索树 (validate-binary-search-tree)
标签
- BST
- DFS
- 中等
题目
这里不贴题了,leetcode打开就行,题目大意:
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
基本思路
其实核心子问题就是,一个节点,左右节点是有范围的,在范围内就满足,否则就返回 false。每个节点相同处理。
-
先实现
dfs函数,参数(节点、该节点的下界和上界) -
对左右子树进行递归,如果
左右子树都满足条件,则返回true,如果有一个返回false,就返回false -
递归出口:
- 遍历到
null节点,返回true - 当前节点的值没有落在规定区间内,返回
false
- 遍历到
写法实现
var isValidBST = function (root) {
// 其实这就是个 dfs 深搜
const dfs = (root, minVal, maxVal) => {
// 递归出口
if (root === null) {
return true
}
// 不满足条件,不是 BST
if (root.val <= minVal || root.val >= maxVal) {
return false
}
// 分别递归判断左右子树
return dfs(root.left, minVal, root.val) && dfs(root.right, root.val, maxVal)
}
// 初始根节点没有限制
return dfs(root, -Infinity, Infinity)
};
74. 恢复二叉搜索树 (recover-binary-search-tree)
标签
- BST
- 困难
题目
这里不贴题了,leetcode打开就行,题目大意:
给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
基本思路
关键点是
- 只有2个节点是错误的,找到交换即可。
- BST 的 中序遍历 依次访问的节点值是递增的。
至于第二条如果不清楚为什么 请移步 这篇二叉树中序遍历和 BST 基本性质
先简单看下中序遍历框架吧
const midOrderTraversal = (node) => {
if(node !== null) {
// 先遍历左子树
midOrderTraversal(node.left) // ---------> (左)
// 然后访问根节点
resList.push(node.val) // ---------> (根)
// 再遍历右子树
midOrderTraversal(node.right) // ---------> (右)
}
}
那就剩下如何找到这两个错误点了,当然直接把中序遍历搞出来,然后对比也行,但可以有更好方式。
顺序错误有可能有2种
- 相邻的两个节点顺序错了,
- 不相邻的,不相邻的第一处肯定是前大后小,第二处是前小后大,所以想变回正确,第一对是
取前者而第二对是取后者然后交换。
只用比较前后访问的节点值,prev 保存上一个访问的节点,当前访问的是 root 节点。
每访问一个节点,如果prev.val >= root.val,就找到了一对“错误对”。
检查一下它是第一对错误对,还是第二对错误对。
遍历结束,就确定了待交换的两个错误点,进行交换。
注意最后交换这两个结点的时候,只是交换他们的值就可以了,而不是交换这两个结点相应的指针指向。
写法实现
const recoverTree = (root) => {
let [preNode, err1, err2] = [new TreeNode(-Infinity), null, null];
// 下面就是中序遍历
const midOrder = (root) => {
if (root !== null) {
midOrder(root.left);
// 访问根做操作,找到错误的 index
// 当前是第一对错误
if (preNode.val >= root.val && err1 === null) {
// 第一对错误点记录前一个元素
err1 = preNode;
}
if (preNode.val >= root.val && err1 !== null) {
// 第二对错误点记录后面一个元素(当然可能两对是一对)
err2 = root;
}
// 当前点用完,就更新前置节点
preNode = root;
midOrder(root.right);
}
};
midOrder(root);
// 错误交换就变回正确的顺序了
[err1.val, err2.val] = [err2.val, err1.val]
};
75. 不同的二叉搜索树 (unique-binary-search-trees)
标签
- 分治
- BST (binary-search-trees)
- 中等
题目
这里不贴题了,leetcode打开就行,题目大意:
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种。
输入:3
输出:5
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
相关知识
这题是上篇的简略版。不了解请移步二叉搜索树
基本思路
-
当 n = 0 时,没有数字,只能形成一种 BST :空树。
-
当 n = 1 时,只有一个数字,只能形成一种 BST :单个节点。
-
用 i 个节点构建 BST,除去根节点,剩
i−1个节点分别构建左、右子树,左子树分配 0 个,则右子树分配到 i−1 个,那么左子树分配 j 个,则右子树分配到i−1-j个。 -
那么 实际上 i 个节点 BST 数 就是
j从0遍历到i - 1的G(j) * G(i−1-j)的和G(i)表示用连续i个数,所构建出的 BST 所有种类数。
这题跟前面不同是不用构建树,算总数就行,更简单。也可以用动态规划找出动态方程解决。
写法实现
const numTrees = (n) => {
// 0 和 1能创造出的 BST 只能是 1种,递归出口
if (n == 0 || n == 1) {
return 1;
}
let num = 0;
for (let j = 0; j < n; j++) {
num += numTrees(j) * numTrees(n - 1 - j);
}
return num;
};
console.log(numTrees(3))
另外我们可以把每轮递归算出的答案缓存起来,提高效率。
const numTrees = (n) => {
const cache = new Map()
const divideAndConquer = (n) => {
if (n == 0 || n == 1) {
return 1;
}
// 如果存在,用缓存,直接 return
if (cache.has(n)) {
return cache.get(n);
}
let count = 0;
for (let j = 0; j < n; j++) {
count += divideAndConquer(j) * divideAndConquer(n - 1 - j);
}
// 缓存下本次结果,共下次使用
cache.set(n, count);
return count;
};
return divideAndConquer(n);
};
console.log(numTrees(3))
另外向大家着重推荐下这位大哥的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列
今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦
搜索我的微信号infinity_9368,可以聊天说地
加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我
presious tower shock the rever monster,我看到就通过,暗号对不上不加哈,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧