Task04 二叉搜索树
4.1 二叉搜索树简介
- 二叉搜索树(Binary Search Tree):
也叫做二叉查找树、有序二叉树或者排序二叉树。是指一棵空树或者具有下列性质的二叉树:
如果任意节点的左子树不为空,则左子树上所有节点的值均小于它的根节点的值。
如果任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值。
任意节点的左子树、右子树均为二叉搜索树。
二叉树的特性:左子树的节点值<根节点值<右子树的节点值
如果以中序遍历的方式遍历整个二叉搜索树时,会得到一个递增序列。
二叉搜索树为了搜索(查找)而生,还可以提高【插入数据】和【删除数据】的速度。
二叉搜索树的操作主要有 4 种:查找,插入,创建,删除。
1.二叉搜索树的查找
在二叉搜索树中查找值为 val 的节点。
二叉树的递归查找步骤如下:
1.如果二叉搜索树为空,则查找失败,结束查找,并返回空指针节点 None。
2.如果二叉搜索树不为空,则将要查找的值 val 与二叉搜索树根节点的值 root.val 进行比较:
1.如果 val == root.val,则查找成功,结束查找,返回被查找到的节点。
2.如果 val < root.val,则递归查找左子树。
3.如果 val > root.val,则递归查找右子树。
from typing import List
from typing import Optional
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
if not root: #查找不成功
return None
if val == root.val: #查找成功
return root
elif val < root.val: #查找左子树
return self.searchBST(root.left, val)
else: #查找右子树
return self.searchBST(root.right, val)
- 二叉搜索树的查找算法分析
二叉搜索树的查找时间复杂度和树的形态有关。
最好情况下,二叉搜索树的形态与二分查找的判定树相似,查找的时间复杂度为 O(log2n)。
在最坏情况下,二叉搜索树的形态为单支树,即只有左子树或者只有右子树,时间复杂度为O(n)。
在平均情况下,二叉搜索树的查找平均时间复杂度为 O(log2n)。
2. 二叉搜索树的插入
在二叉搜索树中插入一个值为 val 的节点(假设当前二叉搜索树中不存在值为 val 的节点)。
对于新插入的节点来说,这个“位置”一般都是在叶子节点上。
具体步骤如下:
1.如果二叉搜索树为空,则创建一个值为 val 的节点,并将其作为二叉搜索树的根节点。
2.如果二叉搜索树不为空,则将待插入的值 val 与二叉搜索树根节点的值 root.val 进行比较:
1.如果 val < root.val,则递归将值为 val 的节点插入到左子树中。
2.如果 val > root.val,则递归将值为 val 的节点插入到右子树中。
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
if root == None: #如果为空,创建一个新的节点
return TreeNode(val)
if val < root.val: #插入左子树
root.left = self.insertIntoBST(root.left, val)
if val > root.val: #插入右子树
root.right = self.insertIntoBST(root.right, val)
return root
3. 二叉搜索树的创建
根据数组序列中的元素值,建立一棵二叉搜索树。
二叉搜索树的创建操作是从空树开始,按照给定数组元素的值,依次进行二叉搜索树的插入操作,最终得到一棵二叉搜索树。具体算法步骤如下:
1.初始化二叉搜索树为空树。
2.遍历数组元素,将数组元素值 nums[i] 依次插入到二叉搜索树中。
3.将数组中全部元素值插入到二叉搜索树中之后,返回二叉搜索树的根节点。
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
if root == None:
return TreeNode(val)
if val < root.val:
root.left = self.insertIntoBST(root.left, val)
if val > root.val:
root.right = self.insertIntoBST(root.right, val)
return root
def buildBST(self, nums) -> TreeNode:
root = TreeNode(val) #初始化二叉搜索树
for num in nums:
self.insertIntoBST(root, num)
return root
4. 二叉搜索树的删除
在二叉搜索树中删除值为 val 的节点。难点在删除节点之后仍然要保持剩下的二叉树仍是二叉搜索树,即依然满足二叉搜索树的特性。
如果删除的是叶子节点,可以直接删除。
如果删除的节点只有一个子节点,让这个仅有的子节点替代他。
如果被删除节点的左右子树均不为空,则根据二叉搜索树的中序遍历有序性,删除该节点时,可以使用其直接前驱(或直接后继)代替被删除节点的位置。
直接前驱:在中序遍历中,节点 p 的直接前驱为其左子树的最右侧的叶子节点。
直接后继:在中序遍历中,节点 p 的直接后继为其右子树的最左侧的叶子节点。
算法步骤如下:
1.如果当前节点为空,则返回当前节点。
2.如果当前节点值大于 val,则递归去左子树中搜索并删除,此时 root.left 也要跟着递归更新。
3.如果当前节点值小于 val,则递归去右子树中搜索并删除,此时 root.right 也要跟着递归更新。
4.如果当前节点值等于 val,则该节点就是待删除节点。
1.如果当前节点的左子树为空,则删除该节点之后,则右子树代替当前节点位置,返回右子树。
2.如果当前节点的右子树为空,则删除该节点之后,则左子树代替当前节点位置,返回左子树。
3.如果当前节点的左右子树都有,则将左子树转移到右子树最左侧的叶子节点位置上,然后右子树代替当前节点位置。
class Solution:
def deleteNode(self, root: TreeNode, val: int) -> TreeNode:
if not root: #root为空,代表未搜索到值为 key的节点,返回空。
return root
if root.val > val: #要删除的节点在左子树上
root.left = self.deleteNode(root.left, val)
return root
elif root.val < val: #要删除的节点在右子树上
root.right = self.deleteNode(root.right, val)
return root
else: #root.val = val,找到要删除的节点;叶子节点则返回空
if not root.left: #左子树为空,返回右子树
return root.right
elif not root.right: #右子树为空,返回左子树
return root.left
else: #左右子树都有,寻找后继节点替代当前节点
curr = root.right
while curr.left: #寻找右子树的最左节点,即后继节点
curr = curr.left
curr.left = root.left #将左子树转移到后继节点位置上
return root.right #返回右子树
4.2 练习(day11)
1.二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
思路一:递归查找,详见4.1.1。时间复杂度:O(N)。空间复杂度:O(N)。
思路二:迭代。时间复杂度:O(N)。空间复杂度:O(1)。
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
while root: #若 root 为空则跳出循环,并返回空节点
if val == root.val: #若 val=root.val则返回 root
return root
if val < root.val: #若 val<root.val,将 root 置为 root.left;
root = root.left
else: #若 val>root.val,将 root 置为 root.right
root = root.right
return None
2.二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果 。
思路一:递归,详见4.1.2。时间复杂度:O(N)。空间复杂度:O(N)。
思路二:迭代。时间复杂度:O(N)。空间复杂度:O(1)。
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
if not root: #若 root 为空则创建一个新节点
return TreeNode(val)
node = root
while node:
if val < node.val: #若 val<node.val
if not node.left: #若 node.left为空,则创建一个新节点
node.left = TreeNode(val)
break
else: #否则,将 node 置为 node.left
node = node.left
else: #若 val>node.val
if not node.right: #若 node.right为空,则创建一个新节点
node.right = TreeNode(val)
break
else: #否则,将 node 置为 node.right
node = node.right
return root
3.删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
思路一:递归,详见4.1.4。时间复杂度:O(N)。空间复杂度:O(N)。
思路二:迭代。时间复杂度:O(N)。空间复杂度:O(1)。
class Solution:
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
cur, curParent = root, None
while cur and cur.val != key:
curParent = cur
cur = cur.left if cur.val > key else cur.right
if cur is None:
return root
if cur.left is None and cur.right is None:
cur = None
elif cur.right is None:
cur = cur.left
elif cur.left is None:
cur = cur.right
else:
successor, successorParent = cur.right, cur
while successor.left:
successorParent = successor
successor = successor.left
if successorParent.val == cur.val:
successorParent.right = successor.right
else:
successorParent.left = successor.right
successor.right = cur.right
successor.left = cur.left
cur = successor
if curParent is None:
return cur
if curParent.left and curParent.left.val == key:
curParent.left = cur
else:
curParent.right = cur
return root
作者:力扣官方题解
链接:https://leetcode.cn/problems/delete-node-in-a-bst/solutions/1529700/shan-chu-er-cha-sou-suo-shu-zhong-de-jie-n6vo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
4.3 练习(day12)
1. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
- 官方题解
思路一:递归。
设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r) 的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。
函数递归调用的入口为 helper(root, -inf, +inf), inf 表示一个无穷大的值。
时间复杂度:O(N)。空间复杂度:O(N)。
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
def helper(node, lower = float('-inf'), upper = float('inf')) -> bool:
if not node:
return True
val = node.val
if val <= lower or val >= upper:
return False
if not helper(node.right, val, upper):
return False
if not helper(node.left, lower, val):
return False
return True
return helper(root)
思路二:中序遍历。
在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。如果均大于说明这个序列是升序的,整棵树是二叉搜索树,否则不是。
时间复杂度:O(N)。空间复杂度:O(N)。
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
stack, inorder = [], float('-inf')
while stack or root:
while root:
stack.append(root)
root = root.left
root = stack.pop()
# 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if root.val <= inorder:
return False
inorder = root.val
root = root.right
return True
2. 将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
思路:根据中序遍历(升序序列)还原二叉搜索树,(不能保证结果唯一)。选择中间位置的数字作为根节点,则根节点的下标为 mid=len(nums)/2。递归生成左右子树
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
if len(nums)==0:
return None
mid = len(nums)//2
node = TreeNode(nums[mid])
node.left = self.sortedArrayToBST(nums[:mid])
node.right = self.sortedArrayToBST(nums[mid+1:])
return node
3. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路:查找第一个值位于p和q中间的节点
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
ancestor = root
while ancestor:
if p.val < ancestor.val and q.val < ancestor.val:
ancestor = ancestor.left
elif p.val > ancestor.val and q.val > ancestor.val:
ancestor = ancestor.right
else:
break
return ancestor