本节是对二叉搜索树(Binary Search Tree,BST)相关题目的汇总。
总结
- 二叉搜索树题目一般都可利用其
性质:- 中序遍历有序
- 搜索时可以确定方向,不用两条路都走
- 二叉搜索树类题目我们可以
将其看成是一个单调递增的数组,递增数组操作起来就会方便很多 - 基于中序遍历有序,中序遍历时有时需要
保存上一个节点,这样能辅助我们在中间节点的逻辑处理- 无论是递归还是迭代的中序遍历,都是在中间节点处理逻辑的最后更新上一个节点
二叉搜索树
二叉搜索树也是一棵二叉树,但是具有一些特点:
- 对于根节点,左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值。
- 任意节点的左、右子树也是二叉搜索树,即同样满足条件
1. - 中序遍历有序,基于前面的规则,二叉搜索树的中序遍历结果单调递增。
LeetCode-700.二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
利用二叉搜索树的特性,我们可以在每个分岔口(节点)确定路径
代码实现:这里给出递归和迭代的方法,注意这里的代码与遍历无关,因为我们每次都能知道目标值会出现在哪条路。
迭代
var searchBST = function (root, val) {
let res = root;
if (!res) return null;
while (res) {
if (res.val > val) {
res = res.left;
} else if (res.val < val) {
res = res.right;
} else {
break;
}
}
return res;
};
递归
function searchRecursive(node, val) {
if (!node) return null;
let res = null;
if (node.val > val) {
res = searchRecursive(node.left, val);
} else if (node.val < val) {
res = searchRecursive(node.right, val);
} else {
res = node;
}
return res;
}
var searchBST = function (root, val) {
return searchRecursive(root, val);
};
LeetCode-98.验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
本题我们就需要利用到二叉搜索树的一个关键特性了,中序遍历有序
我们可以在中序遍历过程中,判断当前值是否大于上一个值。
递归
递归分析:
- 返回值和入参:返回当前节点所代表的树是否是二叉搜索树(
boolean),入参:当前节点 - 终止条件:空节点 返回
true - 单层循环逻辑:
- 左:递归判断左子树是不是BST,一旦发现不是BST就可以中断递归
- 中:判断当前值是否小于上一个值
- 右:递归判断右子树是不是BST,一旦发现不是BST就可以中断递归
实现:重点关注如何获取上一个值
var isValidBST = function (root) {
let preNode = null;
//递归中序遍历+获取上一个值
function valitateBSTRecursive(node) {
if (!node) return true;
//左
const isValidLeft = valitateBSTRecursive(node.left);
//中断递归
if (!isValidLeft) return isValidLeft;
//中
if (preNode && preNode.val >= node.val) {
return false;
}
preNode = node;
//右
const isValidRight = valitateBSTRecursive(node.right);
//中断递归
if (!isValidRight) return isValidRight;
//到这里说明左子树和右子树都是BST,返回true
return true;
}
return valitateBSTRecursive(root);
};
迭代
我们要使用中序遍历的迭代代码,还记得吗?使用栈辅助+标记法。当然,重点还是在于如何获取上一个值。
var isValidBST = function (root) {
let preNode = null;
const stack = [];
stack.push(root);
while (stack.length) {
const node = stack.pop();
if (!node) {
const curNode = stack.pop();
//操作中间节点的位置,这里与preNode比较并更新preNode
if (preNode && preNode.val >= curNode.val) {
return false;
}
preNode = curNode;
continue;
}
//栈后序---入栈顺序为右中左
node.right && stack.push(node.right);
stack.push(node);
stack.push(null);
node.left && stack.push(node.left)
}
return true;
};
从本题中我们可以学到:
- 要合理利用二叉搜索树中序遍历单调递增的性质
- 在二叉搜索树遍历过程中,想要获得
上一个遍历的结点(preNode),我们需要在操作当前结点后更新preNode
LeetCode-530.二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
思路:简单分析一下,要求的是任意两个节点之间的最小差值,我们知道中序遍历是一个单调递增的数组,那么对于一个单调数组来说,最小差值一定存在于两个相邻节点之间,明白了这个,只需要中序遍历的时候保存上一个节点,然后计算差值去更新最小差值就行了。中序遍历和获得上一个节点的代码我们都会了,那这道题就很简单了。
这里只给出递归代码,因为递归更加考察大家的思维,并且代码也相对简短!
递归
递归分析:
- 外层变量:
minDifference最小差值,preNode上一个遍历处理的节点 - 返回值和入参:无返回值,入参为当前节点
- 终止条件:空节点
- 单层逻辑:
- 递归左子树
- 处理中间节点,计算最小差值并更新,更新
preNode - 递归右子树
实现:
var getMinimumDifference = function (root) {
let minDifference = Infinity;
let preNode = null;
function inorderRecursive(node) {
if (!node) return;
//左
inorderRecursive(node.left);
//中
if (preNode) {
const diff = Math.abs(preNode.val - node.val);
if (diff < minDifference) minDifference = diff;
}
preNode = node;
//右
inorderRecursive(node.right);
}
inorderRecursive(root);
return minDifference;
};
LeetCode-501.二叉搜索树中的众数
给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 [众数](即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。 这里规定的二叉搜索树,左孩子(右孩子)小于等于(大于等于)父节点;
思路:
- 思路一:我们可以使用中序遍历收集数组,此时相同的值都是连续出现的,那么再遍历一遍数组统计出现次数最多的值就很简单了
- 思路二:在中序遍历过程中统计出现最多次的元素,由于中序遍历的单调性,我们可以保存上一个节点,如果当前节点与上一个节点相同,那么次数加一,否则次数清零;
注意:树中不止有一个众数,因此我们需要考虑一下几种情况:
- 当前节点是否是新节点,是的话当前节点出现次数应该设为
1,否则+1` 当前节点出现次数 大于 最大出现次数的数,那么应该更新最大出现次数,并且清空已经收集的数再收集;当前节点出现次数 等于 最大出现次数的数,那么收集即可;
思路一
思路一很简单,就不多说了,给出参考代码:
var findMode = function (root) {
const sortedArr = [];
function inorder(node) {
if (!node) return;
inorder(node.left);
sortedArr.push(node.val);
inorder(node.right);
}
//收集中序遍历数组
inorder(root);
const res = [];
let maxCount = 0;
for (let i = 0; i < sortedArr.length;) {
let count = 0;
let curNum = sortedArr[i];
while (sortedArr[i] === curNum) {
count++;
i++;
}
//如果当前计数大于
if (count > maxCount) {
//清空res
res.length = 0;
res.push(curNum);
maxCount = count;
} else if (count === maxCount) {
res.push(curNum);
}
}
return res;
};
思路二
递归+保存preNode
var findMode = function (root) {
let maxCount = 0;
let res = [];
let preNode = null;
let count = 0;
function findRecursive(node) {
if (!node) return count;
//左
findRecursive(node.left);
//中
//是否是新节点
const isNew = preNode ? preNode.val !== node.val : true;
//新节点则重置计数器 否则计数器+1
if (isNew) {
count = 1;
} else {
count++;
}
if (count > maxCount) { //大于最大次数
res.length = 0;
res.push(node.val);
maxCount = count;
} else if (count === maxCount) { //等于最大次数
res.push(node.val);
}
preNode = node;
//右
findRecursive(node.right);
}
findRecursive(root);
return res;
};
LeetCode-236.二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
举例说明:
- 节点5和节点1 的最近公共祖先是 节点3
- 节点6和节点4 的最近公共祖先是 节点5
思路
实际上我们在举例时就已经有一些想法了:
- 找公共祖先就是
在某个节点的左子树和右子树上寻找目标节点,找到了说明这个节点是祖先,ok这里甚至可以确定后序遍历了,因为我们需要左右子树提供的信息 - 那如何找
最近的公共祖先呢?由于我们使用后序来解决这个问题,因此会出现以下两种情况:当前节点的左子树找到了目标节点,右子树也找到了目标节点,那说明当前节点就是最近的,你可以举例证明如果在一边找到了,一边没找到,那么说明目标节点都在找到的那一边。比如节点6和节点4,我们当前是在根节点3,左子树找到了,右子树没找到,此时公共祖先应该是左子树上的某个节点。
再来思考:返回值,我们每次返回找到了还是没找到可行吗?对于一边找到了一边没找到的情况,我们并不能确认谁是最近公共祖先,只能知道在哪边。那么我们返回的应该是当前这个节点。
- 假设寻找节点6和节点4的最近公共祖先,那么在节点2处,左子树没找到,右子树找到了,我们返回节点2到节点5,节点5左子树找到了,右子树也找到了,返回节点5到节点3,节点3左子树找到了,右子树没找到,返回节点5,递归结束。
- 假设寻找节点6和节点8的最近公共祖先,节点5处左子树找到了,右子树没找到,返回节点5到节点3;节点1处右子树找到了,返回节点1到节点3,此时节点3左子树找到了,右子树也找到了,返回节点3推出递归。
那么是不是可以确定了,我们每次递归的返回值应该是当前节点或空,找到了就是当前节点,没找到就是空;
结合代码再来模拟一下寻找的过程,你会对递归理解更透彻一些。需要明确一点,我们不能中断递归,这个遍历过程一定是执行到找到目标节点或空节点(终止条件)才开始退出递归的,并且我们需要利用返回值进行中间节点的逻辑处理。
实现:
function getAncestor(node, p, q) {
if (node == p || node === q || node === null) return node;
//左
const left = getAncestor(node.left, p, q);
//右
const right = getAncestor(node.right, p, q);
//中
if (left && right) return node;
if (!left && right) return right;
if (left && !right) return left;
//左边没找到,右边也没找到,返回空节点
return null;
}
var lowestCommonAncestor = function (root, p, q) {
return getAncestor(root, p, q);
};
LeetCode-235.二叉搜索树的最近公共祖先
为什么引入上一题,是因为本题的缘故,再上一题的基础上,我们能否利用二叉搜索树的特性来解决呢?
思路
二叉搜索树为我们提供了什么便利?
- 如果当前节点的值介于
p,q之间,那么当前节点一定就是最近公共祖先 - 如果小于
p,q最小值,那么往左边找,否则往右边
如下图:目标值为0和5,节点4也是介于之间,但是不是公共祖先,这不就与便利1矛盾了吗?是的,但是如果你从上向下看(先序遍历),那么就不会有问题了!
实现:
function getAncestor(node, p, q) {
if (!node) return;
//中
const max = Math.max(p.val, q.val);
const min = Math.min(p.val, q.val);
if (node.val >= min && node.val <= max) {
return node;
}
let res = null;
if (node.val > max) { //左
res = getAncestor(node.left, p, q);
} else { //右
res = getAncestor(node.right, p, q);
}
return res;
}
var lowestCommonAncestor = function (root, p, q) {
return getAncestor(root, p, q);
};