二叉排序树(BST):一棵会“大小排序”的树
困难4二叉排序树(BST):一棵会“大小排序”的魔法树
你有没有试过在一堆杂乱无章的卡片里快速找到一张数字牌?如果卡片是按大小排好的,你就能很快找到。二叉排序树(也叫二叉查找树)就是一棵能自动帮数字“排队”的树——它把每个数字放到合适的位置,让小的往左走,大的往右走。这样,你只要顺着树枝比较大小,就能像玩“猜数字”游戏一样,又快又准地找到任何数字。
二叉排序树的规则——就像教室里的座位安排
想象教室里有两排座位:左边坐成绩低的同学,右边坐成绩高的同学。每个同学坐的位置都遵守一个规则:比前面同学成绩低的坐左边,比前面同学高的坐右边。二叉排序树的规则和这个一模一样:
- 每个节点(就像每个同学)都有一个数值,我们叫它“键”(key)
- 左子树(左边所有同学)的键都小于当前节点的键
- 右子树(右边所有同学)的键都大于当前节点的键
- 每个子树(左或右)本身也是一棵二叉排序树,规则递归适用
这个规则的魔法之处在于:如果你按中序遍历(先看左边所有同学,再看中间同学,最后看右边所有同学)的顺序报名字,报出来的就是按成绩从小到大排好的顺序!
为什么要用二叉排序树?——生活中的例子
例1:猜数字游戏
老师心里想了一个1~100之间的数字。你猜30,老师说“小了”;你猜70,老师说“大了”……每次猜数后,你就把范围缩小一半。二叉排序树工作方式完全相同:你从树根(相当于第一次猜的数)开始,如果目标数比根小,就只去左子树找;比根大,就去右子树找。这种“每次排除一半”的特性,让查找变得飞快。
例2:整理零花钱记录
假设你每天得到零花钱,想把它们按金额从小到大记在本子上。如果你直接把数字写在空白处,以后要想找某个金额的日期就很麻烦。但如果你用二叉排序树来存,每次拿到新钱时,和树根比一比:小的放左边,大的放右边。等存好后,只要按中序遍历,就能得到从少到多的完整列表,还能快速查找某一天有没有收到10元钱。
例3:排队买冰淇淋
同学们按身高排队买冰淇淋,如果队伍排成二叉排序树的样子(不是直线,而是一棵分叉的树),那么每个人都能在O(log n)步内找到自己的位置——比直接插队快多啦!
新手容易犯的几个错误
-
忘记处理重复值
如果插入一个已有数字,有些实现会把它放在右边(等于或大于),有些会拒绝插入。实际应用中要根据需求决定:比如统计票数时,重复值应该累计计数,而不是再建一个新节点。 -
插入时没有递归到正确位置
新手容易只比较了根节点,就以为“大于就放右孩子”,但右孩子可能已经存在。正确做法是:如果右孩子存在,就继续递归到右子树里;如果右孩子位置为空,才创建新节点。 -
中序遍历顺序混淆
有的同学会搞成“先根、再左、再右”,这是前序遍历。要记住中序遍历是“左→根→右”,才能得到升序序列。 -
树退化成链表
如果插入的数字本身已经排好序(比如依次插入1,2,3,4,5),那么所有节点都会挤到一边,树就变成一根“斜线”,查找速度直线下降。这时候就要用到平衡二叉树(如AVL树、红黑树)来纠正。
完整可运行的代码示例(更详细版)
下面我们用Python实现一个功能更完整的二叉排序树,包含插入、查找、中序遍历、以及删除节点(删除是难点,但值得了解)。代码中每行变量都加了中文注释,方便理解。
class BSTNode:
"""树节点类,每个节点代表一个数字"""
def __init__(self, key): # key 是存储的数字
self.key = key # 当前节点的值
self.left = None # 左孩子指针
self.right = None # 右孩子指针
class BinarySearchTree:
"""二叉排序树类"""
def __init__(self):
self.root = None # 树根初始为空
def insert(self, key):
"""插入一个新值到树中"""
if self.root is None:
self.root = BSTNode(key)
else:
self._insert_recursive(self.root, key)
def _insert_recursive(self, node, key):
"""递归插入,找到正确位置"""
if key < node.key: # 新值比当前节点小
if node.left is None: # 左边空位,直接放
node.left = BSTNode(key)
else: # 左边有节点,继续往左找
self._insert_recursive(node.left, key)
else: # 新值大于等于当前节点(此处允许相等放右边)
if node.right is None:
node.right = BSTNode(key)
else:
self._insert_recursive(node.right, key)
def search(self, key):
"""查找某个值是否存在,返回True/False"""
return self._search_recursive(self.root, key)
def _search_recursive(self, node, key):
"""递归查找"""
if node is None: # 找到空节点,说明不存在
return False
if key == node.key: # 找到了
return True
elif key < node.key: # 比当前小,去左边找
return self._search_recursive(node.left, key)
else: # 比当前大,去右边找
return self._search_recursive(node.right, key)
def inorder(self):
"""中序遍历,返回从小到大排序的列表"""
result = []
self._inorder_recursive(self.root, result)
return result
def _inorder_recursive(self, node, result):
"""递归中序:左→根→右"""
if node:
self._inorder_recursive(node.left, result)
result.append(node.key)
self._inorder_recursive(node.right, result)
def find_min(self, node):
"""找到以node为根的树的最小值(最左边节点)"""
current = node
while current.left:
current = current.left
return current
def delete(self, key):
"""从树中删除指定值的节点(如果存在)"""
self.root = self._delete_recursive(self.root, key)
def _delete_recursive(self, node, key):
"""递归删除,返回删除后的新子树根"""
if node is None:
return None
# 先找到要删除的节点
if key < node.key:
node.left = self._delete_recursive(node.left, key)
elif key > node.key:
node.right = self._delete_recursive(node.right, key)
else: # 找到要删除的节点
# 情况1:没有左孩子,直接用右孩子代替
if node.left is None:
return node.right
# 情况2:没有右孩子,直接用左孩子代替
elif node.right is None:
return node.left
# 情况3:有两个孩子,用右子树的最小节点代替
else:
# 找到右子树中最小的节点
min_node = self.find_min(node.right)
# 把最小节点的值赋给当前节点
node.key = min_node.key
# 删除右子树中的那个最小节点
node.right = self._delete_recursive(node.right, min_node.key)
return node
# ---------- 使用示例 ----------
# 模拟一个班级的考试成绩:小明、小红、小刚等人的分数
scores = [85, 70, 95, 60, 75, 90, 100, 65, 80]
bst = BinarySearchTree()
print("插入分数:", scores)
for s in scores:
bst.insert(s)
print("中序遍历结果(从小到大):", bst.inorder())
# 输出:[60, 65, 70, 75, 80, 85, 90, 95, 100]
# 查找
print("查找 75 分是否在树里:", bst.search(75)) # True
print("查找 99 分是否在树里:", bst.search(99)) # False
# 删除一个节点
print("\n删除 70 分...")
bst.delete(70)
print("删除后中序遍历:", bst.inorder())
# 输出:[60, 65, 75, 80, 85, 90, 95, 100] (70被删掉了)
# 再插入一个新分数
print("\n插入 77 分...")
bst.insert(77)
print("插入后中序遍历:", bst.inorder())
# 输出:[60, 65, 75, 77, 80, 85, 90, 95, 100]
思考题:你能用二叉排序树解决吗?
- 找最接近的数:给你一棵已经建好的二叉排序树,如何找到树中与某个目标值最接近的数?(提示:比较沿途节点与目标值的差,记录最小差值)
- 检查两棵二叉树是否相同:如果两棵树的形状和每个节点的值都一模一样,它们就是相同的。如何用中序遍历来判断?(注意:只靠中序遍历的顺序相同不能保证树相同,因为不同的树可能有相同的中序序列)
- 用中序遍历判断二叉排序树是否正确:如果一棵二叉树的中序遍历结果是递增的,那么它一定是一棵二叉排序树吗?答案是不一定——因为中序遍历只保证数值顺序,但不保证子树间的结构关系。需要额外检查每个节点的值是否比左子树所有值大、比右子树所有值小。
相关指引——学完这个,下一步学什么?
- 平衡二叉树(AVL树):让树的高度始终接近log N,解决“斜线”性能下降问题。适合追求稳定效率的场景。
- 红黑树:一种更实用的平衡树,STL的map和set底层就用它。规则比AVL稍宽松,但插入删除性能更好。
- 堆(Heap):另一种特殊树,用来快速找到最大或最小值(比如优先级队列),但不是有序的。
- B树/B+树:用在数据库和文件系统中,每个节点可以存多个值,减少磁盘读写次数。
要记住:二叉排序树是所有高级树结构的基础。把它的插入、查找、删除、遍历搞熟练,后面学平衡树就会轻松很多。
例题精讲
二叉排序树(BST)中,中序遍历得到的序列是什么?
在二叉排序树中,若某个节点只有左子树,则该节点的值一定大于其所有左子树节点的值。
完成以下Python函数,实现向二叉排序树中插入一个节点。
class TreeNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def insert(root, data):
if root is None:
return TreeNode(data)
if ___:
root.left = insert(root.left, data)
else:
root.right = insert(root.right, data)
return root在二叉排序树中,删除一个既有左子树又有右子树的节点时,通常的做法是:
二叉排序树查找操作的时间复杂度在平均情况下为O(log n),在最坏情况下为O(n)。