题目重点:比较逐次 DFS 与一次构建频数索引,再解释节点变更时索引和锁怎样保持一致。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 树是任意二叉树,节点值可重复;单次返回目标整数出现次数。若给定 BST,剪枝可能依赖重复值放置规则,题目并未说明。
- 训练版先按静态树设计多次查询,再讨论有节点值变更的并发场景;能否持有树的独占修改权须由面试官确认。
解题路线
- 一次查询用栈迭代 DFS,每个节点恰访问一次;空树返回 0,重复值每个节点分别计数。
- 若同一静态树要回答 Q 次不同 target,预处理 value→count 哈希表:一次 O(n),之后每次平均 O(1);比每次扫描 O(Qn) 更适合高 Q。
- 若树更新,新值替换旧值必须在同一同步边界内改树节点与两个频数项;只有持久化索引和树形成同一版本快照,读者才能得到一致结果。
- 锁选型与需求相关:粗粒度读写锁易证明但写入阻塞读;不可变快照/复制更新适合读多写少但内存和回收成本高;仅对哈希表加锁不足以保证树视图一致。
- 不用多线程的加速点是避免重复树遍历、复用索引;单次任意树的全量计数存在 Ω(n) 下界,除非已有额外索引或树结构约束。
手动走一遍
树值 [5,3,5,5](形状任意)查询 5 得 3。若先建索引 {5:3,3:1},一万次查询不必一万次扫描四节点。若把一个 5 改为 3,必须同步变成 {5:2,3:2};只改树或只改索引都会让两种查询口径冲突。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
from collections import Counter
from dataclasses import dataclass
@dataclass
class Node:
value: int
left: "Node | None" = None
right: "Node | None" = None
def count_value(root: Node | None, target: int) -> int:
"""One-off query; arbitrary binary tree, not necessarily a BST."""
if root is None:
return 0
count = 0
stack = [root]
while stack:
node = stack.pop()
count += node.value == target
if node.left is not None:
stack.append(node.left)
if node.right is not None:
stack.append(node.right)
return count
def build_value_index(root: Node | None) -> Counter[int]:
"""Static-tree preprocessing for many different target queries."""
index: Counter[int] = Counter()
if root is None:
return index
stack = [root]
while stack:
node = stack.pop()
index[node.value] += 1
if node.left is not None:
stack.append(node.left)
if node.right is not None:
stack.append(node.right)
return index
复杂度
单查 O(n) 时间、O(h) DFS 栈;静态预处理 O(n) 时间、O(u+h) 空间,u 为不同值数,查询平均 O(1)。变更维护索引平均 O(1) 还需节点定位与锁成本;若需结构插删或多版本,复杂度另计。
易错点与追问
容易失分的边界
- 未经证据把一般二叉树当 BST,只沿一条搜索路径。
- 把多线程当唯一加速方式,忽视 Q 次重复查询可索引。
- 修改树值时忘记同步减旧值/加新值。
- 只锁索引不锁树,却宣称两个状态一致。
- 静态树的平均 O(1) 查询夸为构建也 O(1)。
继续深挖
- 若查询按子树根限定,而不是整棵树,如何做每子树计数索引?
- 若节点结构不断变动,频数索引何时重建?
- 读多写少时 copy-on-write 快照如何发布和回收?
- 若 target 值域很小,数组索引相对哈希表有何优势?
