本文针对BST 的基础操作:判断 BST 的合法性(98)、增(701)、删(450)、查(700)。以几道题来总结出套路模板,以一敌十!
98,验证二叉搜索树,medium
700,二叉搜索树中的搜索,easy
701,二叉搜索树中的插入操作,medium
450,删除二叉搜索树中的节点,medium
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
示例 2:
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
方法一:递归。
思路:很容易想到对于节点root,采用递归判断左子节点的值比它小,右子节点的值比它大,但注意:对每个节点都满足也不一定是BST树,如:
5
/ \
1 6
/ \
4 7
没有满足右子树的节点都比root值大。
建立辅助函数,增加最小节点min和最大节点max 作为辅助函数的参量。对于root,比较root.val
与当前的min.val
和 max.val
,再对root.left
和root.right
进行递归操作。
代码:
class Solution {
public boolean isValidBST(TreeNode root) {
return healper(root, null, null);
}
//是BST树必须满足 min.val < root.val < max.val
public boolean healper(TreeNode root, TreeNode min, TreeNode max){
if(root == null) return true;
if(min != null && min.val >= root.val) return false;
if(max != null && max.val <= root.val) return false;
//左子树范围的最小值是min.val,最大值是当前节点的值,也就是root的值,因为左子树的值要比当前节点小
//右子数范围的最大值是max.val,最小值是当前节点的值,也就是root的值,因为右子树的值要比当前节点大
return healper(root.left, min, root) && healper(root.right, root, max);
}
}
方法二:中序遍历。
思路:中序遍历的结果是从小到大排序的,建立一个全局遍历pre
代表前一个节点,当前节点与前一个节点进行比较。
递归:
class Solution {
//中序遍历,代表前一个节点
TreeNode pre;
//中序遍历,递归
public boolean isValidBST(TreeNode root) {
if(root == null) return true;
// 访问左子树
if(!isValidBST(root.left)) return false;
// 访问当前节点,如果不是按照从小到大的顺序,则返回false;否则pre右移,继续比较
if(pre != null && pre.val >= root.val) return false;
pre = root;
// 访问右子树
if(!isValidBST(root.right)) return false;
return true;
}
}
迭代:
class Solution {
//中序遍历,代表前一个节点
TreeNode pre;
public boolean isValidBST(TreeNode root) {
if(root == null) return true;
Stack<TreeNode> stack = new Stack<>();
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
root = stack.pop();
if(pre != null && pre.val >= root.val) return false;
pre = root;
root = root.right;
}
return true;
}
}
易错点:采用递归方法只与左右子节点比较,但没有保证整棵树是BST树。
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
例如,
给定二叉搜索树:
4
/ \
2 7
/ \
1 3
和值: 2
你应该返回如下子树:
2
/ \
1 3
在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。
思路:对于普通二叉树,判断是否存在值为 val
的节点
public boolean search(TreeNode root, int val) {
if(root == null) return false;
if(root.val == val) return true;
return search(root.left, val) || search(root.right, val);
}
对于BST树,利用其左小右大的规律,不需要递归地搜索两边,类似二分查找思想,根据val
和root.val
的大小比较,就能排除一边。
代码:
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if(root == null) return false;
if(root.val == val) return true;
if(root.val < val) return searchBST(root.right, val);
if(root.val > val) return searchBST(root.left, val);
}
}
简化:
public TreeNode searchBST(TreeNode root, int val) {
if(root == null || root.val == val) return root;
return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}
总结:BST树遍历搜索的框架为
void BST(TreeNode root, int target) {
if (root.val == target){
// 找到目标节点,进行具体操作
}
//目标值target比当前节点值大,在右子树中找
if (root.val < target)
BST(root.right, target);
//目标值target比当前节点值小,在左子树中找
if (root.val > target)
BST(root.left, target);
}
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例 1:
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:
示例 2:
输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]
示例 3:
输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]
思路:
对于函数TreeNode insertIntoBST(TreeNode root, int val)
,表示插入值为 val
的节点后的BST树。由上面总结的查找的模板,再加入修改的操作即可。一旦涉及「改」,函数就要返回TreeNode
类型,并且对递归调用的返回值进行接收。
class Solution {
//查找 + 修改
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null) return new TreeNode(val);
//val值比当前节点值大,将找个值插入右子树,且赋给root.right
if(root.val < val) root.right = insertIntoBST(root.right, val);
if(root.val > val) root.left = insertIntoBST(root.left, val);
return root;
}
}
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h),h 为树的高度。
示例:
root = [5,3,6,2,4,null,7]
key = 3
5
/ \
3 6
/ \ \
2 4 7
给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
5
/ \
4 6
/ \
2 7
另一个正确答案是 [5,2,6,null,4,null,7]。
5
/ \
2 6
\ \
4 7
思路:还是查找 + 修改 类型,按照上面的遍历框架,写出初步解答:
TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return null;
// 找到啦,进行删除
if (root.val == key) {
//一些删除操作
}
// key在左子树,对左子树进行删除操作并重新赋给root.left
else if (root.val > key) {
root.left = deleteNode(root.left, key);
}
// key在右子树,对右子树进行删除操作并重新赋给root.right
else{
root.right = deleteNode(root.right, key);
}
return root;
}
找到要删除的节点后,删除操作有下面情况:
左右子节点有为空的(如12),返回非空的子节点
(deleteNode
函数即返回删除值为key的节点后的树)
如果左右子节点都为空,则返回null
if(root.left == null) return root.right;
if(root.right == null) return root.left;
左右子节点都不为空,就要找到左子树的最大值或者右子树的最小值替换此root,并在左子树(或右子树)删除此节点。
root.val = getMaxLeft(root.left).val;
root.left = deleteNode(root.left, root.val);
代码:
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return null;
//找到了要删除的节点
if(root.val == key){
//1.有子节点为空
if(root.left == null) return root.right;
if(root.right == null) return root.left;
//2.子节点非空,则由左子树的最大值或者右子树的最小值替换此root,并在左子树(或右子树)删除此节点
root.val = getMaxLeft(root.left).val;
root.left = deleteNode(root.left, root.val);
}
//key在右子树,对右子树进行删除操作,并把新的子树重新赋给root.right
else if(root.val < key){
root.right = deleteNode(root.right, key);
}
else
root.left = deleteNode(root.left, key);
return root;
}
//从root.left出发,找到root的左子树的最大值(向右找)
public TreeNode getMaxLeft(TreeNode node){
if(node == null) return null;
while(node.right != null){
node = node.right;
}
return node;
}
}
由BST树的搜索框架,可以实现增(插)删改查,这几项的操作都是搜+改,做题可以先把框架写出来,再分析每步干什么。
void BST(TreeNode root, int target) {
if (root.val == target){
// 找到目标节点,进行具体操作
}
//目标值target比当前节点值大,在右子树中找
if (root.val < target)
BST(root.right, target);
//目标值target比当前节点值小,在左子树中找
if (root.val > target)
BST(root.left, target);
}
对于BST树最大的特点就是左小右大,中序遍历的结果由小到大。
递归最重要的就是弄清函数的含义,如果有返回值代表什么。