LeetCode 173. 二叉搜索树迭代器:BSTIterator类 实现与解析

0 阅读7分钟

核心前提:先明确两个关键基础,才能理解迭代器类的设计逻辑

  1. 二叉搜索树(BST)特性:左子树所有节点值 < 根节点值 < 右子树所有节点值,因此它的中序遍历结果是严格升序的(这是迭代器“按顺序返回元素”的核心依据)。

  2. 迭代器的核心作用:不是一次性遍历整棵树,而是“按需访问”——调用一次next(),才返回下一个升序元素;调用hasNext(),判断是否还有下一个元素,避免一次性存储所有节点(节省内存)。

题目要求的BSTIterator类,本质是「用栈模拟BST中序遍历」,封装成可调用的类,完全贴合题干中“指针初始在最小元素前、next()移动指针、hasNext()判断是否有下一个”的要求。下面结合给出的TypeScript代码,逐部分拆解解释。

一、辅助类:TreeNode(二叉树节点定义)

这是题目预先定义的二叉树节点结构,是实现BSTIterator的基础,代码作用很简单——定义单个节点的组成:

class TreeNode {
  val: number          // 存储当前节点的值(数字类型)
  left: TreeNode | null // 指向左子节点,可能为空(没有左子节点)
  right: TreeNode | null // 指向右子节点,可能为空(没有右子节点)
  // 构造函数:初始化节点,给val、left、right设置默认值(避免未传参报错)
  constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = (val === undefined ? 0 : val) // 未传val,默认值为0
    this.left = (left === undefined ? null : left) // 未传left,默认null
    this.right = (right === undefined ? null : right) // 未传right,默认null
  }
}

注意:题干中重复出现了TreeNode定义,实际编码时只需写一次,重复是为了提醒开发者该类已存在,无需自行定义。

二、核心类:BSTIterator(二叉搜索树迭代器)

该类是解题核心,包含「私有成员变量」「构造函数」「next()方法」「hasNext()方法」四部分,每一部分都围绕“模拟中序遍历、按需访问”展开。

1. 私有成员变量:stack(核心数据结构)

private stack: TreeNode[] = []

关键解释:

  • private修饰:表示该栈只能在BSTIterator类内部访问,外部无法直接操作(封装性,避免误修改)。

  • 类型是TreeNode[]:栈中存储的是二叉树的节点(不是节点值),目的是记录“待遍历的节点路径”。

  • 初始化为空数组:迭代器初始化时,会根据根节点填充栈,栈的作用是“记住下一个要访问的节点”。

核心逻辑:栈顶始终是「下一个要返回的节点」(即中序遍历的下一个元素),这是整个迭代器的核心设计。

2. 构造函数:BSTIterator(root: TreeNode | null)

作用:初始化迭代器,将“指针”定位到“比BST中所有元素都小的虚拟位置”(题干要求),本质是填充栈,让栈顶成为BST的最小元素(首次调用next()会返回它)。

constructor(root: TreeNode | null) {
  if (root) { // 先判断根节点是否为空(空树无需处理)
    this.stack.push(root) // 先把根节点压入栈
    // 沿着根节点的左子树,一直走到底,所有左子节点依次压入栈
    while (root.left) {
      this.stack.push(root.left)
      root = root.left // 移动到下一个左子节点,直到没有左子节点
    }
  }
}

通俗解析(结合示例树,更易理解):

假设BST结构如下(题干隐性示例,最易理解):

初始化(root=7)的执行流程:

  1. root不为空,将7压入栈 → 栈:[7]

  2. 7有左子节点3 → 将3压入栈 → 栈:[7, 3]

  3. 3没有左子节点 → while循环停止

最终栈:[7, 3],栈顶是3(BST的最小元素)。此时迭代器的“指针”在3的前面(虚拟位置,比3小),完全符合题干“指针初始化为不存在于BST、且小于所有元素”的要求。

3. 核心方法:next() → 移动指针并返回当前值

题干要求:将指针向右移动,然后返回指针处的数字;首次调用返回BST最小元素(栈顶元素)。代码逻辑分3步,逐行解析:

next(): number {
  // 步骤1:弹出栈顶节点(这就是当前要返回的元素,指针随之移动到该节点)
  const node = this.stack.pop()
  // 步骤2:防御性判断(题干保证next()调用有效,实际不会执行这行)
  if (!node) return NaN;
  // 步骤3:如果当前节点有右子节点,处理右子树(保证后续next()能返回升序值)
  if (node.right) {
    this.stack.push(node.right) // 先将右子节点压入栈
    let tmp = node.right // 临时变量,用于遍历右子树的左子节点
    // 沿着右子节点的左子树走到底,所有左子节点依次压入栈
    while (tmp.left) {
      this.stack.push(tmp.left)
      tmp = tmp.left
    }
  }
  // 步骤4:返回当前节点的值(指针已移动到该节点)
  return node.val
}

结合示例树,逐次调用next()的执行流程(承接初始化后的栈[7,3]):

  • 第一次调用next()

    1. pop()弹出栈顶3 → node=3,栈变为[7]

    2. 3没有右子节点 → 不执行步骤3

    3. 返回3(首次调用,返回最小元素,符合题干)

  • 第二次调用next()

    1. pop()弹出栈顶7 → node=7,栈变为[]

    2. 7有右子节点15 → 压入15,栈变为[15]

    3. tmp=15,15有左子节点9 → 压入9,栈变为[15,9]

    4. 9没有左子节点 → while循环停止

    5. 返回7(升序的下一个元素)

  • 第三次调用next()

    1. pop()弹出栈顶9 → node=9,栈变为[15]

    2. 9没有右子节点 → 不执行步骤3

    3. 返回9

  • 第四次调用next()

    1. pop()弹出栈顶15 → node=15,栈变为[]

    2. 15有右子节点20 → 压入20,栈变为[20]

    3. 20没有左子节点 → while循环停止

    4. 返回15

  • 第五次调用next()

    1. pop()弹出栈顶20 → node=20,栈变为[]

    2. 20没有右子节点 → 不执行步骤3

    3. 返回20

关键总结:next()的核心是“弹出栈顶(当前元素)→ 处理右子树(补充后续待遍历节点)→ 返回值”,既保证了升序,又实现了“按需访问”(每次只处理一个节点)。

4. 辅助方法:hasNext() → 判断是否有下一个元素

题干要求:如果向指针右侧遍历存在数字,返回true;否则返回false。代码逻辑极其简洁,完全依赖栈的状态:

hasNext(): boolean {
  return this.stack.length > 0 // 栈不为空 → 有下一个元素
}

结合示例树的调用流程,理解更直观:

  • 初始化后,栈[7,3] → length=2>0 → hasNext()=true

  • 第一次next()后,栈[7] → length=1>0 → hasNext()=true

  • 第五次next()后,栈[] → length=0 → hasNext()=false

逻辑本质:栈中存储的是“待遍历的节点”,栈不为空,说明还有未访问的元素;栈为空,说明所有元素都已访问完毕,指针右侧没有数字了。

三、实例化与调用(题干示例)

题干最后给出的实例化代码,告诉你如何使用BSTIterator类,无需自己编写,仅作参考:

// 1. 实例化迭代器对象,传入BST的根节点
var obj = new BSTIterator(root)
// 2. 调用next(),获取下一个元素(指针移动)
var param_1 = obj.next()
// 3. 调用hasNext(),判断是否还有下一个元素
var param_2 = obj.hasNext()

四、核心设计优势(为什么这么实现)

这个实现是LeetCode 173的最优解之一,优势在于“时间+空间”双高效:

  1. 时间复杂度

    • hasNext():O(1),仅判断栈的长度,无循环。

    • next():均摊O(1),每个节点最多被压入栈、弹出栈各一次,整个树遍历下来,总操作次数是O(n),分摊到每次next()调用,就是O(1)。

  2. 空间复杂度:O(h),h是BST的高度(不是节点总数n)。栈中最多存储“从根节点到当前最左子节点”的路径节点,即树的高度,远优于“一次性存储所有节点(O(n))”的方案,尤其适合大尺寸BST。

五、总结

BSTIterator类,本质是「用栈模拟BST的中序遍历」,通过构造函数初始化“最小元素路径”,next()弹出栈顶并补充右子树路径,hasNext()判断栈是否为空,最终实现“按需、升序、高效”地访问BST的所有节点,完全满足题干的所有要求。