leetcode 230二叉搜索树的第k小的元素 递归设计的简单思想

6 篇文章 0 订阅
4 篇文章 0 订阅

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,rLRrRrootLR,

因 此 ∀ r , l , 都 有 l ≤ r o o t ≤ r . 因此 \forall r,l, 都有 l\le root \le r. r,l,lrootr.

那么因此我们可以得出结论
对于根节点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} NodekLrootR,kL,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.1如果k=1,显然为 N o d e k Node_{k} Nodek,返回值
    1.2如果右子树为空 子树节点数为1返回1<<14
    1.3 如果右子树数量为零说明找到返回值,反之返回当前子树的和左移14位
  2. 右子树为空,这里的判断很简单,前面一半都判断完了,直接返回当前子树节点个数|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){
        
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值