第 K 小、区间和与 BST 转有序数组
利用有序的中序遍历,以 O(k) 的时间查找第 k 小元素,并以 O(log n + k) 的时间计算区间内的值之和。
第 K 小、区间和与 BST 转有序数组 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。
BST 中的第 k 小元素
BST 中的第 k 小元素(LeetCode #230)是一道经典问题,直接利用了中序遍历的排序性质。由于中序遍历会按升序访问节点,我们只需在遍历过程中统计节点数量,并返回计数达到 k 时的值。时间复杂度为 O(h + k),其中 h 是树的高度(用于到达最左侧节点),k 是中序遍历过程中移动的步数。
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def kth_smallest(root, k):
count = [0]
result = [None]
def inorder(node):
if not node or result[0] is not None:
return
inorder(node.left)
count[0] += 1
if count[0] == k:
result[0] = node.val
return
inorder(node.right)
inorder(root)
return result[0]
root = TreeNode(3)
root.left = TreeNode(1)
root.right = TreeNode(4)
root.left.right = TreeNode(2)
print(kth_smallest(root, 1)) # 1
print(kth_smallest(root, 2)) # 2第 k 小元素:使用栈的迭代方法
迭代版本使用显式栈的中序遍历模式。不断压入左侧节点直到遇到空节点,然后弹出节点并计数。当计数达到 k 时,返回当前节点的值。这可以避免非常深的树触发 Python 的递归深度限制,时间复杂度同样为 O(h + k),空间复杂度为 O(h)。面试官经常会在递归版本之后要求实现迭代版本。
def kth_smallest_iterative(root, k):
stack = []
curr = root
count = 0
while curr or stack:
while curr: # go as far left as possible
stack.append(curr)
curr = curr.left
curr = stack.pop() # process node
count += 1
if count == k:
return curr.val
curr = curr.right # move to right subtree
return -1 # k out of range
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(6)
root.left.left = TreeNode(2)
root.left.right = TreeNode(4)
root.left.left.left = TreeNode(1)
print(kth_smallest_iterative(root, 3)) # 3BST 中的第 k 大元素
第 k 大元素使用反向中序遍历(右子树 → 根节点 → 左子树),按降序访问节点。移动 k 步后返回当前节点的值。它与第 k 小元素的做法对称,时间复杂度为 O(h + k)。如果知道树的大小,也可以计算 kth_smallest(root, total_count - k + 1),但反向中序方法更加优雅。
def kth_largest(root, k):
count = [0]
result = [None]
def reverse_inorder(node):
if not node or result[0] is not None:
return
reverse_inorder(node.right) # visit LARGER values first
count[0] += 1
if count[0] == k:
result[0] = node.val
return
reverse_inorder(node.left)
reverse_inorder(root)
return result[0]
root = TreeNode(3)
root.left = TreeNode(1)
root.right = TreeNode(4)
root.left.right = TreeNode(2)
print(kth_largest(root, 1)) # 4 (largest)
print(kth_largest(root, 2)) # 3 (2nd largest)BST 的范围和
BST 的范围和(LeetCode #938)要求计算 [low, high] 内所有值的总和。请利用 BST 性质进行剪枝:如果当前节点的值小于 low,那么整个左子树也都小于 low——跳过左子树。如果当前值大于 high,则跳过右子树。这种方法可以剪去许多分支,比完整的中序扫描更高效。
def range_sum_bst(root, low, high):
if not root:
return 0
total = 0
if low <= root.val <= high:
total += root.val
if root.val > low: # left subtree might have values >= low
total += range_sum_bst(root.left, low, high)
if root.val < high: # right subtree might have values <= high
total += range_sum_bst(root.right, low, high)
return total
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(15)
root.left.left = TreeNode(3)
root.left.right = TreeNode(7)
root.right.right = TreeNode(18)
print(range_sum_bst(root, 7, 15)) # 7+10+15 = 32统计范围内的节点数
统计范围 [low, high] 内的节点数遵循相同的剪枝逻辑。另一种方法是在中序数组上使用 bisect_left/右边界二分查找——但直接遍历 BST 的时间复杂度为 O(log n + k),而先转换为数组始终需要 O(n) 时间。除非需要回答大量范围查询,否则请选择直接遍历;在需要回答大量查询时,构建带有子树计数的增强 BST,可以将每次查询的复杂度降至 O(log n)。
def count_range(root, low, high):
if not root:
return 0
count = 0
if low <= root.val <= high:
count += 1
if root.val > low:
count += count_range(root.left, low, high)
if root.val < high:
count += count_range(root.right, low, high)
return count
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(15)
root.left.left = TreeNode(3)
root.left.right = TreeNode(7)
root.right.right = TreeNode(18)
print(count_range(root, 6, 15)) # 7, 10, 15 = 3BST 转排序数组(完整算法)
将 BST 转换为排序数组的时间复杂度为 O(n),空间复杂度为 O(n)。请使用中序遍历,并对每个值调用 append。它是多步骤问题的起点,例如“合并两个 BST”、“查找 BST 的中位数”或“检查两个 BST 是否具有相同的中序序列”。生成的数组支持按索引 O(1) 访问、二分查找和双指针技巧,而 BST 本身无法直接提供这些操作。
def bst_to_sorted(root):
result = []
def inorder(node):
if not node:
return
inorder(node.left)
result.append(node.val)
inorder(node.right)
inorder(root)
return result
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(8)
root.left.left = TreeNode(1)
root.left.right = TreeNode(4)
root.right.left = TreeNode(6)
root.right.right = TreeNode(9)
print(bst_to_sorted(root)) # [1, 3, 4, 5, 6, 8, 9]
# Binary search on the resulting sorted array:
import bisect
arr = bst_to_sorted(root)
print(bisect.bisect_left(arr, 6)) # 4 (index of 6)增强型 BST:子树大小
增强型 BST 会在每个节点存储额外信息,例如其子树的大小。有了子树大小,查找第 k 小值可以达到 O(log n):在每个节点处,如果左子树大小为 k-1,则当前节点就是答案;如果左子树大小 >= k,则递归处理左子树;否则减去左子树大小后递归处理右子树。这正是竞赛编程中所使用的顺序统计树背后的数据结构。
class AugNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
self.size = 1 # subtree size
def get_size(node):
return node.size if node else 0
def update_size(node):
if node:
node.size = 1 + get_size(node.left) + get_size(node.right)
def kth_smallest_aug(root, k):
left_size = get_size(root.left)
if k == left_size + 1:
return root.val # current node is kth
elif k <= left_size:
return kth_smallest_aug(root.left, k)
else:
return kth_smallest_aug(root.right, k - left_size - 1)
print('Augmented BST: O(log n) kth smallest with subtree sizes')查找 BST 中两个节点之间的所有值
要返回严格位于两个节点 p 和 q 之间的所有值(其中 p.val < q.val),可以将中序遍历与范围剪枝结合起来:经过 p.val 后开始收集值,经过 q.val 后停止。这是范围求和的通用化方法,可以在 O(h + k) 时间内得到位于两个查询值之间的有序序列。
def values_between(root, low, high):
result = []
def inorder(node):
if not node:
return
if node.val > low: # might be values > low on left
inorder(node.left)
if low < node.val < high: # strictly between
result.append(node.val)
if node.val < high: # might be values < high on right
inorder(node.right)
inorder(root)
return result
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(15)
root.left.left = TreeNode(3)
root.left.right = TreeNode(7)
root.right.left = TreeNode(12)
root.right.right = TreeNode(18)
print(values_between(root, 6, 15)) # [7, 10, 12]BST 的中位数
BST 的中位数是中序遍历结果中的中间值。对于包含 n 个节点的 BST,中位数位于索引 n // 2 处(索引从 0 开始)。您可以收集完整的有序数组并通过索引访问中位数,也可以分两次遍历:第一次统计 n 个节点,第二次进行中序遍历,在到达第 n // 2 个节点时停止。另一种方法是使用查找第 k 小值的算法,并令 k = n // 2 + 1。
def count_nodes(root):
if not root:
return 0
return 1 + count_nodes(root.left) + count_nodes(root.right)
def median_of_bst(root):
n = count_nodes(root)
if n == 0:
return None
k = n // 2 + 1 # (n+1)/2-th element for odd, n/2+1-th for even
return kth_smallest(root, k)
def kth_smallest(root, k):
count = [0]; result = [None]
def inorder(node):
if not node or result[0] is not None: return
inorder(node.left)
count[0] += 1
if count[0] == k: result[0] = node.val; return
inorder(node.right)
inorder(root); return result[0]
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(8)
root.left.left = TreeNode(1)
root.left.right = TreeNode(4)
print(median_of_bst(root)) # 4 (middle of [1,3,4,5,8])距离目标值最近的 K 个值
查找 BST 中距离目标值最近的 k 个值。一种双指针方法是:将 BST 转换为有序数组,然后使用大小为 k 的滑动窗口。另一种方法是使用大小为 k 的最大堆,压入距离,并在大小超过 k 时弹出元素。有序数组方法的时间复杂度为 O(n),实现简单;堆方法的时间复杂度为 O(n log k),但适用于流式场景。
import heapq
def closest_k_values(root, target, k):
# Collect sorted values
arr = []
def inorder(node):
if not node: return
inorder(node.left)
arr.append(node.val)
inorder(node.right)
inorder(root)
# Two-pointer sliding window of size k
left, right = 0, k - 1
while right < len(arr) - 1:
if abs(arr[left] - target) <= abs(arr[right + 1] - target):
break # left is closer, don't advance
left += 1
right += 1
return arr[left:right + 1]
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(5)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
print(closest_k_values(root, 3.7, 2)) # [3, 4]利用后继顺序性质
许多 BST 问题都可以归结为查找有序序列中的下一个或前一个元素——通过 BST 导航可以在 O(log n) 时间内完成这些操作。我们之前构建的迭代器可以使 next 操作的摊销复杂度达到 O(1)。结合查找第 k 小值、范围求和以及最近值相关知识,您可以通过思考“中序遍历的有序性如何简化这个问题?”来解决大多数 BST 面试题。这种元模式就是解决 BST 问题的指南。
# Meta-pattern for BST problems:
# Step 1: What sorted-order property does this exploit?
# Step 2: Is in-order (ascending) or reverse in-order (descending) needed?
# Step 3: Can I prune using BST ordering to avoid O(n) scan?
# Quick reference:
# kth smallest -> in-order, stop at kth node
# kth largest -> reverse in-order, stop at kth node
# range sum -> in-order + BST pruning
# closest value -> walk toward target, track best
# median -> kth with k = n//2+1
# sorted array -> full in-order
# validate -> in-order prev check or min/max bounds
print('Sorted in-order is the universal BST problem tool')快速检查
测试您对本课数据结构与算法——编程面试准备相关概念的理解。
课程回顾
本课中您学习了:使用中序遍历和逆中序遍历在 O(h+k) 时间内查找第 k 小和第 k 大元素,使用BST 剪枝进行范围求和以高效处理范围查询,以及将 BST 转换为有序数组,作为基于数组的算法基础。接下来我们将学习堆和优先队列。
用 AI 导师学习 Python — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 30
- 课程
- 120
常见问题解答
「第 K 小、区间和与 BST 转有序数组」课时是免费的吗?
是的 — 「第 K 小、区间和与 BST 转有序数组」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。
「第 K 小、区间和与 BST 转有序数组」这节课中我会学到什么?
利用有序的中序遍历,以 O(k) 的时间查找第 k 小元素,并以 O(log n + k) 的时间计算区间内的值之和。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DSA Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「第 K 小、区间和与 BST 转有序数组」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DSA Interview Prep 课中编写并运行代码吗?
能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- BST 插入与查找
- BST 删除:三种情况
- 验证 BST 与中序性质
- 第 K 小、区间和与 BST 转有序数组