力扣解题-230. 二叉搜索树中第 K 小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(k 从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入: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小元素的核心规律:
- BST关键特性:中序遍历BST得到的节点值序列是严格升序的(如示例2中序遍历结果为[1,2,3,4,5,6]);
- 第k小定义:升序序列中第k个元素(从1开始计数)即为第k小元素(如示例2中k=3时,第3个元素是3);
- 算法核心:中序遍历过程中维护一个计数器,计数达到k时立即记录当前节点值并提前终止遍历(避免无效遍历)。
具体执行逻辑
- 初始化变量:
ans:存储最终结果(第k小元素值),初始为0;count:中序遍历的计数变量,初始为0(记录当前遍历到第几个元素);
- 中序遍历递归:
- 递归终止条件:当前节点
node == null,直接返回; - 先递归遍历左子树(中序遍历“左”,优先访问更小的元素);
- 计数+1:
count++(当前节点是升序序列中的第count个元素); - 终止判断:若
count == k,将ans赋值为当前节点值,直接返回(无需继续遍历); - 递归遍历右子树(中序遍历“右”,仅当count<k时需要继续);
- 递归终止条件:当前节点
- 返回结果:遍历完成后,
ans即为第k小元素值。
执行流程可视化(以示例2 root=[5,3,6,2,4,null,null,1]、k=3为例)
| 遍历节点 | count值 | 计数操作 | 终止判断 | ans更新 | 后续操作 |
|---|---|---|---|---|---|
| 1 | 0→1 | count++ | 1≠3 | 0 | 继续遍历 |
| 2 | 1→2 | count++ | 2≠3 | 0 | 继续遍历 |
| 3 | 2→3 | count++ | 3==3 | 3 | 直接返回,终止遍历 |
最终返回ans=3,符合示例2结果(无需遍历4、5、6节点)。 |
关键细节说明
- 提前终止遍历:当
count == k时立即返回,无需遍历右子树和剩余节点,大幅减少无效遍历(如示例2中k=3时,遍历到3后直接终止); - 计数时机:必须在遍历左子树后、处理右子树前计数,符合中序遍历“左→根→右”的顺序;
- 变量作用域:
ans和count定义为类成员变量,保证递归过程中值的连续性; - 边界处理:题目保证1≤k≤n,无需处理k超出范围的场景。
性能说明
- 时间复杂度:最优O(h+k)(平衡BST中h=logn,k较小时效率极高),最坏O(n)(斜树且k=n时需遍历所有节点);
- 空间复杂度:O(h)(递归栈深度等于树的高度);
- 优势:
- 利用BST特性,无需存储完整序列,空间效率最优;
- 支持提前终止遍历,时间效率高于“存储完整序列后取第k个”的方法;
- 递归逻辑简洁,贴合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,计数等于k时立即返回,无需遍历剩余节点;
- 无额外成员变量:所有变量为局部变量,代码封装性更好,无线程安全问题。
性能说明
- 时间复杂度:O(h+k)(与递归法一致);
- 空间复杂度:O(h)(栈深度等于树的高度);
- 优势:
- 非递归实现,避免极端斜树导致的递归栈溢出;
- 计数达到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);
}
核心逻辑说明
- 两步操作:先收集所有节点值到升序列表,再通过索引直接获取第k小元素;
- 直观易懂:无需理解递归计数的细节,仅需知道BST中序遍历是升序;
- 进阶铺垫:若需频繁查找第k小元素,可基于此思路优化(如给每个节点记录左子树节点数)。
性能说明
- 时间复杂度:O(n)(需遍历所有节点);
- 空间复杂度:O(n)(需存储所有节点值);
- 优势:
- 逻辑最直观,新手易理解;
- 代码分步清晰,调试方便;
- 劣势:
- 空间复杂度高,需存储完整序列;
- 必须遍历所有节点,即使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);
}
}
核心逻辑说明
- 节点属性扩展:每个节点记录
size(左子树节点数+自身),插入/删除时同步更新size; - 快速定位:
- 若
k == leftSize + 1:当前节点是第k小元素; - 若
k <= leftSize:第k小元素在左子树,递归查找左子树的第k小元素; - 若
k > leftSize + 1:第k小元素在右子树,递归查找右子树的第k-leftSize-1小元素;
- 若
- 适配场景:插入/删除时仅需更新路径上节点的
size(O(h)),查询时也为O(h),适合频繁修改和查询的场景。
性能说明
- 单次查询时间复杂度:O(h)(平衡BST中h=logn);
- 插入/删除时间复杂度:O(h)(同步更新size);
- 优势:适配进阶场景,大幅提升频繁修改+查询的效率;
- 劣势:需修改节点结构,实现复杂度较高。
总结
- 中序遍历递归计数法(第一次解答):O(h+k)时间+O(h)空间,经典最优解,代码简洁且支持提前终止;
- 中序遍历迭代法:O(h+k)时间+O(h)空间,非递归实现,避免栈溢出,工程实践更友好;
- 存储中序序列法:O(n)时间+O(n)空间,逻辑直观,适合新手理解;
- 进阶优化法:O(h)查询/修改时间,适配频繁修改+查询的进阶场景;
- 关键技巧:
- 核心思想:BST中序遍历为升序序列,第k小元素即升序序列的第k个元素;
- 效率优化:递归/迭代法支持提前终止遍历,比存储完整序列更高效;
- 进阶思路:频繁修改场景下,给节点增加左子树节点数属性,将查询复杂度降至O(h)。