title: 230. 二叉搜索树中第K小的元素
tags:
notebook: leetcode
题目介绍
题目难度: medium
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/kth-smallest-element-in-a-bst
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目样例
示例 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 <= 104
0 <= Node.val <= 104
由二叉搜索树的特性我们知道左子树总小于根节点,右子树总大于根节点。
这
里
我
们
假
设
左
子
树
为
集
合
L
,
有
r
∈
L
,
右
子
树
为
集
合
R
有
r
∈
R
,
根
节
点
为
r
o
o
t
,
∣
L
∣
为
左
子
树
节
点
个
数
∣
R
∣
为
左
子
树
节
点
个
数
,
这里我们假设左子树为集合L,有r\in L ,右子树为集合R有r \in R ,根节点为root,\vert L \vert 为左子树节点个数 \vert R \vert 为左子树节点个数,
这里我们假设左子树为集合L,有r∈L,右子树为集合R有r∈R,根节点为root,∣L∣为左子树节点个数∣R∣为左子树节点个数,
因 此 ∀ r , l , 都 有 l ≤ r o o t ≤ r . 因此 \forall r,l, 都有 l\le root \le r. 因此∀r,l,都有l≤root≤r.
那么因此我们可以得出结论
对于根节点root有
N
o
d
e
第
k
小
∈
{
L
,
k
≤
∣
L
∣
r
o
o
t
,
e
l
s
e
R
,
k
>
∣
R
∣
Node_{第k小}\in \begin{cases} L& ,k \le \vert L\vert \\\\ root&,else \\\\ R & ,k\gt \vert R\vert \end{cases}
Node第k小∈⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧LrootR,k≤∣L∣,else,k>∣R∣
因此
1.如果当前左子树大于等于k说明在左子树
2.左子树节点数+1等于k说明在右子树
3.如果当前右子树+左子树+1大于等于k说明在右子树
4.如果以上都没满足说明不在当前子树需要回溯。
算法思路 1
这里因为偷懒不想再写一个函数了,所以设计了一个原地的递归。
这里主要问题在于原地,怎么解决深度的计算和返回目标值这两个问题。
朴素的办法就是全局变量记录目标值和深度。
而此处用到的方法是采用了位运算的设计方法。
首先我们先来回顾之前我们思路。(递归一定不能细,理清轮廓和每一步做什么优先即先求得一个大致的数学通解)
1.先递归左子树求得左子树深度。(不要管左子树具体会怎么样,知道这一步调用了一个函数求得深度就够了)
2.如果左子树深度小于等于k那么说明
N
o
d
e
k
Node_{k}
Nodek在左子树中,那么我们这里假定我们获得的结果就是目标值,所以直接返回。(通解多假设,当前步骤正确只要确保边界和首项就可以了)
3.如果当前k==左子树节点数+1说明
N
o
d
e
k
Node_{k}
Nodek为当前子树根节点返回当前节点
4.最后显然递归右子树,返回右子树得到的值。
那么现在我们回顾一下我们最开始的那个问题,怎么处理子树节点数和目标值的冲突。
算法实现 1
显然对于不同的数据我们要设计一个标记来区分两种数据类型。
这里其实运气是比较好的,数据返回1e4,返回类型也为int,而我们也知道int是32位的。
因此我们可以做一个二进制划分
差不多在14个二进位处数据大小是到1e4的。
但由于最终调用我们要保证,至少在找到目标值时我们返回的是目标值。
因此在设计时,如果是节点数我们将二进制第14位置1,反之则置零,从而通过判断14位的值来起到判定是否得到目标值的目的。
这里的话采用异或的运算,因为其他位都是零,所以操作数为零的位置异或还是0.
那么我们的核心思路正式成型了
1.递归左子树获取左子树节点数并异或转成正确的节点数目
2.如果节点数目大于1e4或者目标位为真则当前是
N
o
d
e
k
Node_{k}
Nodek的值直接返回,注意之前异或了一次这里要异或回来做个逆运算转换。
3.如果节点数目+1为k值说明根节点是
N
o
d
e
k
Node_{k}
Nodek,返回根节点(这里按道理不用我说了,小于k的话左子树递归已经得到第二步就返回了。)
4.显然接下来递归右子树,右子树递归时k要更新前面已经有了|L|+1个节点了只需要再找k-|L|-1了(还是要异或转换成节点数)
5.判断左子树节点数,是否大于1e4,小于则没找到返回子树大小|L|+|R|+1,找到则返回。这两个操作都要把标记位置反可以合并
6.现在就是边界判断了,根节点出发显然没有问题,唯一的边界就是叶子节点。我们知道叶子结点子树节点数为一,但不要忘了叶子节点也可能是我们的
N
o
d
e
k
Node_{k}
Nodek,所以我们可以把问题扔给空节点,把叶子结点的问题也放到通解里解决,那显然返回n0 = 0,首项设置完毕,通解正确,首项正确,递归正确
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
const int limit;
public:
int kthSmallest(TreeNode* root, int k) {
if(!root)
return limit;
const int &&left = kthSmallest(root->left,k)^limit;
//求左子树节点数
if(left+1==k){
return root->val;
}
if(left>=limit)
return left^limit;
//当前必然是左子树深度+1小的数字
int &&right = kthSmallest(root->right,k-left-1)^limit;
return (right>=limit?right:(left+right+1))^limit;
//如果大于limit说明目标值返回要不返回节点总和
}
Solution():limit(1<<15){
}
};
算法实现 2
值得一提的是,还有一种思路是同时存储后14位存
N
o
d
e
k
Node_{k}
Nodek,前14位存子树节点数,然后通过移位运算,和按位与运算取余的方式来实现。
不过由于对于
N
o
d
e
k
Node_{k}
Nodek=0与子树节点数为零的情况边界判断困难
1.子树节点数为零,可能只是因为有根节点,并不是找到了
N
o
d
e
k
Node_{k}
Nodek
2.
N
o
d
e
k
Node_{k}
Nodek返回值有可能是0,而返回子树节点数时必然低14位为零
由于我们要保证低14位返回正确的结果,所以我们只能寄希望于高14位存储节点数的部分
既然节点数为零的时候有可能会出现混乱,那我们就上移一层到叶子节点,(还是要讨论叶子结点)当然这里并不单纯是叶子节点,而是所有度小于等于1的节点,因为有一个为空总会返回零导致混乱。
我们可以通过dfs的顺序来做分类讨论
- 左子树为空
1.1如果k=1,显然为 N o d e k Node_{k} Nodek,返回值
1.2如果右子树为空 子树节点数为1返回1<<14
1.3 如果右子树数量为零说明找到返回值,反之返回当前子树的和左移14位 - 右子树为空,这里的判断很简单,前面一半都判断完了,直接返回当前子树节点个数|L|+1
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
const int limit;
public:
int kthSmallest(TreeNode* root, int k) {
if(!root->left){
if(k==1)
return root->val;
if(!root->right)
return 1<<14;
int &&right = kthSmallest(root->right,k-1);
return (right>>14)?((right>>14)+1)<<14:right&limit;
}
int &&left = kthSmallest(root->left,k);
//求左子树节点数
if(!(left>>14))
return left&limit;
if((left>>14)+1==k){
return root->val;
}
if(!root->right)
return ((left>>14)+1)<<14;
//当前必然是左子树深度+1小的数字
int &&right = kthSmallest(root->right,k-(left>>14)-1);
return (right>>14)?(((left>>14)+(right>>14)+1)<<14):right&limit;
//如果大于limit说明目标值返回要不返回节点总和
}
Solution():limit((1<<14)-1){
}
};