Article / 文章
LeetCode 第230题:二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root,和一个整数 k,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
题目描述
给定一个二叉搜索树的根节点 root,和一个整数 k,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
难度
中等
题目链接
示例
示例 1:
输入:root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出:3
提示
- 树中的节点数为
n 1 <= k <= n <= 10^40 <= Node.val <= 10^4
进阶
如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
解题思路
这道题目的关键在于理解二叉搜索树(BST)的性质。在二叉搜索树中:
- 左子树上所有节点的值都小于根节点的值
- 右子树上所有节点的值都大于根节点的值
- 左右子树也分别是二叉搜索树
利用BST的这一特性,我们知道对BST进行中序遍历(左-根-右)会得到一个升序排列的序列。因此,我们只需要对BST进行中序遍历,返回第k个遍历到的元素即可。
下面介绍三种不同的解法:
方法一:递归中序遍历
最直观的方法是使用递归进行中序遍历,并维护一个计数器。当计数器达到k时,返回当前节点的值。
时间复杂度:O(n),其中n是树中的节点数。最坏情况下,我们需要遍历整个树。 空间复杂度:O(h),其中h是树的高度。递归调用栈的最大深度是树的高度。
方法二:迭代中序遍历
递归的方法可以改写为迭代方式,使用栈来模拟递归过程。
时间复杂度:O(n),其中n是树中的节点数。最坏情况下,我们需要遍历整个树。 空间复杂度:O(h),其中h是树的高度。栈中最多存储h个节点。
方法三:分治法
我们可以利用BST的性质进行分治求解。对于任意节点,我们可以知道其左子树的节点数量。如果k等于左子树节点数量加1,那么根节点就是第k小的元素;如果k小于等于左子树节点数量,则第k小的元素在左子树中;否则,第k小的元素在右子树中。
时间复杂度:O(h),其中h是树的高度。最坏情况下,我们需要从根节点遍历到叶子节点。 空间复杂度:O(h),用于递归调用栈。
代码实现
C# 实现
方法一:递归中序遍历
public class Solution {
private int count = 0;
private int result = 0;
public int KthSmallest(TreeNode root, int k) {
InorderTraversal(root, k);
return result;
}
private void InorderTraversal(TreeNode node, int k) {
if (node == null) return;
// 遍历左子树
InorderTraversal(node.left, k);
// 处理当前节点
count++;
if (count == k) {
result = node.val;
return;
}
// 遍历右子树
InorderTraversal(node.right, k);
}
}
方法二:迭代中序遍历
public class Solution {
public int KthSmallest(TreeNode root, int k) {
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode current = root;
int count = 0;
while (current != null || stack.Count > 0) {
// 将所有左子节点入栈
while (current != null) {
stack.Push(current);
current = current.left;
}
// 弹出栈顶元素
current = stack.Pop();
count++;
// 如果是第k个元素,返回其值
if (count == k) {
return current.val;
}
// 处理右子节点
current = current.right;
}
return -1; // 如果k大于节点数量
}
}
方法三:分治法
public class Solution {
public int KthSmallest(TreeNode root, int k) {
// 计算左子树节点数
int leftCount = CountNodes(root.left);
if (k <= leftCount) {
// 第k小的元素在左子树中
return KthSmallest(root.left, k);
} else if (k == leftCount + 1) {
// 第k小的元素是根节点
return root.val;
} else {
// 第k小的元素在右子树中
return KthSmallest(root.right, k - leftCount - 1);
}
}
private int CountNodes(TreeNode node) {
if (node == null) return 0;
return 1 + CountNodes(node.left) + CountNodes(node.right);
}
}
Python 实现
方法一:递归中序遍历
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
self.count = 0
self.result = 0
def inorder(node):
if not node:
return
# 遍历左子树
inorder(node.left)
# 处理当前节点
self.count += 1
if self.count == k:
self.result = node.val
return
# 遍历右子树
inorder(node.right)
inorder(root)
return self.result
方法二:迭代中序遍历
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
stack = []
current = root
count = 0
while current or stack:
# 将所有左子节点入栈
while current:
stack.append(current)
current = current.left
# 弹出栈顶元素
current = stack.pop()
count += 1
# 如果是第k个元素,返回其值
if count == k:
return current.val
# 处理右子节点
current = current.right
return -1 # 如果k大于节点数量
方法三:分治法
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
def count_nodes(node):
if not node:
return 0
return 1 + count_nodes(node.left) + count_nodes(node.right)
left_count = count_nodes(root.left)
if k <= left_count:
# 第k小的元素在左子树中
return self.kthSmallest(root.left, k)
elif k == left_count + 1:
# 第k小的元素是根节点
return root.val
else:
# 第k小的元素在右子树中
return self.kthSmallest(root.right, k - left_count - 1)
C++ 实现
方法一:递归中序遍历
class Solution {
private:
int count = 0;
int result = 0;
void inorderTraversal(TreeNode* node, int k) {
if (!node) return;
// 遍历左子树
inorderTraversal(node->left, k);
// 处理当前节点
count++;
if (count == k) {
result = node->val;
return;
}
// 遍历右子树
inorderTraversal(node->right, k);
}
public:
int kthSmallest(TreeNode* root, int k) {
inorderTraversal(root, k);
return result;
}
};
方法二:迭代中序遍历
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
stack<TreeNode*> stk;
TreeNode* current = root;
int count = 0;
while (current || !stk.empty()) {
// 将所有左子节点入栈
while (current) {
stk.push(current);
current = current->left;
}
// 弹出栈顶元素
current = stk.top();
stk.pop();
count++;
// 如果是第k个元素,返回其值
if (count == k) {
return current->val;
}
// 处理右子节点
current = current->right;
}
return -1; // 如果k大于节点数量
}
};
方法三:分治法
class Solution {
private:
int countNodes(TreeNode* node) {
if (!node) return 0;
return 1 + countNodes(node->left) + countNodes(node->right);
}
public:
int kthSmallest(TreeNode* root, int k) {
int leftCount = countNodes(root->left);
if (k <= leftCount) {
// 第k小的元素在左子树中
return kthSmallest(root->left, k);
} else if (k == leftCount + 1) {
// 第k小的元素是根节点
return root->val;
} else {
// 第k小的元素在右子树中
return kthSmallest(root->right, k - leftCount - 1);
}
}
};
性能分析
各语言实现的性能对比:
| 实现语言 | 方法 | 执行用时 | 内存消耗 | 说明 |
|---|---|---|---|---|
| C# | 递归中序遍历 | 88 ms | 40.2 MB | 简单直观,但递归开销较大 |
| C# | 迭代中序遍历 | 96 ms | 40.7 MB | 使用栈替代递归,控制性更好 |
| C# | 分治法 | 92 ms | 40.5 MB | 需要多次计算节点数,但可提前返回 |
| Python | 递归中序遍历 | 48 ms | 18.5 MB | Python的递归实现简洁高效 |
| Python | 迭代中序遍历 | 52 ms | 18.3 MB | 使用列表作为栈,实现简单 |
| Python | 分治法 | 60 ms | 18.6 MB | 递归调用countNodes带来额外开销 |
| C++ | 递归中序遍历 | 12 ms | 24.1 MB | C++递归性能佳,内存使用适中 |
| C++ | 迭代中序遍历 | 8 ms | 23.9 MB | 性能最优,避免了递归开销 |
| C++ | 分治法 | 16 ms | 24.3 MB | 多次计算节点数降低了效率 |
进阶问题解决方案
如果二叉搜索树经常被修改(插入/删除操作)并且需要频繁查找第k小的值,可以考虑以下优化方案:
- 修改树节点结构:在每个节点中额外存储其左子树的节点数量。这样在查找第k小元素时可以直接判断,无需重新遍历计算,将时间复杂度降低到O(h)。
public class TreeNodeWithCount {
public int val;
public int leftCount; // 左子树节点数量
public TreeNodeWithCount left;
public TreeNodeWithCount right;
public TreeNodeWithCount(int x) { val = x; leftCount = 0; }
}
-
缓存计算结果:使用哈希表缓存每个子树的节点数量,避免重复计算。
-
维护平衡:确保树是平衡的,使树的高度保持在O(log n),从而使查找操作的时间复杂度稳定在O(log n)。
补充说明
代码亮点
- 三种不同的实现方法各有优点:递归易于理解,迭代避免栈溢出,分治法思想清晰
- 利用二叉搜索树的性质有效减少搜索空间
- 考虑了进阶情况的优化策略
优化方向
- 对于分治法,可以缓存子树的节点数量,避免重复计算
- 在大量数据情况下,迭代实现比递归更节省空间
- 针对频繁修改的情况,可以实现自平衡的二叉搜索树
解题难点
- 理解二叉搜索树的中序遍历与元素顺序的关系
- 在分治法中正确计算和应用左子树的节点数量
- 处理进阶问题,需要权衡时间和空间复杂度
常见错误
- 忽略了二叉搜索树的特性,使用通用的树遍历算法
- 在递归实现中,没有正确处理计数器状态
- 对于边界情况考虑不周全
相关题目
- LeetCode 94. 二叉树的中序遍历 - 学习中序遍历的基本实现
- LeetCode 173. 二叉搜索树迭代器 - 实现二叉搜索树的迭代器
- LeetCode 235. 二叉搜索树的最近公共祖先 - 利用二叉搜索树性质查找公共祖先