数据结构:图文详解 搜索二叉树(搜索二叉树的概念与性质,查找,插入,删除)

news2024/11/15 9:47:50


 

目录

搜索二叉树的相关概念和性质

搜索二叉树的查找

搜索二叉树的插入

搜索二叉树的删除

1.删除节点只有右子树,左子树为空

2.删除节点只有左子树,右子树为空

3.删除节点左右子树都不为空

搜索二叉树的完整代码实现

 


搜索二叉树的相关概念和性质

搜索二叉树(Binary Search Tree,简称BST)是一种树形数据结构,具有以下性质:

  1. 每个节点最多有两个子节点,分别称为左子节点和右子节点
  2. 左子节点的值小于父节点的值,右子节点的值大于父节点的值
  3. 中序遍历搜索二叉树得到的序列是有序的

搜索二叉树提供了快速的插入删除搜索操作,因为它能够通过比较节点值来减少搜索的范围。比如,要搜索一个值为x的节点,可以从根节点开始,如果x小于当前节点的值,则继续在左子树中搜索,如果x大于当前节点的值,则继续在右子树中搜索。重复这个过程,直到找到目标节点或者搜索到叶子节点。

插入和删除操作同样也是通过比较节点值来找到合适的位置进行操作的。具体的插入和删除操作可以根据需要进行扩展,保持BST的特性。

BST的时间复杂度取决于树的高度,在最坏情况下(树被构造成链表),时间复杂度为O(n),其中n是节点的数量。但是在平均情况下,BST的时间复杂度是O(log n)

搜索二叉树的应用非常广泛,比如数据库索引、缓存等。它提供了高效的数据访问操作,并且能够保持数据的有序性。

如下图就是一颗搜索二叉树,如果我们对这颗搜索二叉树进行遍历的话,我们就会得到 0 1 2 3 4 5 6 7 8 9 的有序队列,也就是我们上述提到的第三条性质。

我们随便拿到其中的一部分进行说明,对于任意一个节点他的左子节点一定小于该节点,而该节点一定小于该节点的右子节点


搜索二叉树的查找

搜索二叉树一般有三种操作:查找,插入,删除

而在其中查找是最为容易的,同时其他俩个操作也都是基于查找实现的。对于一颗搜索二叉树,因为他的规则是否的明确,节点之间是存在明确的大小关系的,所以我们就利用这个大小关系来进行查找操作,具体操作起来有点像二分查找,我们首先拿要查找的值与根节点的值进行比较,如果根节点的值就是我们要查找的元素,那我们就查找成功,然后返回。如果根节点的值<查找的值,那就说明我们要查找的值在根节点的右子树,如果根节点的值>查找的值,那就说明我们要查找的值在根节点的左侧。然后我们反复的将左右子树的根节点带入进行判断。最终我们就可以得出结果。

我们给出如下的图示举例:

对于这样的搜索过程,我们可以分析他的效率,第一次查找我们排除了左边5个节点,第二次查找我们排除了右边2个节点,我们仅仅进行了3次判断就得到了我们想要的结果。这相较于我们常规的数组二分查找,提高了效率和确定性。由此可见搜索二叉树的效率还是非常高的。

对于这样的搜索过程,我们可以给出相应的代码:

    //查找
    public boolean search(int val) {
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val == val) {
                return true;
            } else if (cur.val < val) {
                cur = cur.right;
            } else {
                cur = cur.left;
            }
        }
        return false;
    }

搜索二叉树的插入

对于搜索二叉树的插入,我们需要解决俩个问题:

  1. 找到往哪个节点后插入
  2. 往这个节点后插入时选择左子节点还是右子节点

对于第一个问题,其实本质上就是对节点的查找,查找出合适的插入节点;对于第二个问题,我们需要判断插入节点和目标节点的值的大小关系,另外在代码层面实现时有一点需要注意,我们找到插入节点的位置后,在使用指针指向的时候要避免空指针的问题,我们用下图举例:

假如我们要将节点 “7” 插入到搜索二叉树中,原本节点 “6” 的左右子节点都为空,那我们在插入的时候就不能直接将新节点的值赋给这俩个空节点,不然就会出现 null = newNode 这样的空指针异常,所以我们必须要将插入节点的父节点记录下来,通过父节点的指向来进行插入

    //插入
    public void insert(int val) {
        TreeNode newNode = new TreeNode(val);
        if (root == null) {
            root = newNode;
            return;
        }
        TreeNode cur = root;//记录插入节点
        TreeNode parent = null;//插入节点的父亲节点
        while (cur != null) {
            if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            }else {//重复元素不予插入
                return;
            }
        }
        //判断在父亲节点的左边还是右边进行插入
        if (parent.val < val) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
    }

搜索二叉树的删除

对于整个搜索二叉树的操作中,删除节点是最麻烦的,因为我们需要对很多种情况进行判断

因为BST中元素与元素之间是存在严谨的大小关系的,所以在我们删除元素之后也应该任然保持这样的关系,为此我们将删除节点可能出现的情况做出分类:

  1. 删除节点只有右子树,左子树为空
  2. 删除节点只有左子树,右子树为空
  3. 删除节点左右子树都不为空

尽管我们对删除节点的状态做出了分类,但这任然不能包含所有的情况,上述三种情况是对删除节点的子树情况做出的分类,事实上删除节点的位置,也就是和父节点的关系也会影响我们的删除过程。以下我们分别对上述三种情况做出进一步的分类:

1.删除节点只有右子树,左子树为空

如图,在这种情况下,我们根据删除节点的位置又可以分出三类:

情况一:当删除节点为根节点的时候,因为删除节点没有左子树,所以我们直接将删除节点的右子节点更新为新的根节点,也就是

root = cur.right;

情况二:当删除节点为父亲节点的左子节点的时候,因为删除节点没有左子树,所以我们直接让父亲节点的左指针指向删除节点的右子节点,也就是

parent.left = cur.right;

情况三:当删除节点为父亲节点的右子节点的时候,因为删除节点没有左子树,所以我们直接让父亲节点的右指针指向删除节点的右子节点,也就是

parent.right = cur.right;

以上便是删除节点左子树为空的情况 

2.删除节点只有左子树,右子树为空

如图,在这种情况下,我们根据删除节点的位置又可以分出三类:

情况一:当删除节点为根节点的时候,因为删除节点没有右子树,所以我们直接将删除节点的左子节点更新为新的根节点,也就是

root = cur.left;

情况二:当删除节点为父亲节点的左子节点的时候,因为删除节点没有右子树,所以我们直接让父亲节点的左指针指向删除节点的左子节点,也就是

parent.left = cur.left;

情况三:当删除节点为父亲节点的右子节点的时候,因为删除节点没有右子树,所以我们直接让父亲节点的右指针指向删除节点的左子节点,也就是

parent.right = cur.left;

3.删除节点左右子树都不为空

在这种情况下,我们往往需要在多个叶子节点中选取一个合适的叶子节点,并且将其替换掉删除节点,最后再将这个叶子节点删除,我们拿下图的情况一举例:

那么我们就需要解决一个问题,如何找到合适的叶子节点,在这里笔者给大家提供一个方法:

  • 找删除节点左子树中的最大值
  • 找删除节点右子树中的最小值

上述俩中方法选择一种即可,我们随便拿其中一种方法分析举例

假如我们找到了删除节点右子树中的最小值,那他就可以同时满足俩个条件, 他比删除节点中的所有右子树节点都小,又因为他在右子树,所以他也大于删除节点的左子节点,那么就同时满足了成为搜索二叉树节点的俩个条件,即该节点大于左子节点,小于右子节点。

在删除节点的时候,我们也需要使用父亲节点来避免出现空指针异常的情况,就如前文提到的那样。当我们找到删除节点后,其实我们还需要对删除节点的位置进行判断,就像上图的情况二和情况三一样,原因很简单,删除操作需要使用指针操作,如果删除节点在父亲节点的右边,那我们就需要更改父亲节点的右指针指向,反之则要更改父亲节点的左指针指向,因此我们对这种情况还得做出判断。

我们将删除节点总的代码给出如下:

    //删除
    public void remove(int val) {
        TreeNode cur = root;
        TreeNode parent = null;
        //先找到要删除的节点的位置
        while (cur != null) {
            if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            }else {
                //找到要删除的节点了
                removeNode(parent,cur);//调用函数删除节点
                return;
            }
        }
    }
    
    private void removeNode(TreeNode parent,TreeNode cur) {
        //对节点的状态进行分类
        if (cur.left == null) {//要删除节点左子树为空树
            if (cur == root) {
                root = cur.right;
            }else if (cur == parent.left) {
                parent.left = cur.right;
            }else {
                parent.right = cur.right;
            }
        }else if (cur.right == null) {//要删除节点右子树为空树
            if (cur == root) {
                root = cur.left;
            }else if (cur == parent.left) {
                parent.left = cur.left;
            }else {
                parent.right = cur.left;
            }
        }else {//要删除节点左右都不为空
            TreeNode temp = cur.right;//用来指向叶子节点
            TreeNode tempParent = cur;//用来指向叶子节点的父节点
            while (temp != null) {
                tempParent = temp;
                temp = temp.left;
            }//找到了要替换的节点
            cur.val = temp.val;
            //替换完成后就删除叶子节点
            if (tempParent.left == temp) {
                tempParent.left = temp.right;//删除叶子节点
            }else {
                tempParent.right = temp.right;//删除叶子节点
            }
        }

前半部分是用来找到删除节点,后半部是根据我们的分析对删除节点的情况做出分类判断然后进行删除操作。

搜索二叉树的完整代码实现

为了方便读者使用,我们这里将整个搜索二叉树的完整代码给出:

public class BinarySearchTree {
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        
        public TreeNode(int val) {
            this.val = val;
        }
    }
    
    public TreeNode root;
    
    //查找
    public boolean search(int val) {
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val == val) {
                return true;
            } else if (cur.val < val) {
                cur = cur.right;
            } else {
                cur = cur.left;
            }
        }
        return false;
    }
    
    //插入
    public void insert(int val) {
        TreeNode newNode = new TreeNode(val);
        if (root == null) {
            root = newNode;
            return;
        }
        TreeNode cur = root;//记录插入节点
        TreeNode parent = null;//插入节点的父亲节点
        while (cur != null) {
            if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            }else {//重复元素不予插入
                return;
            }
        }
        //判断在父亲节点的左边还是右边进行插入
        if (parent.val < val) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
    }
    
    //删除
    public void remove(int val) {
        TreeNode cur = root;
        TreeNode parent = null;
        //先找到要删除的节点的位置
        while (cur != null) {
            if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            }else {
                //找到要删除的节点了
                removeNode(parent,cur);//调用函数删除节点
                return;
            }
        }
    }
    
    private void removeNode(TreeNode parent,TreeNode cur) {
        //对节点的状态进行分类
        if (cur.left == null) {//要删除节点左子树为空树
            if (cur == root) {
                root = cur.right;
            }else if (cur == parent.left) {
                parent.left = cur.right;
            }else {
                parent.right = cur.right;
            }
        }else if (cur.right == null) {//要删除节点右子树为空树
            if (cur == root) {
                root = cur.left;
            }else if (cur == parent.left) {
                parent.left = cur.left;
            }else {
                parent.right = cur.left;
            }
        }else {//要删除节点左右都不为空
            TreeNode temp = cur.right;//用来指向叶子节点
            TreeNode tempParent = cur;//用来指向叶子节点的父节点
            while (temp != null) {
                tempParent = temp;
                temp = temp.left;
            }//找到了要替换的节点
            cur.val = temp.val;
            //替换完成后就删除叶子节点
            if (tempParent.left == temp) {
                tempParent.left = temp.right;//删除叶子节点
            }else {
                tempParent.right = temp.right;//删除叶子节点
            }
        }
    }
}



 本次的分享就到此为止了,希望我的分享能给您带来帮助,也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

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

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

相关文章

Java设计模式-组合模式(13)

大家好,我是馆长!今天开始我们讲的是结构型模式中的组合模式。老规矩,讲解之前再次熟悉下结构型模式包含:代理模式、适配器模式、桥接模式、装饰器模式、外观模式、享元模式、组合模式,共7种设计模式。 组合模式(Composite Pattern) 定义 组合(Composite)模式:又叫…

【大厂AI课学习笔记】1.3 人工智能产业发展(2)

&#xff08;注&#xff1a;腾讯AI课学习笔记。&#xff09; 1.3.1 需求侧 转型需求&#xff1a;人口红利转化为创新红利。 场景丰富&#xff1a;超大规模且多样的应用场景。主要是我们的场景大&#xff0c;数据资源丰富。 抗疫加速&#xff1a;疫情常态化&#xff0c;催生新…

(十一)springboot实战——springboot3下关于WebFlux项目的一些常用功能整合

前言 本节内容主要是对webflux项目一些常用功能的介绍&#xff0c;例如系统集成swagger接口文档&#xff0c;方便接口测试以及前后端项目联调测试&#xff1b;使用actuator完成系统各种指标的监控功能&#xff1b;系统使用logback日志框架完成项目日志的收集&#xff1b;使用过…

reactnative 调用原生ui组件

reactnative 调用原生ui组件 ![组件对应关系](https://img-blog.csdnimg.cn/direct/c4351ad7bd38411e9c13087f1059a4b0.png)1.该样例已textView,介绍。 新建MyTextViewManager 文件,继承SimpleViewManager。import android.graphics.Color; import android.widget.TextView;…

redis设计与实践的总结

Redis是一款高性能的Key-Value存储系统&#xff0c;它可以用作缓存、消息队列、计数器、排行榜等多种应用场景。在实际应用中&#xff0c;如何设计和使用Redis是非常关键的。本文将介绍Redis的设计原则和最佳实践&#xff0c;帮助您更好地利用Redis提高应用性能和可靠性。 ###…

【Linux】文件基础、文件系统调用接口、文件描述符

目录 文件基础 系统调用接口 open close write 实现文件写入 实现文件内容追加 read 实现文件读取 文件描述符fd 文件基础 1.空文件&#xff0c;也要在磁盘占用空间。 2.文件内容属性 3.文件操作&#xff1a;是单独对于内容或属性、或者内容和属性 4.文件路径文件名&…

C++ 菱形继承和虚拟菱形继承

菱形继承和虚拟菱形继承 菱形继承1. 概念2. 产生的问题 虚拟菱形继承1.1 使用1.2 原理 菱形继承 1. 概念 菱形继承是多继承的一个特殊情况&#xff0c;多继承是指一个子类类继承了两个或以上的直接父类&#xff0c;而菱形继承问题的产生是因为该子类的父类&#xff0c;继承了…

MTK8365安卓核心板_联发科MT8365(Genio 350)核心板规格参数

MTK8365安卓核心板是一款高性能的嵌入式处理器产品&#xff0c;基于联发科领先的SoC架构和先进的12纳米工艺。它集成了四核ARM Cortex-A53处理器&#xff0c;每个核心频率高达2.0 GHz&#xff0c;搭载强大的多标准视频加速器&#xff0c;支持高达1080p 60fps的视频解码。此外&a…

SpringBoot使用当前类代理类(内部事务)解决方案

文章目录 一、场景描述二、解决方案1. 使用 Lazy&#xff08;推荐&#xff09;2. 使用方法注入3. 使用 ApplicationContext4. 分离服务层5. AspectJ 代理模式 在 Spring Boot 开发中&#xff0c;我们时常遇到需要在一个类的内部调用自己的其他方法&#xff0c;并且这些方法可能…

一步步教大家在windows环境下搭建SkyWalking,百分百成功(内附spring boot demo工程源码)

本文详细的介绍了skywalking在Win10上的环境安装过程&#xff0c;es 、oap 和应用jar包都在一台机器上运行。其中文章中提供了es、oap、agent、以及springboot demo工程的下载链接。相信刚接触Skywalking的同学&#xff0c;只需要按照本文内容一步步操作就会完成skywalking的环…

Kotlin 协程:深入理解 ‘async { }‘

Kotlin 协程&#xff1a;深入理解 ‘async { }’ Kotlin 协程是一种强大的异步编程工具&#xff0c;它提供了一种简洁、易读的方式来处理并发和异步操作。在 Kotlin 协程库中&#xff0c;async {} 是一个关键的函数&#xff0c;它允许我们启动一个新的协程&#xff0c;并返回一…

[Python-闫式DP]

闫式DP分析法 闫老师是将DP问题归结为了有限集合中的最值问题。 动态规划有两个阶段&#xff0c;一是状态表示&#xff0c;二是状态计算。 状态表示 f(i,j) 状态表示是一个化零为整的过程&#xff0c;动态规划的做题思路不是暴力法的每一个物品都去枚举&#xff0c;而是将相…

二叉树-堆应用(1)

目录 堆排序 整体思路 代码实现 Q1建大堆/小堆 Q2数据个数和下标 TopK问题 整体思路 代码实现 Q1造数据CreateData Q2建大堆/小堆 建堆的两种方法这里会用到前面的向上/向下调整/交换函数。向上调整&向下调整算法-CSDN博客 堆排序 整体思路 建堆&#xff08;直…

安全通道堵塞识别摄像机

当建筑物的安全通道发生堵塞时&#xff0c;可能会给人员疏散和救援带来重大隐患。为了及时识别和解决安全通道堵塞问题&#xff0c;专门设计了安全通道堵塞识别摄像机&#xff0c;它具有监测、识别和报警功能&#xff0c;可在第一时间发现通道堵塞情况。这种摄像机通常安装在通…

LeetCode--171

171. Excel 表列序号 给你一个字符串 columnTitle &#xff0c;表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。 例如&#xff1a; A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 ... 示例 1: 输入: columnTitle "A" 输出:…

WebGL技术开发框架

WebGL技术框架是一些提供了便捷API和工具的库&#xff0c;用于简化和加速在Web浏览器中使用WebGL进行3D图形开发。以下是一些常用的WebGL技术框架&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.Th…

ElementUI 组件:Layout布局(el-row、el-col)

ElementUI安装与使用指南 Layout布局 点击下载learnelementuispringboot项目源码 效果图 el-row_el-col.vue页面效果图 项目里el-row_el-col.vue代码 <script> export default {name:el-row_el-col 布局 }</script><template><div class"roo…

Python代码重构库之rope使用详解

概要 Python是一门强大的编程语言,但在大型项目中,维护和重构代码可能会变得复杂和困难。为了提高开发人员的效率和准确性,有许多工具可用于辅助代码重构和智能代码补全。其中之一是Python Rope。 Python Rope是一个用于Python编程语言的强大工具,它提供了丰富的功能,包…

STM32低功耗模式

一、低功耗模式介绍 STM32 的低功耗模式有 3 种&#xff1a; 1)睡眠模式&#xff08;CM3 内核停止&#xff0c;外设仍然运行&#xff09; 2)停止模式&#xff08;所有时钟都停止&#xff09; 3)待机模式&#xff08;1.8V 内核电源关闭&#xff09; 在这三种低功耗模式中&#…

[机器学习]简单线性回归——最小二乘法

一.线性回归及最小二乘法概念 2.代码实现 # 0.引入依赖 import numpy as np import matplotlib.pyplot as plt# 1.导入数据 points np.genfromtxt(data.csv, delimiter,) # points[0,0]# 提取points中的两列数据&#xff0c;分别作为x&#xff0c;y x points[:, 0] y poi…