LeetCode 230. 二叉搜索树中第 K 小的元素:解题思路+双解法实现

1 阅读6分钟

今天拆解一道二叉搜索树(BST)的经典应用题——LeetCode 230. 二叉搜索树中第 K 小的元素。这道题核心考察 BST 的特性,难度中等,却是面试中高频出现的基础题,掌握两种核心解法(迭代+递归),能帮我们更灵活应对同类问题。

先看题目要求:给定一个二叉搜索树的根节点 root 和整数 k(k 从 1 开始计数),设计算法查找其中第 k 小的元素。

在动手写代码前,我们必须先抓住一个关键前提——二叉搜索树的中序遍历特性:BST 的中序遍历(左 → 根 → 右)结果是升序排列的。这意味着,中序遍历序列的第 k-1 个元素(索引从 0 开始),就是题目要求的第 k 小的元素。

基于这个核心特性,我们可以衍生出两种解法:迭代式中序遍历(适合处理大数据量,避免递归栈溢出)和递归式中序遍历(代码简洁,易于理解)。下面分别拆解两种解法的思路和代码实现。

一、题目前置知识(快速回顾)

首先明确 TreeNode 的定义(题目已给出,这里统一梳理,方便后续理解代码):

class TreeNode {
  val: number
  left: TreeNode | null
  right: TreeNode | null
  constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = (val === undefined ? 0 : val)
    this.left = (left === undefined ? null : left)
    this.right = (right === undefined ? null : right)
  }
}

核心提醒:BST 的左子树所有节点值 < 根节点值,右子树所有节点值 > 根节点值,中序遍历升序是解题的核心突破口。

二、解法一:迭代式中序遍历(推荐,无栈溢出风险)

1. 解题思路

迭代式中序遍历通过「栈」模拟递归过程,避免了递归可能出现的栈溢出问题(当树的深度极大时,递归会触发栈溢出,迭代则不会)。步骤如下:

  1. 初始化一个空栈,以及当前节点 curr 指向根节点 root;

  2. 先将 curr 及其所有左子节点依次压入栈(对应中序遍历的「左」);

  3. 弹出栈顶节点(此时是当前最小的节点),对其进行计数(k--);

  4. 如果 k 减至 0,说明当前弹出的节点就是第 k 小的元素,直接返回其值;

  5. 否则,将当前节点的右子节点作为新的 curr,重复步骤 2-4(处理右子树的「左」)。

简单说:就是通过栈「左到底」,再依次弹出计数,找到第 k 个弹出的节点即可。

2. 代码实现

function kthSmallest_1(root: TreeNode | null, k: number): number {
  if (!root) return 0; // 边界条件:树为空,返回0(题目默认k有效,实际可抛异常)
  let res = 0;
  const stack: TreeNode[] = [];
  let curr: TreeNode | null = root;
  
  // 第一步:将当前节点及其所有左子节点压入栈
  while (curr) {
    stack.push(curr);
    curr = curr.left;
  }
  
  // 第二步:弹出栈顶,计数,处理右子树
  while (stack.length) {
    const node = stack.pop()!; // 弹出当前最小节点(栈顶)
    
    // 将当前节点的右子节点及其所有左子节点压入栈
    let right = node.right;
    while (right) {
      stack.push(right);
      right = right.left;
    }
    
    k--; // 计数减1
    if (k === 0) { // 找到第k小的元素
      res = node.val;
      break;
    }
  }
  return res;
};

3. 复杂度分析

  • 时间复杂度:O(h + k),h 是树的高度。最坏情况下,树是斜树(高度为 n),需要遍历 k 个节点,此时复杂度为 O(n);最好情况下,树是完全二叉树(h = logn),复杂度为 O(logn + k)。

  • 空间复杂度:O(h),栈的大小最多为树的高度(斜树时为 n,完全二叉树时为 logn)。

三、解法二:递归式中序遍历(简洁,易于理解)

1. 解题思路

递归式中序遍历更符合我们对「左→根→右」的直观理解,核心是通过递归遍历左子树,在遍历到根节点时进行计数,一旦计数达到 k,就记录结果并提前终止递归(避免不必要的遍历)。步骤如下:

  1. 定义一个递归辅助函数 helper,接收当前节点;

  2. 递归遍历当前节点的左子树(优先处理左,保证升序);

  3. 遍历到根节点时,k 减 1,若 k 为 0,记录当前节点值为结果,直接返回(提前终止);

  4. 递归遍历当前节点的右子树;

  5. 从根节点开始调用 helper,最终返回记录的结果。

关键优化:当 k 减至 0 时,直接终止递归(包括父递归),避免遍历整个树,提升效率。

2. 代码实现

function kthSmallest_2(root: TreeNode | null, k: number): number {
  if (!root) return 0; // 边界条件
  let res = 0;
  
  // 递归中序遍历辅助函数
  const helper = (node: TreeNode | null) => {
    if (!node || k === 0) return; // 节点为空或已找到结果,提前终止
    helper(node.left); // 遍历左子树(左)
    
    // 处理当前节点(根)
    k--;
    if (k === 0) {
      res = node.val;
      return;
    }
    
    helper(node.right); // 遍历右子树(右)
  };
  
  helper(root); // 启动递归
  return res;
};

3. 复杂度分析

  • 时间复杂度:O(h + k),与迭代法一致。最坏情况遍历整个树(k = n),复杂度 O(n);最好情况遍历到第 k 个节点就终止,复杂度 O(logn + k)。

  • 空间复杂度:O(h),递归栈的深度等于树的高度(斜树时为 n,完全二叉树时为 logn),存在栈溢出风险(树深度极大时)。

四、两种解法对比与选择

解法优点缺点适用场景
迭代式(kthSmallest_1)无栈溢出风险,可控性强代码稍长,需要手动维护栈树深度较大、大数据量场景
递归式(kthSmallest_2)代码简洁,易于理解和编写树深度极大时可能栈溢出算法题解题、树深度较小时

五、常见易错点提醒

  1. k 从 1 开始计数:注意计数时是 k-- 后判断是否为 0,而非先判断再减(否则会漏判第 1 个元素)。

  2. 边界条件处理:当 root 为 null 时,题目默认返回 0(实际场景可根据需求抛异常,比如 k 无效时)。

  3. 递归提前终止:必须在 helper 函数中判断 k === 0 时直接返回,否则会继续遍历右子树,浪费性能。

  4. 栈的维护:迭代法中,弹出节点后,必须将其右子节点及其所有左子节点压入栈,否则会遗漏右子树的元素。

六、总结

LeetCode 230 题的核心是利用 BST 中序遍历升序的特性,两种解法本质都是中序遍历的不同实现,只是遍历方式(迭代/递归)不同。

实际解题时,优先选择迭代法(避免栈溢出);如果是面试手写题,递归法更简洁,能快速写出正确代码,但需注意说明其栈溢出的局限性。

掌握这道题后,能举一反三解决同类 BST 问题,比如「找第 k 大的元素」(中序遍历逆序,右→根→左)、「验证 BST 的有效性」等,建议大家动手敲一遍代码,感受两种遍历方式的差异。