本文参考labuladong算法笔记[二叉搜索树心法(基操篇) | labuladong 的算法笔记]
1、概述
我们前文 东哥带你刷二叉搜索树(特性篇) 介绍了 BST 的基本特性,还利用二叉搜索树「中序遍历有序」的特性来解决了几道题目,本文来实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中「删」和「判断合法性」略微复杂。
BST 的基础操作主要依赖「左小右大」的特性,可以在二叉树中做类似二分搜索的操作,寻找一个元素的效率很高。
对于 BST 相关的问题,你可能会经常看到类似下面这样的代码逻辑:
def BST(root: TreeNode, target: int) -> None:
if root.val == target:
# 找到目标,做点什么
if root.val < target:
BST(root.right, target)
if root.val > target:
BST(root.left, target)
这个代码框架其实和二叉树的遍历框架差不多,无非就是利用了 BST 左小右大的特性而已。接下来看下 BST 这种结构的基础操作是如何实现的。
2、判断 BST 的合法性
力扣98 .「验证二叉搜索树」
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3] 输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6] 输出:false 解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
- 树中节点数目范围在
[1, 104]
内 -231 <= Node.val <= 231 - 1
【思路】
注意,这里是有坑的哦。按照 BST 左小右大的特性,每个节点想要判断自己是否是合法的 BST 节点,要做的事不就是比较自己和左右孩子吗?感觉应该这样写代码:
def isValidBST(root: TreeNode) -> bool:
if root is None:
return True
# root 的左边应该更小
if root.left is not None and root.left.val >= root.val:
return False
# root 的右边应该更大
if root.right is not None and root.right.val <= root.val:
return False
return isValidBST(root.left) and isValidBST(root.right)
但是这个算法出现了错误,BST 的每个节点应该要小于右边子树的所有节点,下面这个二叉树显然不是 BST,因为节点 10 的右子树中有一个节点 6,但是我们的算法会把它判定为合法 BST:
错误的原因在于,对于每一个节点 root
,代码值检查了它的左右孩子节点是否符合左小右大的原则;但是根据 BST 的定义,root
的整个左子树都要小于 root.val
,整个右子树都要大于 root.val
。
问题是,对于某一个节点 root
,他只能管得了自己的左右子节点,怎么把 root
的约束传递给左右子树呢?请看正确的代码:
【python】
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
return self._isValidBST(root, None, None)
# 定义:该函数返回 root 为根的子树的所有节点是否满足 max.val > root.val > min.val
def _isValidBST(self, root: TreeNode, min: TreeNode, max: TreeNode) -> bool:
# base case
if root is None:
return True
# 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if min is not None and root.val <= min.val:
return False
if max is not None and root.val >= max.val:
return False
# 根据定义,限定左子树的最大值是 root.val,右子树的最小值是 root.val
return self._isValidBST(root.left, min, root) and self._isValidBST(root.right, root, max)
3、在 BST 中搜索元素
700 .「二叉搜索树中的搜索」
给定二叉搜索树(BST)的根节点 root
和一个整数值 val
。
你需要在 BST 中找到节点值等于 val
的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null
。
示例 1:
输入:root = [4,2,7,1,3], val = 2 输出:[2,1,3]
示例 2:
输入:root = [4,2,7,1,3], val = 5 输出:[]
提示:
- 树中节点数在
[1, 5000]
范围内 1 <= Node.val <= 107
root
是二叉搜索树1 <= val <= 107
如果是在一棵普通的二叉树中寻找,可以这样写代码:
def searchBST(root, target):
if not root:
return None
if root.val == target:
return root
# 当前节点没找到就递归地去左右子树寻找
left = searchBST(root.left, target)
right = searchBST(root.right, target)
return left if left else right
这样写完全正确,但这段代码相当于穷举了所有节点,适用于所有二叉树。那么应该如何充分利用 BST 的特殊性,把「左小右大」的特性用上?
很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 target
和 root.val
的大小比较,就能排除一边。我们把上面的思路稍稍改动:
def searchBST(root: TreeNode, target: int) -> TreeNode:
# 如果二叉树为空,直接返回
if not root:
return None
# 去左子树搜索
if root.val > target:
return searchBST(root.left, target)
# 去右子树搜索
if root.val < target:
return searchBST(root.right, target)
# 当前节点就是目标值
return root
4、在 BST 中插入一个数
对数据结构的操作无非遍历 + 访问,遍历就是「找」,访问就是「改」。具体到这个问题,插入一个数,就是先找到插入位置,然后进行插入操作。
因为 BST 一般不会存在值重复的节点,所以我们一般不会在 BST 中插入已存在的值。下面的代码都默认不会向 BST 中插入已存在的值。
上一个问题,我们总结了 BST 中的遍历框架,就是「找」的问题。直接套框架,加上「改」的操作即可。
一旦涉及「改」,就类似二叉树的构造问题,函数要返回 TreeNode
类型,并且要对递归调用的返回值进行接收。
701. 「二叉搜索树中的插入操作」
给定二叉搜索树(BST)的根节点 root
和要插入树中的值 value
,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例 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]
提示:
- 树中的节点数将在
[0, 104]
的范围内。 -108 <= Node.val <= 108
- 所有值
Node.val
是 独一无二 的。 -108 <= val <= 108
- 保证
val
在原始BST中不存在。
【python】
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
if not root:
# 找到空位置插入新节点
return TreeNode(val)
# 去右子树找插入位置
if root.val < val:
root.right = self.insertIntoBST(root.right, val)
# 去左子树找插入位置
if root.val > val:
root.left = self.insertIntoBST(root.left, val)
# 返回 root,上层递归会接收返回值作为子节点
return root
5、在 BST 中删除一个数
450. 删除二叉搜索树中的节点 | 力扣
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
示例 1:
输入:root = [5,3,6,2,4,null,7], key = 3 输出:[5,4,6,2,null,null,7] 解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。 另一个正确答案是 [5,2,6,null,4,null,7]。
示例 2:
输入: root = [5,3,6,2,4,null,7], key = 0 输出: [5,3,6,2,4,null,7] 解释: 二叉树不包含值为 0 的节点
示例 3:
输入: root = [], key = 0 输出: []
提示:
- 节点数的范围
[0, 104]
. -105 <= Node.val <= 105
- 节点值唯一
root
是合法的二叉搜索树-105 <= key <= 105
进阶: 要求算法时间复杂度为 O(h),h 为树的高度。
【思路】
这个问题稍微复杂,跟插入操作类似,先「找」再「改」,先把框架写出来再说:
def deleteNode(root: TreeNode, key: int) -> TreeNode:
if root.val == key:
# 找到啦,进行删除
elif root.val > key:
# 去左子树找
root.left = deleteNode(root.left, key)
elif root.val < key:
# 去右子树找
root.right = deleteNode(root.right, key)
return root
找到目标节点了,比方说是节点 A
,如何删除这个节点,这是难点。因为删除节点的同时不能破坏 BST 的性质。有三种情况,用图片来说明。
情况 1:A
恰好是末端节点,两个子节点都为空,那么它可以当场去世了。
if (root.left == null && root.right == null)
return null;
情况 2:A
只有一个非空子节点,那么它要让这个孩子接替自己的位置。
// 排除了情况 1 之后
if (root.left == null) return root.right;
if (root.right == null) return root.left;
情况 3:A
有两个子节点,麻烦了,为了不破坏 BST 的性质,A
必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。
if (root.left != null && root.right != null) {
// 找到右子树的最小节点
TreeNode minNode = getMin(root.right);
// 把 root 改成 minNode
root.val = minNode.val;
// 转而去删除 minNode
root.right = deleteNode(root.right, minNode.val);
}
三种情况分析完毕,填入框架,简化一下代码:
class Solution:
'''
思路:
1、考虑每个节点应该做什么,应该用递归思路求解
2、对于单个节点,分为节点值 大于/小于/等于 key三种情况讨论
3、节点值大于或小于key时,直接递归调用 deleteNode 函数,重塑该节点
4、节点值等于key时,考虑被删节点处的三种结构,无子节点、有一个子节点、有两个子节点
5、若有两个子节点,可找到左子树最大节点或右子树最小节点将其删除,并对node做替换
6、min_node.left = root.left, min_node.right = root.right
'''
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
if root == None:
return None
if root.val == key:
# 这两个 if 把情况 1 和 2 都正确处理了
if root.left == None:
return root.right
if root.right == None:
return root.left
# 处理情况 3
# 获得右子树最小的节点
minNode = self.getMin(root.right)
# 删除右子树最小的节点
root.right = self.deleteNode(root.right, minNode.val)
# 用右子树最小的节点替换 root 节点
minNode.left = root.left
minNode.right = root.right
root = minNode
elif root.val > key:
root.left = self.deleteNode(root.left, key)
elif root.val < key:
root.right = self.deleteNode(root.right, key)
return root
def getMin(self, node: TreeNode) -> TreeNode:
# BST 最左边的就是最小的
while node.left != None:
node = node.left
return node
6、总结
1、如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。
2、掌握 BST 的增删查改方法。
3、递归修改数据结构时,需要对递归调用的返回值进行接收,并返回修改后的节点。