(b1)BST:查找

官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·2 天前
二叉搜索树查找时间复杂度递归与迭代

BST:查找

看完你能写出BST查找的递归与迭代版本,并分析其复杂度边界

按 空格/→ 演示下一步

1 / 17 页

全部页面点击任意一页,跳回舞台从这页播放

二叉搜索树查找时间复杂度递归与迭代

BST:查找

看完你能写出BST查找的递归与迭代版本,并分析其复杂度边界

1第 1 页 · BST:查找

BST的树结构

从根出发,每层比较一次决定走左或走右,箭头方向就是BST不变式给出的查找路径。

图解渲染中…
a1根节点,唯一入口,所有查找的起点a5演示目标——下面查找示例要找的就是37a2左子树根,继续遵循同样的左<根<右规则
2第 2 页 · BST的树结构

BST查找流程

从根节点出发,每层用key比较筛选方向,直到命中或落空。

1
从根出发
BST的有序性以根为锚点,查找的唯一入口
2
比较当前节点
把目标key和当前节点key做比较,是筛选依据
3
小于左大于右
比较后选左或右子树,沿单边向下、不回溯
4
相等则命中
找到目标节点,携带节点引用返回
5
落到空则未命中
走到null说明目标不在树中,查找终止
3第 3 页 · BST查找流程

查找的本质

在一个有序数组里找数字,每次和中间元素比较就能砍掉一半——这就是二分查找。但数组插入删除太慢,BST 把这个「砍半」的逻辑搬进了树结构里。

二分砍半
每次比较排除一半候选,问题规模指数级缩小
BST 承载二分逻辑
把有序关系编码进左右子树,不再依赖物理数组
O(log n) 的来源
比较次数 ≤ 树高 = ⌈log₂(n+1)⌉(满二叉树时)
猜数字(1-100 选一个)对应 →BST 查找

「大了/小了」每次砍掉一半可能,和树中走左/右是同一思路

T(n)=T(n/2)+O(1)=O(logn)T(n) = T(n/2) + O(1) = O(\log n)
4第 4 页 · 查找的本质

递归查找分解

把"在BST里找一个值"拆解为层层缩小的递归调用,每一步都在严格缩小问题规模。

1
锁定当前子问题
每次调用都在问:目标值在当前这棵子树里吗?
2
与子树根比较
等于命中、小于待左查、大于待右查,三选一
3
利用BST性质选边
目标只可能在被选中的那一侧,另一侧整棵可丢
4
递归调用自身
对更小的子树再次发起同一查找,问题规模严格下降
5
触达空子树终止
遇到空节点说明目标不在此路径,返回未找到
5第 5 页 · 递归查找分解

迭代查找分解

把递归的'调用栈'换成 while 循环,逐步展开迭代查找的执行路径。

1
初始化游标
用变量 curr 指向根节点,代替递归入口参数
2
判断循环终止
curr 为空(走到底)或值为目标(找到)就退出
3
比较并转向
目标小则 curr=curr.left,大则 curr=curr.right
4
返回结果
curr 非空即找到对应节点,空则目标不存在
6第 6 页 · 迭代查找分解

查找实例演示

在一棵BST上演示查找12的完整路径:从根起,每步与当前节点比大小决定走向。

图解渲染中…
R根节点(值为8),所有查找的起点N12目标节点,查找终止的位置
7第 7 页 · 查找实例演示

算法理解自测

点击作答

关于BST查找的递归与迭代实现,以下哪项描述是正确的?

8第 8 页 · 算法理解自测

递归实现代码

python

把上一页的"分解"翻译成真实代码——递归查找的 Python 实现。

代码高亮加载中…

L3-L4 是两个停止口;L8、L11 是两次自我调用,分别把问题规模缩到左/右子树。

9第 9 页 · 递归实现代码

迭代实现代码

python

把递归的「向下递归」翻译成 while 循环,搜索路径完全一致,只是消掉了调用栈。

代码高亮加载中…

迭代版本用 cur 指针沿搜索路径下沉,三次比较对应「命中 / 走左 / 走右」三种状态,循环退出只有两种:命中 return cur,或路径到尽头 return None。

10第 10 页 · 迭代实现代码

递归vs迭代对比

BST 查找两种写法都能正确,但性能和适用场景差异不小——选错可能导致栈溢出。

递归实现
  • 代码简洁,依赖系统调用栈
  • 空间复杂度 O(h),h 为树高
  • 适合树浅、面试或简洁优先
迭代实现
  • 用 while + 变量循环,代码略长
  • 空间复杂度 O(1),只用一个指针
  • 适合树深、生产环境防栈溢出
树浅且追求简洁选递归;树可能很深(如百万节点)必须选迭代,否则递归栈会溢出。
11第 11 页 · 递归vs迭代对比

复杂度深入分析

前面看了递归与迭代两种实现,现在问个根本问题:查找到底有多快?答案不在节点个数 n,而在树的高度 h——这个隐藏变量决定了 O(log n) 还是 O(n)。

复杂度由树高决定
比较次数等于根到目标节点的路径长度,记作 O(h)
平均情况 O(log n)
满二叉树高度约 log n,每层排除一半节点
最坏情况 O(n)
退化成单链,树高等于节点数 n
边界:插入顺序
有序序列依次插入,会构造出最坏的链表
查字典对应 →BST 树高决定快慢

有目录二分跳转对应 log n;目录撕了只能逐页翻对应 O(n)

T=O(h),log2(n+1)1hn1T = O(h), \quad \log_2(n+1) - 1 \leq h \leq n-1
12第 12 页 · 复杂度深入分析

边界情况处理

把查找算法丢进四种最极端的树形里跑一遍,看它会不会崩。

1
空树
根节点为null,比较前就触发递归终止,返回未找到
2
单节点
要么一次命中root,要么走到唯一的空子节点返回
3
只有左子树
目标偏小时一路向左递归到底,偏大时首次向右就撞空
4
只有右子树
与左子树镜像对称,方向相反但行为完全一致
5
边界安全
递归终止与空指针判断覆盖了所有极端情况,不会越界
13第 13 页 · 边界情况处理

查找失败的语义

你去图书馆查一本书,管理员查完系统后说'没有'——这不是系统坏了,而是明确告诉你:这本书确实不在馆藏里。BST 查找返回 null,含义完全一样。

null 是合法出口
搜索正常结束的两条路径:找到返回节点,未找到返回 null
已穷尽搜索范围
走到 null 前 BST 不变式已逐层排除目标,证明确实不在树中
调用者据此判定
拿到 null 即'目标不存在',无需抛异常或继续查找
翻字典查字对应 →BST 返回 null

翻到末尾都没找到,说明字典里没收录这个字

14第 14 页 · 查找失败的语义

树高与性能边界

上一页得到 BST 查找是 O(h),h 是树高。h 不是常数——同一组数据,插入顺序不同,h 可从 log n 涨到 n,效率差几个量级。

平衡态下界
完全二叉树时 h = ⌊log₂(n+1)⌋,这是查找最优情况
退化态上界
依次插入递增数据,每个节点只有一个右孩子,h = n-1
百万节点差五万倍
n = 10⁶ 时,平衡态 h ≈ 20,退化态 h ≈ 10⁶,差距五万倍
h 由建树顺序决定
插入顺序不同 h 就不同,BST 不自带 O(log n) 保证
从矮到高依次排队对应 →BST 按递增数据插入

队尾总比前一个略高,要找某人只能从队头逐个数过去

Tsearch=O(h),h[log2(n+1),n1]T_{search} = O(h), h \in [\lfloor \log_2(n+1) \rfloor, n-1]
15第 15 页 · 树高与性能边界

BST查找小结

  • 左小右大是结构承诺,搜索本质是一路剪枝
  • 递归直观简洁,迭代无栈深风险更稳
  • 时间复杂度O(h),h由树形而非数据量决定
  • 终止顺序:先判null(区间空)后判等值(命中)
  • 查找失败返回null是契约,由调用方判空
延伸主题:BST的插入与删除AVL与红黑树的平衡机制floor/ceiling等区间查询
16第 16 页 · BST查找小结

课后思考

先自己想,再看参考答案。这三个问题没有标准答案,重点是思考路径本身。

1为什么BST查找平均时间复杂度是O(log n),最坏情况却退化成O(n)?

参考答案关键看树高。完全平衡时高度≈log n;若数据有序插入,树退化为链表,高度=n。树高决定比较次数。

2如果BST中存储的是(key, value)对而非单纯key,查找算法该如何改造?

参考答案按key定位节点,找到后返回value;删除也按key定位。要保证key仍满足BST有序性。这正是Map的简化模型。

3当BST出现重复key时,应放左子树、右子树,还是两侧都允许?这如何改变查找语义?

参考答案经典做法是统一放右子树(或左),便于删除;若两侧都允许,查找可能命中多个,"找一个"和"找全部"语义会分裂。

17第 17 页 · 课后思考