【数据结构】99%的人都知道的超好用二叉搜索树

【数据结构】99%的人都知道的超好用二叉搜索树

笔者近期学习了二叉搜索树,与其说是学习了此种数据结构,倒不如说是先在力扣上做了相关题目,而后觉得对其了解甚浅,于是再去找资料…今天就结合力扣题目,向大家介绍一下二叉搜索树。

我们先抛开树形结构不谈,来看看我们司空见惯的二分查找。

从折半查找说起(Binary Search)

当我们用有序数组保存数据时,采用折半查找,可以以O(logn)的效率高效地查找数据。

如下图所示,我们采用二分查找在数组中寻找一个值:

image-20240421112008492

无论需要寻找的值在数组中哪一个位置,都可以用O(logn)的时间复杂度寻找到。

但数组毕竟有其局限性,当我们想要插入或删除某个元素时,就容易遇到一些问题。

例如,我们如果要删除8这个元素

image-20240421112238906

首先我们要将8移出,而后将8后方所有元素都向前移动一位,时间复杂度达到了O(n)。

添加操作同理,需要在数组中添加某一元素,需要找到合适部位,而后将该元素右侧所有元素向右移动一位,再插入元素。

二叉搜索树(Binary Search Tree)

为了解决这一问题,二叉搜索树应运而生,在二叉搜索树中,我们可以实现添加、删除、查找的时间复杂度均为O(logn),接下来我们看看二叉搜索树如何实现查找,删除,添加操作。

注:本篇博客中关于二叉树的图片均用如下网站进行制作。

Binary Search Tree Visualization (usfca.edu)

首先介绍一下二叉搜索树的特点:

所有的左子树都小于根,所有的右子树都大于根。

对于任何子树都是如此。

如果对二叉搜索树中序遍历(即左中右)一定是从小到大

如图所示,这是一棵二叉搜索树:

image-20240421113843843

查找

原题链接:

700. 二叉搜索树中的搜索 - 力扣(LeetCode)

image-20240421125713310

以图为例,我如果要在如下的树中寻找7这个值:

image-20240421130653443

首先遍历根节点,发现比7小,故而遍历右子树

image-20240421130738148

发现12比7大,故而遍历左子树

image-20240421130814381

最后找到7这个节点

image-20240421130833585

接下来阐述代码思路:

使用递归法写代码,又或者是写二叉树的代码,总离不开递归三部曲,这在接下来的代码阐释中也会体现。

递归三部曲:

首先确定递归函数返回值及其参数。

而后确定递归停止条件。

最后确定单层递归逻辑。

在本题中:

  1. 确定递归函数返回值及其参数
    • 返回值:TreeNode,表示找到的节点或者是null(如果未找到)。
    • 参数:root(当前遍历的BST节点),val(要查找的值)。
  2. 确定递归停止条件
    • 代码中的停止条件是 root == null || root.val == val。如果当前节点为null(即没有更多的节点可以遍历),或者当前节点的值等于要查找的值,递归将停止。
  3. 确定单层递归逻辑
    • 单层递归逻辑是:如果当前节点的值小于要查找的值val,则递归地在右子树上进行查找;否则,在左子树上进行查找。这是BST的性质,左子树上的所有值都小于根节点的值,右子树上的所有值都大于或等于根节点的值。

AC代码如下:

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if(root == null || root.val == val) return root;
        TreeNode result = new TreeNode();
        if(root.val < val){
            result = searchBST(root.right,val);
        }else{
            result = searchBST(root.left,val);
        }
        return result;
    }
}

删除

原题链接:

450. 删除二叉搜索树中的节点 - 力扣(LeetCode)

image-20240421130132908

同样地,首先上图:

同样的二叉树,假设我要删除32这个节点

image-20240421131720805

首先用同样的方式找到32节点所在位置,删除即可。

但如果我们要删除12这个节点,情况就会有所不同:

image-20240421131833739

同样地,首先找到12这个节点

image-20240421131857181

而后将7移动到12原来的位置上,再将32连接至7上。

事实上,我们要分五种情况考虑删除问题。

  1. 没找到需要删除的节点
  2. 叶子结点左右都为空
  3. 左不空右空
  4. 左空右不空
  5. 左不空右不空

依旧套用递归三部曲的模板。

  1. 确定递归函数返回值及其参数
    • 返回值:TreeNode,表示删除节点后的树的根节点。
    • 参数:root(当前遍历的树节点),key(要删除的节点的值)。
  2. 确定递归停止条件
    • 递归的停止条件是当rootnull时,即未找到需要删除的节点。在这种情况下,函数将返回null,表示不需要做任何改变。
    • 如果找到需要删除的节点root.val == key,则根据该节点的子节点情况来决定如何处理:
      • 如果节点是叶子节点(没有子节点或者只有一个子节点),可以直接返回其子节点(如果有的话),或者返回null(如果没有子节点)。
      • 如果节点有两个子节点,找到该节点右子树中最左侧的节点(即右子树的最小值节点),将其值复制到当前节点,然后删除那个最小值节点。这样做的原因是二叉搜索树的性质,即任何节点的左子树都小于该节点,右子树都大于该节点。
  3. 确定单层递归逻辑
    • 如果当前节点的值大于要删除的key,在左子树中查找并删除节点。
    • 如果当前节点的值小于要删除的key,在右子树中查找并删除节点。

AC代码如下:

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        //如果没找到需要删除的节点
        if (root == null) return root;
        if (root.val == key) { 
            //叶子节点左右都为空 || 左空右不空
            if (root.left == null) {
                return root.right;
            //左不空右空
            }else if(root.right == null) {
                return root.left;
            //左右都不为空
            }else{
                //找到当前节点右子树中最左侧的值
                TreeNode cur = root.right;
                while (cur.left != null) {
                    cur = cur.left;
                }
                cur.left = root.left;
                root = root.right;
                return root;
            }
        }
        if (root.val > key) root.left = deleteNode(root.left, key);
        if (root.val < key) root.right = deleteNode(root.right, key);
        return root;
    }
}

添加

原题链接:

701. 二叉搜索树中的插入操作 - 力扣(LeetCode)

image-20240421130119665

不说废话,上图:

image-20240421132611326

同样的树,假设我要插入一个10

image-20240421132651802

首先新建了一个10的节点,而后按照二叉搜索树的遍历顺序依次比较。

image-20240421132811079

最终找到合适的位置并插入。

  1. 确定递归函数返回值及其参数
    • 返回值:TreeNode,表示插入新节点后的BST的根节点。
    • 参数:root(当前遍历的BST节点),val(要插入的新节点的值)。
  2. 确定递归停止条件
    • 递归的停止条件是当rootnull时,即到达了BST的末端,需要在此处插入新节点。
  3. 确定单层递归逻辑
    • 如果新节点的值val大于当前节点的值root.val,则递归地在右子树上插入新节点。
    • 如果新节点的值val小于当前节点的值root.val,则递归地在左子树上插入新节点。
    • 在递归调用返回后,当前节点的左或右子节点将会指向新插入的节点。

AC代码及题解如下:

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        TreeNode node = new TreeNode(val);
        if(root == null) root = node;
        //搜索到合适位置
        if(root.val < val){
            root.right = insertIntoBST(root.right,val);
        }
        if(root.val > val){
            root.left = insertIntoBST(root.left,val);
        }
        return root;
    }
}

构造

原题链接:

108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

image-20240421130151628

本题不上图,直接上题解。

  1. 确定递归函数返回值及其参数

    • 返回值:TreeNode,表示平衡二叉搜索树的根节点。
    • 参数:
      • nums:一个整数数组,表示排序后的序列。
      • left:当前递归调用处理的数组的左边界。
      • right:当前递归调用处理的数组的右边界。
  2. 确定递归停止条件

    • 递归的停止条件是left > right,这意味着当前子数组为空,没有元素可以构造成树的节点。
  3. 确定单层递归逻辑

    • 首先,计算子数组的中间索引mid,这是为了确保树的平衡。

    • 接着,创建一个新的TreeNode对象,其值为nums[mid],这个节点将成为当前递归调用构建的BST的根节点。

    • 然后,递归地调用

      traversal
      

      函数来构建左子树和右子树:

      • 左子树的节点由数组中从leftmid - 1的元素构成。
      • 右子树的节点由数组中从mid + 1right的元素构成。
class Solution {
    //构造平衡二叉搜索树
	public TreeNode sortedArrayToBST(int[] nums) {
		TreeNode root = traversal(nums, 0, nums.length - 1);
		return root;
	}

	private TreeNode traversal(int[] nums, int left, int right) {
		if (left > right) return null;

		int mid = left + ((right - left) >> 1);
		TreeNode root = new TreeNode(nums[mid]);
		root.left = traversal(nums, left, mid - 1);
		root.right = traversal(nums, mid + 1, right);
		return root;
	}
}

结语

自上次发出《二叉爆炸》那篇博客之后已经有一周,这一周学习时间较为紧张,关于数据结构仅对二叉搜索树作了一些了解,以后可能会学习一些较为复杂的树,如红黑树、234树等。

参考文献

代码随想录 (programmercarl.com)

Leetcode部分题解

Binary Search Tree Visualization (usfca.edu)

二叉搜索树(二叉排序树)(二叉查找树)_哔哩哔哩_bilibili

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Cloud 运维篇1——Jenkins CI/CD 持续集成部署

Jenkins 1、Jenkins是什么&#xff1f; Jenkins 是一款开源 CI/CD 软件&#xff0c;用于自动化各种任务&#xff0c;包括构建、测试和部署软件。 Jenkins 支持各种运行方式&#xff0c;可通过系统包、Docker 或者一个独立的 Java 程序。 Jenkins Docker Compose持续集成流…

没有理由不加倍努力

最近su7很火&#xff0c;各隐藏大佬都纷纷从后台来到前台&#xff0c;把整个网红界的网红等级提升了好几个档次。红衣大叔更是借此机会在疯狂地打造自己的网红IP。 千亿大佬都这还般努力&#xff0c;作为平民的自己哪还有不努力的理由。 加倍努力&#xff01;

如何在PostgreSQL中使用pg_stat_statements插件进行SQL性能统计和分析?

文章目录 一、启用pg_stat_statements插件二、查看统计信息三、定期重置统计信息四、注意事项 PostgreSQL中的pg_stat_statements是一个强大的插件&#xff0c;用于追踪执行时间最长的SQL语句。通过它&#xff0c;我们可以获取有关SQL语句执行频率、总执行时间、平均执行时间等…

[创业之路-106] :经济学十大陷阱与核心思想:系统论、社会进化论、周期论、阴阳互转论

目录 前言&#xff1a; 一、流动性陷阱。 二、中等收入陷阱。 三、修昔底德陷阱。 四、塔西佗陷阱。 五、金德尔伯格陷阱。 六、卢梭陷阱。 七、拉美陷阱。 八、阿喀琉斯之踵。 九、布拉德伯里悖论。 十、李约瑟之谜 结论&#xff1a;上述陷阱的…

C++相关概念和易错语法(5)(析构函数、拷贝构造、运算符重载、赋值重载)

上篇文章分享了一些构造函数和析构函数的易错点&#xff0c;这篇文章则将继续分享一些构造函数、拷贝构造函数的易错点。 1.变量声明处赋缺省值 我们已经知道了自动构造函数的初始化规则了。我们可以认为这个初始化规则比较保守&#xff0c;能不修改成员变量的值就不修改&…

实在RPA设计器试用导引

一、产品概述 实在RPA设计器是一款将人工智能(AI)与机器人流程自动化(RPA)深度融合的可视化自动流程编辑器。它通过AI推荐与桌面嵌入式交互&#xff0c;极大简化了RPA的使用难度&#xff0c;让普通业务人员也能轻松使用。实在RPA设计器具备以下核心优势&#xff1a; 兼容性&a…

Redis详解和Spring Data Redis应用

注意事项 如何快速进入命令行窗口什么是配置类 Redis简介 Redis是一个开源的使用ANSI C语言编写的、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库&#xff0c;并提供多种语言的API。它通常被称为数据结构服务器&#xff0c;因为值&#xff08;value&#xff09…

数电期末复习(二)逻辑代数基础

这里写目录标题 2.1 二值逻辑变量与基本逻辑运算2.1.1 与运算2.1.2 或运算2.1.3 非运算2.1.4 常用复合逻辑运算 2.2 逻辑函数的建立及其表示方法2.2.1 真值表表示2.2.2 逻辑函数表达式表示2.2.3 逻辑图表示方法2.2.4 波形图表示方法 2.3 逻辑代数2.3.1 逻辑代数的基本定律和恒等…

从例题出发,提高离散数学兴趣(一)集合关系

关系的性质&#xff1a;(反)自反性&#xff0c;&#xff08;反&#xff09;对称性&#xff0c;可传递性&#xff01; 例题一&#xff1a; 复合关系与逆关系&#xff1a; 例题二&#xff1a; 覆盖与划分与等价关系&#xff1a; 重要的证明&#xff1a; 偏序关系&#xff08;自反…

Java面试八股之System.gc和Runtime.gc的作用分别是什么

System.gc和Runtime.gc的作用分别是什么 从代码中我们能看出&#xff0c;这两个方法其实本质上都是调用的Runtime类中的gc()方法&#xff0c;并且Runtime类中的gc()是一个native方法。之前我们也讲过&#xff0c;这个仅仅是给JVM一个垃圾回收的信号&#xff0c;具体是否进行垃圾…

对组合模式的理解

目录 一、场景1、题目描述 【[案例来源](https://kamacoder.com/problempage.php?pid1090)】2、输入描述3、输出描述4、输入示例5、输出示例 二、实现&#xff08;假的组合模式&#xff09;1、代码2、为什么上面的写法是假的组合模式&#xff1f; 三、实现&#xff08;真的组合…

初识C++·类和对象(中)(3)

前言&#xff0c;最难的已经结束了&#xff0c;来点轻松了放松一下。 目录 1 流重载 2 const成员 3 取地址及const取地址操作符重载 1 流重载 C语言中printf和scanf是有局限性&#xff0c;只能直接打印内置类型&#xff0c;对于自定义类型就哦豁了&#xff0c;所以在C中就…

38. UE5 RPG 修改火球术的攻击方向以及按住Shift攻击

在前面&#xff0c;我们实现了火球术火球的制作&#xff0c;能够在释放火球术时&#xff0c;角色将播放释放技能动画&#xff0c;并实现了对火球的目标的服务器同步功能。 我们先回忆一下之前完成的内容。 在前面&#xff0c;我们先做了一个Actor&#xff0c;用于承载发射的火…

java线程-线程池

简介 工作原理 如何获取线程池对象 构造器的参数含义 注意事项 构造器-ThreadPoolExecutor // ArrayBlockingQueue 是一个有界的阻塞队列&#xff0c;它的内部实现是一个数组。有界的意思是它的容量是有限的&#xff0c;我们必须在创建 ArrayBlockingQueue 实例的时候指定容量…

01-服务与服务间的通信

这里是极简版&#xff0c;仅用作记录 概述 前端和后端可以使用axios等进行http请求 服务和服务之间也是可以进行http请求的spring封装的RestTemplate可以进行请求 用法 使用bean注解进行依赖注入 在需要的地方&#xff0c;自动注入RestTemplate进行服务和服务之间的通信 注…

[2021最新]大数据平台CDH存储组件kudu之启用HA高可用(添加多个master)

今天在做kudu高可用的时候没有参考官网&#xff0c;直接按照常规方式&#xff08;添加角色—>编辑属性—>启动&#xff09;结果发现报错&#xff1f;然后参考了一下文档之后发现这玩意儿还有点玄学&#xff0c;做一下记录。 1.添加两个master。kudu master有leader和foll…

深入解析Tomcat的工作流程

tomcat解析 Tomcat是一个广泛使用的开源Servlet容器&#xff0c;用于托管Java Web应用程序。理解Tomcat的工作流程对于开发人员和系统管理员来说是非常重要的。本文将深入探讨Tomcat的工作原理&#xff0c;包括请求处理、线程池管理、类加载、以及与Web服务器之间的通信。 ###…

思颜肌密:匠心独蕴,传世掠影

赋予延绵岁月以华彩乐章&#xff0c;将来自时间的承诺注入生活每分每秒&#xff0c;在思颜肌密的世界里&#xff0c;恒久之美并非遥不可及&#xff0c;它是艺术&#xff0c;亦是心意。华美节日翩然而至&#xff0c;思颜肌密拉开神秘帷幕&#xff0c;在惊鸿掠影中向世人展现传世…

[数据结构与算法]-什么是二叉树?

二叉树是一种数据结构&#xff0c;由节点组成&#xff0c;每个节点最多有两个子节点&#xff0c;分别称为左子节点和右子节点。二叉树的每个节点包含一个值&#xff0c;并且左子节点的值小于等于父节点的值&#xff0c;右子节点的值大于等于父节点的值。这个性质使得二叉树在搜…

Linux系统维护:增加空闲内存的大小,以便进程有足够的基础内存(空闲内存)来运行

目录 一、问题 二、解决思路 &#xff08;一&#xff09;问题分析 &#xff08;二&#xff09;思路 1. 清理缓存 2. 结束不必要的进程 3. 优化应用程序和服务 4. 增加物理内存 5、注意事项 三、实际处理 &#xff08;一&#xff09;结束不必要的程序 &#xff08;二…