力扣解题-230. 二叉搜索树中第 K 小的元素

0 阅读8分钟

力扣解题-230. 二叉搜索树中第 K 小的元素

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

示例 1:

image.png

输入:root = [3,1,4,null,2], k = 1

输出:1

示例 2:

image.png

输入:root = [5,3,6,2,4,null,null,1], k = 3

输出:3

提示:

树中的节点数为 n 。

1 <= k <= n <= 10⁴

0 <= Node.val <= 10⁴

进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

Related Topics

树、深度优先搜索、二叉搜索树、二叉树


第一次解答

解题思路

核心方法:BST中序遍历计数法,利用BST“中序遍历结果为升序序列”的核心特性,通过中序遍历过程中实时计数,当计数等于k时直接记录当前节点值(即第k小元素),无需遍历完整棵树,时间复杂度最优为O(h+k)(h为树的高度)、空间复杂度O(h),是本题的经典最优解法。

核心逻辑拆解

BST找第k小元素的核心规律:

  1. BST关键特性:中序遍历BST得到的节点值序列是严格升序的(如示例2中序遍历结果为[1,2,3,4,5,6]);
  2. 第k小定义:升序序列中第k个元素(从1开始计数)即为第k小元素(如示例2中k=3时,第3个元素是3);
  3. 算法核心:中序遍历过程中维护一个计数器,计数达到k时立即记录当前节点值并提前终止遍历(避免无效遍历)。
具体执行逻辑
  1. 初始化变量
    • ans:存储最终结果(第k小元素值),初始为0;
    • count:中序遍历的计数变量,初始为0(记录当前遍历到第几个元素);
  2. 中序遍历递归
    • 递归终止条件:当前节点node == null,直接返回;
    • 先递归遍历左子树(中序遍历“左”,优先访问更小的元素);
    • 计数+1:count++(当前节点是升序序列中的第count个元素);
    • 终止判断:若count == k,将ans赋值为当前节点值,直接返回(无需继续遍历);
    • 递归遍历右子树(中序遍历“右”,仅当count<k时需要继续);
  3. 返回结果:遍历完成后,ans即为第k小元素值。
执行流程可视化(以示例2 root=[5,3,6,2,4,null,null,1]、k=3为例)
遍历节点count值计数操作终止判断ans更新后续操作
10→1count++1≠30继续遍历
21→2count++2≠30继续遍历
32→3count++3==33直接返回,终止遍历
最终返回ans=3,符合示例2结果(无需遍历4、5、6节点)。
关键细节说明
  • 提前终止遍历:当count == k时立即返回,无需遍历右子树和剩余节点,大幅减少无效遍历(如示例2中k=3时,遍历到3后直接终止);
  • 计数时机:必须在遍历左子树后、处理右子树前计数,符合中序遍历“左→根→右”的顺序;
  • 变量作用域anscount定义为类成员变量,保证递归过程中值的连续性;
  • 边界处理:题目保证1≤k≤n,无需处理k超出范围的场景。
性能说明
  • 时间复杂度:最优O(h+k)(平衡BST中h=logn,k较小时效率极高),最坏O(n)(斜树且k=n时需遍历所有节点);
  • 空间复杂度:O(h)(递归栈深度等于树的高度);
  • 优势:
    1. 利用BST特性,无需存储完整序列,空间效率最优;
    2. 支持提前终止遍历,时间效率高于“存储完整序列后取第k个”的方法;
    3. 递归逻辑简洁,贴合BST中序遍历的经典范式。
    private int ans=0;
    private int count = 0;

    public int kthSmallest(TreeNode root, int k) {
        inorder(root,k);
        return ans;
    }
    public void inorder(TreeNode node,int k) {
        if (node == null) {
            return;
        }
        inorder(node.left,k);
        count++;
        if (count == k) {
            ans = node.val;
            return;
        }
        inorder(node.right,k);
    }

示例解答

解题思路

解法1:中序遍历迭代法(非递归,避免栈溢出)

核心方法:使用栈模拟中序遍历的递归过程,遍历过程中计数,计数达到k时立即返回当前节点值,避免递归栈溢出风险(如斜树场景),且支持更灵活的终止控制。

代码实现
public int kthSmallest(TreeNode root, int k) {
    Stack<TreeNode> stack = new Stack<>();
    TreeNode curr = root;
    int count = 0; // 计数变量
    
    // 中序遍历迭代模板
    while (curr != null || !stack.isEmpty()) {
        // 遍历到左子树最深处
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        // 弹出并处理当前节点
        curr = stack.pop();
        count++;
        // 计数达到k,直接返回当前节点值
        if (count == k) {
            return curr.val;
        }
        // 遍历右子树
        curr = curr.right;
    }
    // 题目保证k有效,此处仅为语法兼容
    return -1;
}
核心逻辑说明
  1. 栈模拟递归:通过栈存储待处理的节点,先遍历到左子树最深处,再弹出节点处理,最后遍历右子树;
  2. 实时计数终止:每弹出一个节点计数+1,计数等于k时立即返回,无需遍历剩余节点;
  3. 无额外成员变量:所有变量为局部变量,代码封装性更好,无线程安全问题。
性能说明
  • 时间复杂度:O(h+k)(与递归法一致);
  • 空间复杂度:O(h)(栈深度等于树的高度);
  • 优势:
    1. 非递归实现,避免极端斜树导致的递归栈溢出;
    2. 计数达到k时直接返回,无需继续执行,效率更高;
  • 劣势:代码量略多于递归法,需记忆中序遍历迭代模板。
解法2:存储中序序列法(直观,适合进阶优化铺垫)

核心方法:先通过中序遍历将BST节点值存入升序列表,再直接取列表中第k-1个元素(列表索引从0开始),逻辑最直观,且为进阶优化提供基础思路。

代码实现
public int kthSmallest(TreeNode root, int k) {
    List<Integer> list = new ArrayList<>();
    // 中序遍历收集所有节点值(升序)
    inorderCollect(root, list);
    // 取第k-1个元素(k从1开始,列表索引从0开始)
    return list.get(k-1);
}

private void inorderCollect(TreeNode node, List<Integer> list) {
    if (node == null) {
        return;
    }
    inorderCollect(node.left, list);
    list.add(node.val);
    inorderCollect(node.right, list);
}
核心逻辑说明
  1. 两步操作:先收集所有节点值到升序列表,再通过索引直接获取第k小元素;
  2. 直观易懂:无需理解递归计数的细节,仅需知道BST中序遍历是升序;
  3. 进阶铺垫:若需频繁查找第k小元素,可基于此思路优化(如给每个节点记录左子树节点数)。
性能说明
  • 时间复杂度:O(n)(需遍历所有节点);
  • 空间复杂度:O(n)(需存储所有节点值);
  • 优势:
    1. 逻辑最直观,新手易理解;
    2. 代码分步清晰,调试方便;
  • 劣势:
    1. 空间复杂度高,需存储完整序列;
    2. 必须遍历所有节点,即使k很小也无法提前终止。
解法3:进阶优化(应对频繁修改/查询场景)

核心方法:给BST的每个节点增加“左子树节点数”属性(记为size),通过节点的size值快速定位第k小元素,将单次查询时间复杂度降至O(h),适配“频繁修改+频繁查询”的进阶场景。

代码实现(核心思路)
// 定义带大小属性的BST节点
class TreeNodeWithSize {
    int val;
    int size; // 左子树节点数 + 1(自身)
    TreeNodeWithSize left;
    TreeNodeWithSize right;
    TreeNodeWithSize(int val) {
        this.val = val;
        this.size = 1;
    }
}

public int kthSmallestOpt(TreeNodeWithSize root, int k) {
    // 左子树节点数
    int leftSize = root.left == null ? 0 : root.left.size;
    if (k == leftSize + 1) {
        // 当前节点就是第k小元素
        return root.val;
    } else if (k <= leftSize) {
        // 第k小元素在左子树
        return kthSmallestOpt(root.left, k);
    } else {
        // 第k小元素在右子树,k需减去左子树节点数+1
        return kthSmallestOpt(root.right, k - leftSize - 1);
    }
}
核心逻辑说明
  1. 节点属性扩展:每个节点记录size(左子树节点数+自身),插入/删除时同步更新size
  2. 快速定位
    • k == leftSize + 1:当前节点是第k小元素;
    • k <= leftSize:第k小元素在左子树,递归查找左子树的第k小元素;
    • k > leftSize + 1:第k小元素在右子树,递归查找右子树的第k-leftSize-1小元素;
  3. 适配场景:插入/删除时仅需更新路径上节点的size(O(h)),查询时也为O(h),适合频繁修改和查询的场景。
性能说明
  • 单次查询时间复杂度:O(h)(平衡BST中h=logn);
  • 插入/删除时间复杂度:O(h)(同步更新size);
  • 优势:适配进阶场景,大幅提升频繁修改+查询的效率;
  • 劣势:需修改节点结构,实现复杂度较高。

总结

  1. 中序遍历递归计数法(第一次解答):O(h+k)时间+O(h)空间,经典最优解,代码简洁且支持提前终止;
  2. 中序遍历迭代法:O(h+k)时间+O(h)空间,非递归实现,避免栈溢出,工程实践更友好;
  3. 存储中序序列法:O(n)时间+O(n)空间,逻辑直观,适合新手理解;
  4. 进阶优化法:O(h)查询/修改时间,适配频繁修改+查询的进阶场景;
  5. 关键技巧
    • 核心思想:BST中序遍历为升序序列,第k小元素即升序序列的第k个元素;
    • 效率优化:递归/迭代法支持提前终止遍历,比存储完整序列更高效;
    • 进阶思路:频繁修改场景下,给节点增加左子树节点数属性,将查询复杂度降至O(h)。