二叉搜索树
什么是二叉搜索树
二叉搜索树(Binary Search Tree,BST)是一种常用的数据结构,它是一棵二叉树,其中每个节点的值都大于其左子树中任意节点的值,小于其右子树中任意节点的值。
也就是说,对于一棵 BST,任何一个节点的左子树都比它小,任何一个节点的右子树都比它大。因此,如果我们对 BST 进行中序遍历,得到的就是一个有序的序列。
二叉搜索树的优点是:
- 支持快速的查找、插入、删除操作,时间复杂度为 O(logN)。
- 中序遍历的结果是有序的,可以进行快速的查找最大值、最小值、第 K 小值等操作。
- 由于是一种基于指针的数据结构,可以支持动态的插入、删除操作。
二叉搜索树的缺点是:
- 当 BST 的节点极度不平衡时,查找、插入、删除操作的时间复杂度可能会退化到 O(N),失去了二叉搜索树的优点。
- 如果使用 BST 存储一些特殊的数据,例如退化成链表的情况,它的性能可能会比其他数据结构差。
因此,在实际应用中,我们需要根据具体的需求选择合适的数据结构。
解法
二叉搜索树问题的解法有很多种,下面介绍几种常见的解法:
- 中序遍历
由于二叉搜索树的中序遍历结果是一个升序序列,因此可以通过中序遍历来解决一些问题。例如,验证二叉搜索树、二叉搜索树中第K小的元素等问题,都可以通过中序遍历来解决。
- 递归
二叉搜索树问题常常可以使用递归解决,因为二叉搜索树的左子树和右子树也是二叉搜索树,可以用相同的方法处理。例如,二叉搜索树的搜索、插入、删除等问题,都可以使用递归解决。
- 迭代
二叉搜索树问题也可以使用迭代解决,使用迭代时需要借助栈或队列等数据结构。例如,二叉搜索树的搜索、插入、删除等问题,都可以使用迭代解决。
- Morris 遍历
Morris 遍历是一种不需要栈和队列的二叉树遍历算法,可以在 O(1) 的空间复杂度下完成中序遍历。这种遍历方法基于线索二叉树的思想,可以在遍历时动态修改树的结构,非常高效。例如,二叉搜索树中第K小的元素问题,可以使用 Morris 遍历解决。
- 双指针
对于二叉搜索树中查找两个节点之间的距离等问题,可以使用双指针技巧解决。双指针的思想是,定义两个指针指向树中的节点,然后通过移动指针来计算它们之间的距离或关系。例如,二叉搜索树中两个节点的最近公共祖先问题,可以使用双指针解决。
总之,二叉搜索树问题的解法多种多样,需要根据具体问题来选择合适的方法。但是需要注意的是,二叉搜索树的插入、删除操作需要保证树的平衡性,因此在实现时需要格外小心。
二叉树中序遍历
这两个代码都是求二叉树的中序遍历,第一个是非递归写法,第二个是递归写法。
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack = []
res = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
res.append(root.val)
root = root.right
return res
非递归写法使用了栈来模拟递归的过程,每次将左子树中的节点全部压入栈中,然后弹出栈顶元素,将其值加入结果数组中,再将右子树赋值给根节点,继续遍历右子树,直到栈为空。
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def inorder(root):
if not root:
return
inorder(root.left)
res.append(root.val)
inorder(root.right)
inorder(root)
return res
递归写法则是典型的深度优先遍历,先遍历左子树,然后将根节点的值加入结果数组中,再遍历右子树。
这两种方法本质上是相同的,只是实现方式不同。在实际应用中,应根据具体的情况选择合适的方法。如果递归层数太深可能会导致栈溢出,而非递归方法则需要手动维护栈,增加了实现难度。
二叉搜索树和中序遍历之间存在着密切的关系。
二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树,它的每个节点的值都大于其左子树中的所有节点的值,小于其右子树中的所有节点的值。也就是说,如果对 BST 进行中序遍历,得到的序列是一个递增的有序序列。
因此,中序遍历是判断一个二叉树是否为 BST 的有效方法之一。我们只需要按照中序遍历的顺序依次比较相邻的节点,如果当前节点的值大于等于前一个节点的值,则这棵树是 BST。
此外,中序遍历还可以对 BST 进行排序,将树中的所有节点按照大小顺序排列。如果需要在 BST 中查找某个值,我们只需要对 BST 进行中序遍历,找到第一个大于等于目标值的节点即可。
因此,可以说二叉搜索树和中序遍历是密不可分的。二者相互依存,相互促进,为我们解决问题提供了很大的便利性。
530. 二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。
示例 1:
输入: root = [4,2,6,1,3]
输出: 1
示例 2:
输入: root = [1,0,48,null,null,12,49]
输出: 1
提示:
- 树中节点的数目范围是
[2, 104] 0 <= Node.val <= 105
class Solution:
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
stack = []
num = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
num.append(root.val)
root = root.right
res = float('inf')
for i in range(len(num)-1):
res = min(abs(num[i+1]-num[i]),res)
return res
这段代码是用来求二叉搜索树中任意两个节点值之差的最小值。
代码的实现过程为:首先用中序遍历得到 BST 中的所有节点值,并将其存储在数组 num 中;然后对数组 num 进行遍历,求出任意两个相邻节点值之差的最小值。
具体来说,我们可以通过以下步骤来理解代码:
-
定义一个栈 stack 和一个数组 num,用来存储中序遍历得到的节点值。
-
当 root 不为空或者栈 stack 不为空时,进行循环遍历。
-
在循环中,将 root 和其所有左子树节点依次入栈 stack 中,直到 root 为空。
-
弹出栈顶节点 root,并将其值加入数组 num 中。
-
将 root 指向其右子树,进行下一轮遍历。
-
遍历完成后,得到一个有序的节点值数组 num。
-
对数组 num 进行遍历,计算任意两个相邻节点值之差的最小值,返回结果。
由于是通过中序遍历得到的节点值数组,数组中的节点值是有序的。因此,我们可以通过遍历数组 num 来求出任意两个相邻节点值之差的最小值。
该算法的时间复杂度为 O(n),其中 n 表示 BST 中的节点数。空间复杂度为 O(n),主要用于存储节点值数组和栈的空间。
代码中可以优化掉那个res数组:
class Solution:
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
pre = float('-inf')
minDiff = float('inf')
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
minDiff = min(minDiff, root.val-pre)
pre = root.val
root = root.right
return minDiff
-
初始化pre为负无穷,minDiff为正无穷,stack为空。
-
当root非空或stack非空时,执行以下循环:
a. 当root非空时,将root入栈,再将root的左子节点作为root继续执行循环,直到root为空。
b. 当root为空时,从stack中弹出一个节点作为root。
c. 计算当前节点值root.val与前一个节点值pre的差,更新minDiff的值为当前minDiff和root.val-pre的最小值。
d. 将pre的值更新为root.val。
e. 将root的右子节点作为新的root,继续执行循环。
-
返回minDiff的值。
230. 二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
示例 1:
输入: root = [3,1,4,null,2], k = 1
输出: 1
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
输出: 3
提示:
- 树中的节点数为
n。 1 <= k <= n <= 1040 <= Node.val <= 104
进阶: 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
res = []
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
res.append(root.val)
root = root.right
return res[k-1]
这段代码是用来寻找二叉搜索树中第 k 小的节点值的。它的实现过程为:
-
定义一个空列表 res 和一个栈 stack,用于存储二叉搜索树的节点值;
-
如果当前节点存在或者栈不为空,则进行循环遍历;
-
在循环中,将当前节点及其左子树中所有的节点入栈 stack 中,直到当前节点为空;
-
弹出栈顶元素,将其节点值加入列表 res 中;
-
将当前节点指向其右子树;
-
遍历完成后,根据列表 res 中的顺序,返回第 k 小的节点值 res[k-1]。
由于二叉搜索树的中序遍历得到的结果是一个有序的序列,因此,通过中序遍历可以得到二叉搜索树的节点值的有序序列。这样就可以根据有序序列的特性,很方便地寻找第 k 小的节点值。
该算法的时间复杂度为 O(n),其中 n 表示二叉搜索树的节点数。因为需要遍历整个二叉搜索树并将节点值存入列表中,所以空间复杂度为 O(n),其中 n 表示二叉搜索树的节点数。
当然其实根本没必要使用额外的那个res
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
k -= 1
if k == 0:
return root.val
root = root.right
这段代码是用来查找二叉搜索树中第 k 小的元素。具体的实现过程为:
-
初始化一个栈 stack,用于存储遍历过的节点。
-
进入循环,当根节点不为空或栈不为空时,进行如下操作:
-
不断将当前节点的左子节点入栈,直到左子节点为空。
-
弹出栈顶节点,并将 k 减1。
-
如果 k 减到0,则返回当前节点的值。
-
将当前节点指向右子节点,进入下一轮循环。
-
-
如果未找到第 k 小的元素,则返回 None。
时间复杂度为 O(H + k),其中 H 为树的高度,空间复杂度为 O(H)。这里的空间复杂度是指栈空间的使用。
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
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
res = []
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
res.append(root.val)
root = root.right
for i in range(len(res)-1):
if res[i] >= res[i+1]:
return False
return True
这段代码的功能是判断给定的二叉树是否为二叉搜索树(BST)。算法的主要思路是通过中序遍历得到二叉树的节点值序列,然后判断序列是否有序。
具体来说,代码的实现过程为:
-
定义一个栈和一个数组,分别用于存储节点和节点值;
-
当二叉树不为空或者栈不为空时,进行循环遍历;
-
在循环中,将二叉树的所有左子树节点依次入栈,直到遇到叶子节点;
-
弹出栈顶节点,将其值存入数组;
-
将二叉树的指针指向其右子树,进行下一轮遍历;
-
遍历完成后,得到一个有序的节点值数组;
-
对节点值数组进行遍历,判断序列是否有序,如果不是则返回 False,否则返回 True。
该算法的时间复杂度为 O(n),其中 n 表示二叉树的节点数,主要用于中序遍历的过程。空间复杂度也为 O(n),主要用于存储栈和节点值数组的空间。
当然这段代码也可以优化掉那个res数组:
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
stack = []
pre = float('-inf')
while stack or root:
while root:
stack.append(root)
root = root.left
root = stack.pop()
if root.val <= pre:
return False
pre = root.val
root = root.right
return True
算法思路:
采用迭代的方式进行中序遍历,由于中序遍历得到的结果是一个递增的序列,因此只需要判断当前节点是否大于前一个节点即可。
具体实现:
-
初始化一个栈stack和一个pre变量,用于记录前一个遍历的节点的值,初始值为负无穷。
-
当栈不为空或当前节点不为空时,执行下面的操作:
-
当前节点存在,将其左子树的所有节点入栈。
-
取出栈顶元素,如果其值小于或等于pre,说明不满足二叉搜索树的定义,返回False。
-
将当前节点的值赋值给pre,遍历其右子树。
-
如果循环结束,仍未返回False,说明遍历完成,返回True。
-
时间复杂度为O(n),空间复杂度为O(n)。
173. 二叉搜索树迭代器
实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root)初始化BSTIterator类的一个对象。BST 的根节点root会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。boolean hasNext()如果向指针右侧遍历存在数字,则返回true;否则返回false。int next()将指针向右移动,然后返回指针处的数字。
注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。
你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。
示例:
输入
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]
解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next(); // 返回 3
bSTIterator.next(); // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 20
bSTIterator.hasNext(); // 返回 False
提示:
- 树中节点的数目在范围
[1, 105]内 0 <= Node.val <= 106- 最多调用
105次hasNext和next操作
class BSTIterator:
def __init__(self, root: Optional[TreeNode]):
self.queue = deque([root])
self.iterator = []
self.idx = 0
self.inOrder(root)
def inOrder(self,root):
if not root:
return
self.inOrder(root.left)
self.iterator.append(root.val)
self.inOrder(root.right)
def next(self) -> int:
n = self.iterator[self.idx]
self.idx += 1
return n
def hasNext(self) -> bool:
if self.idx < len(self.iterator):
return True
else:
return False
把inOrder改成这样也OK:
def inOrder(self,root):
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
self.iterator.append(root.val)
root = root.right
补充练习
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 <= 107root是二叉搜索树1 <= val <= 107
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root:
return
if root.val == val:
return root
elif root.val < val:
return self.searchBST(root.right,val)
elif root.val > val:
return self.searchBST(root.left,val)
这段代码的思路是利用二叉搜索树的特性,即左子树中的所有节点的值都小于根节点的值,右子树中的所有节点的值都大于根节点的值。因此,可以根据root的值和val的大小关系来判断要在哪个子树中查找。如果找到了节点,就返回该节点;否则返回None。
501. 二叉搜索树中的众数
给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。
假定 BST 满足如下定义:
- 结点左子树中所含节点的值 小于等于 当前节点的值
- 结点右子树中所含节点的值 大于等于 当前节点的值
- 左子树和右子树都是二叉搜索树
示例 1:
输入: root = [1,null,2,2]
输出: [2]
示例 2:
输入: root = [0]
输出: [0]
提示:
- 树中节点的数目在范围
[1, 104]内 -105 <= Node.val <= 105
进阶: 你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)
class Solution:
def findMode(self, root):
res = Counter()
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop()
res[root.val] += 1
root = root.right
m = max(res.values())
ans = []
for k,v in res.items():
if v == m:
ans.append(k)
return ans
-
初始化一个 Counter 对象
res,用于存储树中每个值的频率。 -
初始化一个堆栈以跟踪树中的节点。
-
当根节点不为 None 或堆栈不为空时:
- 将所有左子节点推入堆栈。
- 从堆栈中弹出顶部节点。
- 在 Counter
res中递增节点值的频率。 - 将右子节点推入堆栈(如果存在)。
-
在 Counter
res中查找最大频率m。 -
创建一个空列表
ans。 -
对于 Counter
res中的每个键值对(k,v):- 如果值
v等于最大频率m,则将键k添加到列表ans。
- 如果值
-
返回包含树的众数的列表
ans。