← 返回题目列表

如何设计一个二叉搜索树迭代器?

高频 中等 第 10 / 27 题 更新于 2026/07/29
二叉搜索树BST迭代器中序遍历

简化版

BST 迭代器要按升序不断返回下一个元素,本质是“可暂停的中序遍历”。用一个栈保存尚未访问的祖先路径:初始化时把根到最左节点一路压栈;next() 弹出栈顶作为答案,再把它的右子树一路压左;hasNext() 判断栈是否为空。

详细版

因为 BST 的中序遍历结果是升序,所以迭代器只要模拟中序遍历即可。普通中序遍历会一次性遍历完整棵树;迭代器要求每次调用 next() 只前进一步,因此不能提前把所有节点放进数组,否则空间是 O(n),也不符合“懒加载”的思想。

更好的做法是维护一个单调访问栈。栈里保存“下一批可能被访问的节点”,栈顶永远是当前还没输出的最小节点。初始化时 pushLeft(root);每次 next() 弹出一个节点 node,如果它有右子树,就对 node.right 执行 pushLeft,保证下一个最小节点又回到栈顶。单次 next() 的均摊时间 O(1),最坏一次可能 O(h),空间 O(h)。

完整版教学

一、为什么这题不是简单中序遍历

如果题目只问“输出 BST 升序序列”,直接中序遍历即可。但迭代器多了两个接口:next()hasNext()。这意味着遍历过程要能暂停、恢复,并且每次只交付一个值。把所有节点提前放进数组当然也能过功能,但它把初始化变成 O(n),空间也是 O(n),当树有 100000 个节点而用户只调用 3 次 next() 时,前面 99997 个节点的遍历都浪费了。

普通中序遍历:一次性跑完 root -> list
BSTIterator:每次 next 只推进到下一个节点

这类题的面试重点是“懒遍历”。你要证明自己知道 BST 的中序有序,也知道如何用栈保存遍历现场,而不是把结果数组当成唯一方案。

二、栈里到底保存什么

栈里保存的是“从当前子树一路向左走的路径”。初始化时,把 root, root.left, root.left.left... 压入栈,直到空。因为 BST 中最小值一定在最左边,所以栈顶就是第一个要返回的节点。

        7
       / \
      3   15
         /  \
        9    20

初始化 pushLeft(7):
push 7, push 3
stack top -> 3

当弹出 3 后,3 没有右子树,下一个就是栈里的 7。当弹出 7 后,7 有右子树 15,不能直接返回 15,因为 15 的左边还有 9,所以要对 15 执行 pushLeft,压入 15、9,栈顶变成 9。

三、用数字序列模拟几次 next

用上面的树,迭代器输出应该是 3, 7, 9, 15, 20。栈的变化如下:

操作返回值栈变化说明栈顶
初始化-压入 7、33
next()3弹出 3,无右子树7
next()7弹出 7,对右子树 15 压左:15、99
next()9弹出 9,无右子树15
next()15弹出 15,对右子树 20 压左:2020
next()20弹出 20,无右子树

这个过程说明一个关键点:栈顶永远是“尚未输出的最小节点”。只要这个不变量成立,next() 弹栈返回就是正确的。

四、标准代码写法

实现时最好抽一个私有方法 pushLeft,这样构造函数和 next() 都能复用,代码更不容易写乱。

class BSTIterator {
    private Deque<TreeNode> stack = new ArrayDeque<>();

    public BSTIterator(TreeNode root) {
        pushLeft(root);
    }

    public int next() {
        TreeNode node = stack.pop();
        if (node.right != null) {
            pushLeft(node.right);
        }
        return node.val;
    }

    public boolean hasNext() {
        return !stack.isEmpty();
    }

    private void pushLeft(TreeNode node) {
        while (node != null) {
            stack.push(node);
            node = node.left;
        }
    }
}

next() 默认在题目约束下调用前一定有下一个元素;真实工程里可以在栈空时抛异常。面试题里不必过度设计异常处理,重点是遍历状态维护。

五、为什么说 next 均摊 O(1)

单看某一次 next(),它可能弹出一个节点后又沿右子树压入一条左链,最坏 O(h)。但从整棵树生命周期看,每个节点只会被压栈一次、弹栈一次,所以 n 次 next() 总成本是 O(n)。平均到每次,就是均摊 O(1)。

总压栈次数 = n
总弹栈次数 = n
n 次 next 的总操作数约 2n
均摊成本 = O(1)

空间复杂度是 O(h),因为栈里最多保存一条从根到叶子的路径。平衡 BST 中 h 约为 log2(n),例如 n=1023 时高度约 10;如果退化成链表,h 可能等于 n。

六、常见误区与追问

记忆钩子:BSTIterator 不是存答案数组,而是存“中序遍历走到一半的现场”。

  • 误区:初始化时把整棵树中序遍历进数组最简单,所以就是最优。 数组法功能正确,但空间 O(n),且失去懒加载优势。
  • 误区:弹出节点后直接把右孩子压栈即可。 右子树里最小的节点在右孩子的最左链上,必须 pushLeft(node.right)
  • 误区:hasNext() 要继续搜索树。 搜索状态已经保存在栈里,判断栈是否为空即可。
  • 追问:为什么 next() 是均摊 O(1)? 因为每个节点整个生命周期只进栈一次、出栈一次,n 次调用总成本 O(n)。
  • 追问:能不能支持 prev() 可以,但需要额外维护双向迭代状态,或改成双栈/缓存已访问节点,空间和逻辑都会增加。
  • 追问:如果树会动态插入删除怎么办? 这版迭代器默认树结构不变;动态修改会破坏栈中路径,需要并发修改检测或重新设计。

七、加强记忆

BST 迭代器就是把中序遍历拆成很多次 next() 调用:构造时压根到最左路径,保证栈顶是最小未访问节点;每次弹出栈顶后,如果它有右子树,就把右子树的最左路径压进去,保证下一个栈顶仍然是最小未访问节点。面试回答要强调三个点:不提前展开数组、栈保存遍历现场、每个节点只压弹一次,所以空间 O(h),next() 均摊 O(1)。