LeetCode 剑指 Offer 54. 二叉搜索树的第k大节点
题目描述



题意分析
输入一棵二叉搜索树和一个正整数
k,要求返回树中第 k 大节点的值,只要值本身,不需要返回节点或整个排名序列。「二叉搜索树」这个约束是全题最强的信号:树里任意节点的左子树全部小于它、右子树全部大于它,所以按左根右的中序顺序访问节点,得到的值天然是严格升序的。换句话说,这棵树已经把「排序」这件事做完了,我们要做的只是从这条有序序列里取出正确的位置。
另一个必须先想清楚的点是「第 k 大」而不是第 k 小。升序序列里第 k 大位于倒数第 k 个位置,如果按升序去数,必须先知道节点总数
n,再去取第n - k + 1个,等于要遍历两遍。反过来,如果能让访问顺序变成从大到小,那么第 k 大就是「从头数的第 k 个」,一遍即可。边界方面:题目保证
1 <= k <= n,也就是k一定落在树内,不必处理 k 越界返回什么;但节点值可能重复的题面变体要留意,本题按互不相同处理,排名即出现顺序。树可能退化成一条链(每个节点只有右孩子),此时树高等于节点数,递归深度是最坏情况,需要在复杂度里体现。
解法:反向中序遍历计数
核心思路
问题关键:BST 的中序遍历是升序,但题目要第
k大。如果先遍历全部节点再从数组尾部取值,会多用 $O(n)$ 空间,也无法在找到答案后停止。为什么选反向中序:把“左、根、右”改为“右、根、左”,访问顺序就从大到小。使用显式栈迭代遍历,每弹出一个节点就令
k--,k == 0时当前值即为答案,无需统计节点总数或做下标换算。不变量:每次沿当前右链压栈完成后,栈顶就是剩余节点中的最大值;已弹出的
t个节点恰好是整棵树最大的t个。弹出节点后再转向它的左子树,因为左子树中的值都比它小,却大于更早祖先左侧尚未发现的节点。正确性:由 BST 的大小关系,每次弹出的值严格递减,第
k次弹出的节点必然是第k大。题目保证k合法,所以循环一定能在栈耗尽前返回。
解题步骤
- 从根开始沿右链压栈,直到当前节点为空。
- 弹出栈顶,这就是降序遍历的下一个节点;令
k--。- 若
k == 0,立即返回当前节点值;否则转向它的左孩子。- 重复“压入右链—弹栈访问—转向左子树”。
口述样例:BST
[5,3,7,2,4,6,8]的反向中序依次弹出8、7、6...,所以k = 3时返回6,其余更小节点不必访问。
代码实现
class Solution {
public int kthLargest(TreeNode root, int k) {
Deque<TreeNode> stack = new ArrayDeque<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.right;
}
root = stack.pop();
if (--k == 0) {
return root.val;
}
root = root.left;
}
return -1;
}
}
func kthLargest(root *TreeNode, k int) int {
stack := make([]*TreeNode, 0)
for root != nil || len(stack) > 0 {
for root != nil {
stack = append(stack, root)
root = root.Right
}
root = stack[len(stack)-1]
stack = stack[:len(stack)-1]
k--
if k == 0 {
return root.Val
}
root = root.Left
}
return -1
}
复杂度分析
- 时间复杂度:$O(h + k)$。初始压入最右链最多走
h层,之后访问前k个降序节点;最坏仍为 $O(n)$。- 空间复杂度:$O(h)$。显式栈最多保存一条根到叶路径;平衡树为 $O(\log n)$,退化树为 $O(n)$。
关键点总结
- BST 的左根右中序为升序,右根左反向中序为降序;第
k大直接数降序第k个。k--必须发生在节点出栈时,因为出栈才表示真正访问了节点。- 找到答案立即返回,避免为了一个排名遍历整棵树。
- 若要频繁查询第
k大,可在节点中维护子树大小,使单次查询降为 $O(h)$;代价是更新树时同步维护计数。
易错点总结
- 遍历方向写成左根右:样例树
k = 3会返回第 3 小的4,而不是第 3 大的6。- 先访问节点再走右子树:会破坏降序顺序,最大值不再最先被计数。
- 入栈时就递减
k:入栈只是发现节点,尚未完成右侧更大节点的访问,排名会提前。- 弹栈后忘记转向左孩子:会漏掉当前节点与其祖先之间的值。
- 已采用降序遍历后又取第
n-k+1个:发生二次换算,结果反而变成第k小。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 98. 验证二叉搜索树 | 中等 | 中序判严格递增,或上下界向下传递 |
| 230. 二叉搜索树中第 K 小的元素 | 中等 | 本题的镜像,左根右中序数第 k 个即第 k 小 |
| 426. 将二叉搜索树转化为排序的双向链表 | 中等 | 中序过程中记录前驱,原地改指针成循环双链表 |
| 538. 把二叉搜索树转换为累加树 | 中等 | 同样走右根左,但累加后缀和而非计数 |
| 897. 递增顺序搜索树 | 简单 | 中序重排成只有右孩子的单链树 |
| 1038. 从二叉搜索树到更大和树 | 中等 | 与 538 等价题面,反序中序前缀累加 |
| LCR 052. 递增顺序搜索树 | 简单 | 897 的 LCR 版本,考察中序拼接时的断链处理 |
| LCR 054. 把二叉搜索树转换为累加树 | 中等 | 538 的 LCR 版本,反序遍历中维护运行和 |
| 剑指 Offer 36. 二叉搜索树与双向链表 | 中等 | 426 的剑指版本,额外要求首尾相连 |
| 面试题 04.05. 合法二叉搜索树 | 中等 | 98 的等价题,注意相等值与整型边界 |
| 面试题 17.12. BiNode | 简单 | 中序展开为右斜链,要求 $O(1)$ 额外空间原地做 |