Storage Infrastructure SWE · Hiring Manager / Data Structures

Apple 二叉树目标值计数:单次遍历、重复查询与并发

比较逐次 DFS 与一次构建频数索引,再解释节点变更时索引和锁怎样保持一致。

题目重点:比较逐次 DFS 与一次构建频数索引,再解释节点变更时索引和锁怎样保持一致。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 树是任意二叉树,节点值可重复;单次返回目标整数出现次数。若给定 BST,剪枝可能依赖重复值放置规则,题目并未说明。
  • 训练版先按静态树设计多次查询,再讨论有节点值变更的并发场景;能否持有树的独占修改权须由面试官确认。

解题路线

  1. 一次查询用栈迭代 DFS,每个节点恰访问一次;空树返回 0,重复值每个节点分别计数。
  2. 若同一静态树要回答 Q 次不同 target,预处理 value→count 哈希表:一次 O(n),之后每次平均 O(1);比每次扫描 O(Qn) 更适合高 Q。
  3. 若树更新,新值替换旧值必须在同一同步边界内改树节点与两个频数项;只有持久化索引和树形成同一版本快照,读者才能得到一致结果。
  4. 锁选型与需求相关:粗粒度读写锁易证明但写入阻塞读;不可变快照/复制更新适合读多写少但内存和回收成本高;仅对哈希表加锁不足以保证树视图一致。
  5. 不用多线程的加速点是避免重复树遍历、复用索引;单次任意树的全量计数存在 Ω(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 值域很小,数组索引相对哈希表有何优势?
✓已复制微信号!