leetcode230. 二叉搜索树中第 K 小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 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 <= 104
  • 0 <= Node.val <= 104

进阶: 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

步骤1:问题定义与分析

题目要求:

给定一个二叉搜索树(BST)的根节点 root,和一个整数 k,要求找出二叉搜索树中的第 k 小的元素。

  • 输入

    • root:一个二叉搜索树的根节点。
    • k:一个整数,表示要查找的第 k 小的元素,1 <= k <= n,其中 n 是树的节点数。
  • 输出

    • 返回树中第 k 小的元素。
边界条件和约束:
  • 1 <= k <= n <= 10^4:树中节点的数目最多为 10,000。
  • 0 <= Node.val <= 10^4:节点值的范围在 0 到 10,000 之间。
树的性质:
  • 二叉搜索树(BST)的特点是对于每一个节点,左子树的所有节点值小于当前节点值,右子树的所有节点值大于当前节点值。
潜在的问题:
  • 对于大规模的数据,必须优化算法,以便避免时间复杂度过高。

步骤2:问题分解与解决方案

方法1:中序遍历

二叉搜索树的中序遍历(递归或迭代)会生成一个按升序排列的节点值序列。因此,我们可以通过中序遍历 BST 来依次访问每一个节点,直到找到第 k 小的节点。

算法步骤:
  1. 对于给定的 BST,进行中序遍历(递归或迭代)。
  2. 在遍历过程中,维护一个计数器,记录访问的节点数量。当计数器等于 k 时,返回当前节点的值。
时间复杂度分析:
  • 时间复杂度 :中序遍历的时间复杂度为 O(n),其中 n 是树的节点数。由于我们只需要遍历到第 k 个节点,因此最坏情况下需要遍历整个树,复杂度仍为 O(n)。
  • 空间复杂度 :递归栈的空间复杂度为 O(h),其中 h 是树的高度。最坏情况下,树是退化为链表时,高度为 n,空间复杂度为 O(n)。
优化建议:
  • 如果树非常大,且 k 很小,可以通过在遍历过程中提前停止来减少不必要的操作。
  • 但最主要的瓶颈还是中序遍历的复杂度。如果树非常深且频繁更新,可能需要其他优化方法。
方法2:基于堆的数据结构优化

如果树经常被修改(插入/删除),我们可以利用一个 最小堆最大堆 来优化查找第 k 小元素的操作。

算法步骤:
  1. 使用堆来存储 BST 中的元素。可以使用最小堆来在每次查询时高效地提取出最小的元素。
  2. 插入和删除时维护堆的有序性,确保可以在 O(log n) 的时间内更新。
时间复杂度分析:
  • 插入/删除操作:每次插入或删除的时间复杂度为 O(log n)。
  • 查询第 k 小元素:每次查询的时间复杂度为 O(k)。
  • 这种方法适合树频繁更新但需要快速查询的场景。
方法3:Morris 中序遍历(空间优化)

Morris 中序遍历利用了二叉树的线索化技术,可以在 O(1) 的空间复杂度下进行中序遍历。

算法步骤:
  1. 通过 Morris 遍历实现对树的中序遍历,不需要递归栈或额外的空间。
  2. 在遍历过程中,维护一个计数器,直到找到第 k 小的节点。
时间复杂度分析:
  • 时间复杂度:O(n),因为每个节点最多被访问两次。
  • 空间复杂度:O(1),不需要额外的栈空间。
方法4:平衡二叉搜索树

如果树经常被修改且需要频繁查询第 k 小的元素,可以考虑使用 平衡二叉搜索树(如 AVL 树、红黑树)。这类树可以通过平衡调整确保查询和修改操作的时间复杂度为 O(log n)。

算法步骤:
  1. 使用平衡二叉搜索树,在每个节点保存一个额外的字段记录其左子树的节点数。
  2. 对于查询操作,通过比较左子树的大小来决定向左子树或右子树移动。
时间复杂度分析:
  • 插入/删除:O(log n)。
  • 查询第 k 小元素:O(log n),通过利用左子树的节点数快速定位。

步骤3:C++ 代码实现

这里我们采用中序遍历的方法来实现,代码如下:

cpp 复制代码
/**
 * 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 {
public:
// 中序遍历的递归实现
void inOrder(TreeNode* root, int k, int& count, int& result) {
    if (!root) return;

    // 遍历左子树
    inOrder(root->left, k, count, result);
    
    // 访问当前节点
    count++;
    if (count == k) {
        result = root->val;
        return;
    }

    // 遍历右子树
    inOrder(root->right, k, count, result);
}

// 查找第k小的元素
int kthSmallest(TreeNode* root, int k) {
    int count = 0;
    int result = -1;
    inOrder(root, k, count, result);
    return result;
}
};
代码注释:
  • inOrder 函数实现了中序遍历,count 用来计数遍历到的节点数,result 用来存储第 k 小的节点的值。
  • 在遍历过程中,当 count == k 时,记录当前节点的值并停止遍历。
  • kthSmallest 函数调用 inOrder 来查找第 k 小的元素。

步骤4:通过解决这个问题获得的启发

  • 这个问题让我们深刻认识到如何利用数据结构的特性(如中序遍历)和树的结构(如二叉搜索树的顺序性质)来优化查询操作。
  • 进一步的优化方向是结合堆或平衡二叉树来处理频繁插入/删除操作的场景,这对于某些实时更新数据场景尤其重要。

步骤5:实际应用分析

应用场景:
  1. 数据库查询优化 : 在数据库中,许多查询涉及到排序和查找第 k 小或第 k 大的元素。通过利用中序遍历的思想,可以优化数据库中对于排序查询的效率,特别是在没有完全排序的情况下。

  2. 数据流分析 : 在实时数据流分析中,需要快速找到前 k 大的数据项。比如在金融交易系统中,每天的交易量会产生海量的数据,快速找到最大交易量的前 k 个产品或者客户。

实现方法:
  • 对于实时流数据,可以使用堆来维护当前最大的 k 个元素,堆的操作可以在 O(log k) 时间内完成更新。这样,当数据流不断变化时,依然可以快速找到当前流中的第 k 小元素。
相关推荐
winstongit9 分钟前
捷联惯导原理和算法预备知识
算法·机器人
£suPerpanda1 小时前
P3916 图的遍历(Tarjan缩点和反向建边)
数据结构·c++·算法·深度优先·图论
IT古董1 小时前
【机器学习】机器学习的基本分类-监督学习-决策树-C4.5 算法
人工智能·学习·算法·决策树·机器学习·分类
m0_694938011 小时前
Leetcode打卡:棋盘上有效移动组合的数目
算法·leetcode·职场和发展
kitesxian1 小时前
Leetcode543. 二叉树的直径(HOT100)
算法·深度优先
我是博博啦1 小时前
matlab中disp,fprintf,sprintf,display,dlmwrite输出函数之间的区别
算法·matlab
生信宝典1 小时前
分而治之—利用决策树和规则进行分类
算法·决策树·分类
pzx_0012 小时前
【时间序列预测】基于Pytorch实现CNN_LSTM算法
人工智能·pytorch·python·算法·cnn·lstm
搏博4 小时前
路径规划之启发式算法之四:蚁群算法(Ant Colony Optimization,ACO)
人工智能·算法·机器学习
鸽鸽程序猿4 小时前
【算法】【优选算法】位运算(下)
算法·位运算