二叉搜索树中第K小的元素[中等]

优质博文:IT-BLOG-CN

一、题目

给定一个二叉搜索树的根节点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】中序遍历: 二叉搜索树具有如下性质:
  ● 结点的左子树只包含小于当前结点的数。
  ● 结点的右子树只包含大于当前结点的数。
  ● 所有左子树和右子树自身必须也是二叉搜索树。

二叉树的中序遍历即按照访问左子树——根结点——右子树的方式遍历二叉树;在访问其左子树和右子树时,我们也按照同样的方式遍历;直到遍历完整棵树。

思路和算法: 因为二叉搜索树和中序遍历的性质,所以二叉搜索树的中序遍历是按照键增加的顺序进行的。于是,我们可以通过中序遍历找到第k个最小元素。

class Solution {public int kthSmallest(TreeNode root, int k) {Deque<TreeNode> stack = new ArrayDeque<TreeNode>();while (root != null || !stack.isEmpty()) {while (root != null) {stack.push(root);root = root.left;}root = stack.pop();--k;if (k == 0) {break;}root = root.right;}return root.val;}
}

时间复杂度: 时间复杂度:O(H+k),其中H是树的高度。在开始遍历之前,我们需要O(H)到达叶结点。当树是平衡树时,时间复杂度取得最小值O(log⁡N+k);当树是线性树(树中每个结点都只有一个子结点或没有子结点)时,时间复杂度取得最大值O(N+k)
空间复杂度: O(H),栈中最多需要存储H个元素。当树是平衡树时,空间复杂度取得最小值O(log⁡N);当树是线性树时,空间复杂度取得最大值O(N)

【2】记录子树的结点数: 如果你需要频繁地查找第k小的值,你将如何优化算法?

思路和算法: 在方法一中,我们之所以需要中序遍历前k个元素,是因为我们不知道子树的结点数量,不得不通过遍历子树的方式来获知。因此,我们可以记录下以每个结点为根结点的子树的结点数,并在查找第k小的值时,使用如下方法搜索:
  ● 令node等于根结点,开始搜索。
  ● 对当前结点node进行如下操作:
    ○ 如果node的左子树的结点数left小于k−1,则第k小的元素一定在node的右子树中,令node等于其的右子结点,k等于k−left−1,并继续搜索;
    ○ 如果node的左子树的结点数left等于k−1,则第k小的元素即为node,结束搜索并返回node即可;
    ○ 如果node的左子树的结点数left大于k−1,则第k小的元素一定在node的左子树中,令node等于其左子结点,并继续搜索。

在实现中,我们既可以将以每个结点为根结点的子树的结点数存储在结点中,也可以将其记录在哈希表中。

class Solution {public int kthSmallest(TreeNode root, int k) {MyBst bst = new MyBst(root);return bst.kthSmallest(k);}
}class MyBst {TreeNode root;Map<TreeNode, Integer> nodeNum;public MyBst(TreeNode root) {this.root = root;this.nodeNum = new HashMap<TreeNode, Integer>();countNodeNum(root);}// 返回二叉搜索树中第k小的元素public int kthSmallest(int k) {TreeNode node = root;while (node != null) {int left = getNodeNum(node.left);if (left < k - 1) {node = node.right;k -= left + 1;} else if (left == k - 1) {break;} else {node = node.left;}}return node.val;}// 统计以node为根结点的子树的结点数private int countNodeNum(TreeNode node) {if (node == null) {return 0;}nodeNum.put(node, 1 + countNodeNum(node.left) + countNodeNum(node.right));return nodeNum.get(node);}// 获取以node为根结点的子树的结点数private int getNodeNum(TreeNode node) {return nodeNum.getOrDefault(node, 0);}
}

时间复杂度: 预处理的时间复杂度为O(N),其中N是树中结点的总数;我们需要遍历树中所有结点来统计以每个结点为根结点的子树的结点数。搜索的时间复杂度为O(H),其中H是树的高度;当树是平衡树时,时间复杂度取得最小值O(log⁡N);当树是线性树时,时间复杂度取得最大值O(N)
空间复杂度: O(N),用于存储以每个结点为根结点的子树的结点数。

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

方法三需要先掌握 平衡二叉搜索树(AVL树) 的知识。平衡二叉搜索树具有如下性质:
  ● 平衡二叉搜索树中每个结点的左子树和右子树的高度最多相差1
  ● 平衡二叉搜索树的子树也是平衡二叉搜索树;
  ● 一棵存有 nnn 个结点的平衡二叉搜索树的高度是O(log⁡n)

思路和算法: 我们注意到在方法二中搜索二叉搜索树的时间复杂度为O(H),其中H是树的高度;当树是平衡树时,时间复杂度取得最小值O(log⁡N)。因此,我们在记录子树的结点数的基础上,将二叉搜索树转换为平衡二叉搜索树,并在插入和删除操作中维护它的平衡状态。

class Solution {public int kthSmallest(TreeNode root, int k) {// 中序遍历生成数值列表List<Integer> inorderList = new ArrayList<Integer>();inorder(root, inorderList);// 构造平衡二叉搜索树AVL avl = new AVL(inorderList);// 模拟1000次插入和删除操作int[] randomNums = new int[1000];Random random = new Random();for (int i = 0; i < 1000; ++i) {randomNums[i] = random.nextInt(10001);avl.insert(randomNums[i]);}shuffle(randomNums); // 列表乱序for (int i = 0; i < 1000; ++i) {avl.delete(randomNums[i]);}return avl.kthSmallest(k);}private void inorder(TreeNode node, List<Integer> inorderList) {if (node.left != null) {inorder(node.left, inorderList);}inorderList.add(node.val);if (node.right != null) {inorder(node.right, inorderList);}}private void shuffle(int[] arr) {Random random = new Random();int length = arr.length;for (int i = 0; i < length; i++) {int randIndex = random.nextInt(length);int temp = arr[i];arr[i] = arr[randIndex];arr[randIndex] = temp;}}
}// 平衡二叉搜索树(AVL树):允许重复值
class AVL {Node root;// 平衡二叉搜索树结点class Node {int val;Node parent;Node left;Node right;int size;int height;public Node(int val) {this(val, null);}public Node(int val, Node parent) {this(val, parent, null, null);}public Node(int val, Node parent, Node left, Node right) {this.val = val;this.parent = parent;this.left = left;this.right = right;this.height = 0; // 结点高度:以node为根节点的子树的高度(高度定义:叶结点的高度是0)this.size = 1; // 结点元素数:以node为根节点的子树的节点总数}}public AVL(List<Integer> vals) {if (vals != null) {this.root = build(vals, 0, vals.size() - 1, null);}}// 根据vals[l:r]构造平衡二叉搜索树 -> 返回根结点private Node build(List<Integer> vals, int l, int r, Node parent) {int m = (l + r) >> 1;Node node = new Node(vals.get(m), parent);if (l <= m - 1) {node.left = build(vals, l, m - 1, node);}if (m + 1 <= r) {node.right = build(vals, m + 1, r, node);}recompute(node);return node;}// 返回二叉搜索树中第k小的元素public int kthSmallest(int k) {Node node = root;while (node != null) {int left = getSize(node.left);if (left < k - 1) {node = node.right;k -= left + 1;} else if (left == k - 1) {break;} else {node = node.left;}}return node.val;}public void insert(int v) {if (root == null) {root = new Node(v);} else {// 计算新结点的添加位置Node node = subtreeSearch(root, v);boolean isAddLeft = v <= node.val; // 是否将新结点添加到node的左子结点if (node.val == v) { // 如果值为v的结点已存在if (node.left != null) { // 值为v的结点存在左子结点,则添加到其左子树的最右侧node = subtreeLast(node.left);isAddLeft = false;} else { // 值为v的结点不存在左子结点,则添加到其左子结点isAddLeft = true;}}// 添加新结点Node leaf = new Node(v, node);if (isAddLeft) {node.left = leaf;} else {node.right = leaf;}rebalance(leaf);}}// 删除值为v的结点 -> 返回是否成功删除结点public boolean delete(int v) {if (root == null) {return false;}Node node = subtreeSearch(root, v);if (node.val != v) { // 没有找到需要删除的结点return false;}// 处理当前结点既有左子树也有右子树的情况// 若左子树比右子树高度低,则将当前结点替换为右子树最左侧的结点,并移除右子树最左侧的结点// 若右子树比左子树高度低,则将当前结点替换为左子树最右侧的结点,并移除左子树最右侧的结点if (node.left != null && node.right != null) {Node replacement = null;if (node.left.height <= node.right.height) {replacement = subtreeFirst(node.right);} else {replacement = subtreeLast(node.left);}node.val = replacement.val;node = replacement;}Node parent = node.parent;delete(node);rebalance(parent);return true;}// 删除结点p并用它的子结点代替它,结点p至多只能有1个子结点private void delete(Node node) {if (node.left != null && node.right != null) {return;// throw new Exception("Node has two children");}Node child = node.left != null ? node.left : node.right;if (child != null) {child.parent = node.parent;}if (node == root) {root = child;} else {Node parent = node.parent;if (node == parent.left) {parent.left = child;} else {parent.right = child;}}node.parent = node;}// 在以node为根结点的子树中搜索值为v的结点,如果没有值为v的结点,则返回值为v的结点应该在的位置的父结点private Node subtreeSearch(Node node, int v) {if (node.val < v && node.right != null) {return subtreeSearch(node.right, v);} else if (node.val > v && node.left != null) {return subtreeSearch(node.left, v);} else {return node;}}// 重新计算node结点的高度和元素数private void recompute(Node node) {node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));node.size = 1 + getSize(node.left) + getSize(node.right);}// 从node结点开始(含node结点)逐个向上重新平衡二叉树,并更新结点高度和元素数private void rebalance(Node node) {while (node != null) {int oldHeight = node.height, oldSize = node.size;if (!isBalanced(node)) {node = restructure(tallGrandchild(node));recompute(node.left);recompute(node.right);}recompute(node);if (node.height == oldHeight && node.size == oldSize) {node = null; // 如果结点高度和元素数都没有变化则不需要再继续向上调整} else {node = node.parent;}}}// 判断node结点是否平衡private boolean isBalanced(Node node) {return Math.abs(getHeight(node.left) - getHeight(node.right)) <= 1;}// 获取node结点更高的子树private Node tallChild(Node node) {if (getHeight(node.left) > getHeight(node.right)) {return node.left;} else {return node.right;}}// 获取node结点更高的子树中的更高的子树private Node tallGrandchild(Node node) {Node child = tallChild(node);return tallChild(child);}// 重新连接父结点和子结点(子结点允许为空)private static void relink(Node parent, Node child, boolean isLeft) {if (isLeft) {parent.left = child;} else {parent.right = child;}if (child != null) {child.parent = parent;}}// 旋转操作private void rotate(Node node) {Node parent = node.parent;Node grandparent = parent.parent;if (grandparent == null) {root = node;node.parent = null;} else {relink(grandparent, node, parent == grandparent.left);}if (node == parent.left) {relink(parent, node.right, true);relink(node, parent, false);} else {relink(parent, node.left, false);relink(node, parent, true);}}// trinode操作private Node restructure(Node node) {Node parent = node.parent;Node grandparent = parent.parent;if ((node == parent.right) == (parent == grandparent.right)) { // 处理需要一次旋转的情况rotate(parent);return parent;} else { // 处理需要两次旋转的情况:第1次旋转后即成为需要一次旋转的情况rotate(node);rotate(node);return node;}}// 返回以node为根结点的子树的第1个元素private static Node subtreeFirst(Node node) {while (node.left != null) {node = node.left;}return node;}// 返回以node为根结点的子树的最后1个元素private static Node subtreeLast(Node node) {while (node.right != null) {node = node.right;}return node;}// 获取以node为根结点的子树的高度private static int getHeight(Node node) {return node != null ? node.height : 0;}// 获取以node为根结点的子树的结点数private static int getSize(Node node) {return node != null ? node.size : 0;}
}

时间复杂度: 预处理的时间复杂度为O(N),其中N是树中结点的总数。插入、删除和搜索的时间复杂度均为 O(log⁡N)
空间复杂度: O(N),用于存储平衡二叉搜索树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/212226.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RHEL8_Linux硬盘管理

主要介绍Linux磁盘管理 了解分区的概念对硬盘进行分区常见的分区swap分区的管理 1.了解分区的概念 1&#xff09;新的硬盘首先需要对其进行分区和格式化&#xff0c;下面来了解以下硬盘的结构&#xff0c;如图。 2&#xff09;硬盘的磁盘上有一个个圈&#xff0c;每两个圈组…

JVM虚拟机系统性学习-类加载子系统

类加载子系统 类加载的时机 类加载的时机主要有 4 个&#xff1a; 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时&#xff0c;如果对应的类没有初始化&#xff0c;则要先进行初始化 new 关键字创建对象时读取或设置一个类型的静态字段时&#xff08;被 …

javaSwing酒店管理系统

一、 使用方法&#xff1a; 在使用前&#xff0c;需要到druid.properties 配置文件中&#xff0c;修改自己对应于自己数据库的属性&#xff1b;如用户名&#xff0c;密码等 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql:///hotel?useUnicodetrue&characterEn…

midwayjs从零开始创建项目,连接mikro-orm框架(必须有java的springboot基础)

前言&#xff1a; 我一直都是用java的springboot开发项目&#xff0c;然后进来新公司之后&#xff0c;公司的后端是用node.js&#xff0c;然后框架用的是 midwayjs &#xff0c;然后网上的资料比较少&#xff0c;在此特地记录一波 文档&#xff1a; 1.官方文档&#xff1a;介绍…

Spring Boot 3.0 : 集成flyway数据库版本控制工具

目录 Spring Boot 3.0 : 集成flyway数据库版本控制工具flyway是什么为什么使用flyway主要特性支持的数据库&#xff1a; flyway如何使用spring boot 集成实现引入依赖配置sql版本控制约定3种版本类型 运行SpringFlyway 8.2.1及以后版本不再支持MySQL&#xff1f; 个人主页: 【⭐…

常见web漏洞的流量分析

常见web漏洞的流量分析 文章目录 常见web漏洞的流量分析工具sql注入的流量分析XSS注入的流量分析文件上传漏洞流量分析文件包含漏洞流量分析文件读取漏洞流量分析ssrf流量分析shiro反序列化流量分析jwt流量分析暴力破解流量分析命令执行流量分析反弹shell 工具 攻击机受害机wi…

Unity DOTS中的baking(一) Baker简介

Unity DOTS中的baking&#xff08;一&#xff09; Baker简介 baking是DOTS ECS工作流的一环&#xff0c;大概的意思就是将原先Editor下的GameObject数据&#xff0c;全部转换为Entity数据的过程。baking是一个不可逆的过程&#xff0c;原先的GameObject在运行时不复存在&#x…

leetcode 股票DP系列 总结篇

121. 买卖股票的最佳时机 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。 只能进行一次交易 很简单&#xff0c;只需边遍历边记录最小值即可。 class Solution { public:int maxProfit(vector<int>& prices) {int res …

【git】关于git二三事

文章目录 前言一、创建版本库1.通过命令 git init 把这个目录变成git可以管理的仓库2.将修改的内容添加到版本库2.1 git add .2.2 git commit -m "Xxxx"2.3 git status 2.4 git diff readme.txt3.版本回退3.1 git log3.2 git reset --hard HEAD^ 二、理解工作区与暂存…

操作系统内部机制学习

切换线程时需要保存什么 函数需要保存吗&#xff1f;函数在Flash上&#xff0c;不会被破坏&#xff0c;无需保存。函数执行到了哪里&#xff1f;需要保存吗&#xff1f;需要保存。全局变量需要保存吗&#xff1f;全局变量在内存上&#xff0c;无需保存。局部变量需要保存吗&am…

Leetcode—337.打家劫舍III【中等】

2023每日刷题&#xff08;五十二&#xff09; Leetcode—337.打家劫舍III 算法思想 实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(null…

I.MX6ULL_Linux_驱动篇(46)linux LCD驱动

LCD 是很常用的一个外设&#xff0c;在Linux 下LCD 的使用更加广泛&#xff0c;在搭配 QT 这样的 GUI 库下可以制作出非常精美的 UI 界面。本章我们就来学习一下如何在 Linux 下驱动 LCD 屏幕。 Linux 下 LCD 驱动简析 Framebuffer 设备 先来回顾一下裸机的时候 LCD 驱动是怎…

前端入门:HTML初级指南,网页的简单实现!

代码部分&#xff1a; <!DOCTYPE html> <!-- 上方为DOCTYPE声明&#xff0c;指定文档类型为HTML --> <html lang"en"> <!-- html标签为整个页面的根元素 --> <head> <!-- title标签用于定义文档标题 --> <title>初始HT…

单点登录方案调研与实现

作用 在一个系统登录后&#xff0c;其他系统也能共享该登录状态&#xff0c;无需重新登录。 演进 cookie → session → token →单点登录 Cookie 可以实现浏览器和服务器状态的记录&#xff0c;但Cookie会出现存储体积过大和可以在前后端修改的问题 Session 为了解决Co…

UVM建造测试用例

&#xff08;1&#xff09;加入base_test 在一个实际应用的UVM验证平台中&#xff0c;my_env并不是树根&#xff0c;通常来说&#xff0c;树根是一个基于uvm_test派生的类。真正的测试用例都是基于base_test派生的一个类。 class base_test extends uvm_test;my_env e…

14-2(C++11)类型推导、类型计算

14-2&#xff08;C11&#xff09;类型推导、类型计算 类型推导auto关键字auto类型推断本质auto与引用 联用auto关键字的使用限制 类型计算类型计算分类与类型推导相比四种类型计算的规则返回值后置 类型推导 auto关键字 C98中&#xff0c;auto表示栈变量&#xff0c;通常省略…

Leetcode刷题笔记题解(C++):25. K 个一组翻转链表

思路&#xff1a;利用栈的特性&#xff0c;K个节点压入栈中依次弹出组成新的链表&#xff0c;不够K个节点则保持不变 /*** struct ListNode {* int val;* struct ListNode *next;* ListNode(int x) : val(x), next(nullptr) {}* };*/ #include <stack> class Solution { …

在国内,现在月薪1万是什么水平?

看到网友发帖问&#xff1a;现在月薪1W是什么水平&#xff1f; 在现如今的情况下&#xff0c;似乎月薪过万这个标准已经成为衡量个人能力的一个标准了&#xff0c;尤其是现在互联网横行的时代&#xff0c;好像年入百万&#xff0c;年入千万就应该是属于大众的平均水平。 我不是…

kafka入门(四):消费者

消费者 (Consumer ) 消费者 订阅 Kafka 中的主题 (Topic) &#xff0c;并 拉取消息。 消费者群组&#xff08; Consumer Group&#xff09; 每一个消费者都有一个对应的 消费者群组。 一个群组里的消费者订阅的是同一个主题&#xff0c;每个消费者接收主题的一部分分区的消息…

大师学SwiftUI第18章Part2 - 存储图片和自定义相机

存储图片 在前面的示例中&#xff0c;我们在屏幕上展示了图片&#xff0c;但也可以将其存储到文件或数据库中。另外有时使用相机将照片存储到设备的相册薄里会很有用&#xff0c;这样可供其它应用访问。UIKit框架提供了如下两个保存图片和视频的函数。 UIImageWriteToSavedPh…