数据结构,二叉搜索树的详解

news2024/12/23 1:25:01

🧑‍💻作者:程序猿爱打拳,Java领域新星创作者,阿里云社区博客专家。

🗃️文章收录于:数据结构与算法

🗂️JavaSE的学习:JavaSE

🗂️MySQL数据库的学习: MySQL数据库


在学习数据结构过程中,我们难免会遇到二叉搜索树。网上已经有大量的关于二叉树的博文讲解,但只有少量博文以一步步写代码的形式进行讲解,学起来非常困难。因此,我把二叉搜索树的遍历方式、查找节点、插入节点、删除节点通过图片和文字以及代码的形式一步步展示给大家。

目录

1. 二叉搜索树的概念

2. 二叉搜索树的操作

2.1 查找节点

2.2 插入节点

2.3 删除结点

1. 二叉搜索树的概念

二叉搜索树又称为二叉排序树,它的特点为左子树往后的所有结点都比根节点小,右子树往后的所有结点都比根节点大。根据下图来理解:

通过上图我们可以观察到,以20为根节点时左侧的所有节点都比20要小,右侧的所有节点都要比20要大。当以20的左子树18、右子树31作为根节点时也是左边节点比根节点小,右边节点比根节点大。

那么以这种结构组成的二叉树,我们就叫做二叉搜索树。因此我们能得出这些性质:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

首先我们要知道,一个节点是由左子树,值,右子树组成,通常左子树用 left 表示,值用 val 表示,右子树用 right 表示。把三个部分综合起来这样就形成了一个节点,树的根节点用 root 表示因此我们可以定义一个静态内部类 TreeNode 来搭建这个节点:

    //节点结构
    static class TreeNode {
        public TreeNode left;//左子树
        public TreeNode right;//右子树
        public int val;//值

        public TreeNode(int val) {
            this.val = val;//构造方法为成员变量赋值
        }
    }
    public TreeNode root;//根节点

这样节点的搭建就完成了,我们可以看到这个节点内包含的 left、val、right 这三个代表量。 下面,我们就来讲解二叉搜索树的操作:查找节点、插入节点、删除节点


2. 二叉搜索树的操作

2.1 查找节点

查找二叉树的节点,我们根据二叉搜索树的性质下可以列出以下几个条件,

  1. 若根节点不为空
  2. 根节点等于要查找的节点,返回根节点
  3. 根节点大于要查找的节点,在根节点的左子树查找
  4. 根节点小于要查找的节点,在根节点的右子树查找
  5. 否则,返回null

(1).根节点为空

根节点为空代表这个树是不存在的,因此我们直接返回null即可:

    if(root == null) {
        return null;//根节点为空
    }

(2).根节点等于要查找的节点

根节点等于要查找的节点我们直接返回根节点即可,这种情况相当于一下子就找到了。因此它的时间复杂度为O(1)

因此,我们可以写出这样的代码:

    if(root.val == key) {
        return root;//根节点等于要查找的值
    } 
 

root.valroot 节点值,key 是我们要查找的数据。注意,我们写代码是一步一步来写的,能实现当前的模块,就写当前的代码。最后综合起来并进行修改这样就能写出一段完整的代码。


(3).根节点大于要查找的节点,在根节点的左子树查找

根节点大于要查找的节点,为了满足二叉搜索树的左子树小于根节点的性质,我们要在根节点的左子树进行查找。

 因此,我们可以写出以下代码:

    if(root.val > key) {
        root = root.left; //被查找的值小于根节点
    }

如果 root.val 大于我们要查找的值key,使根节点变为 root 的 left 节点。


(4).根节点小于要查找的节点

根节点小于要查找的节点,为了满足二叉搜索树的右子树大于根节点的性质,在根节点的右子树查找。

因此,我们可以写出以下代码:

    if(root.val < key) {
        root = root.right;//被查找的值大于根节点
    }

把以上所有的代码综合起来,这样我们就能写出查找结点的方法。我把查找的结点的方法名命为 findNode,方法内的代码根据上方代码进行修改而成。

方法内的流程为:1.判断节点是否为空,为空则返回null。2.在根节点不为空的情况下,判断要查找的节点是否为根节点是则返回根节点,否则判断该节点在根节点的左子树还是右子树,是左子树就把根节点置为左子树,是右子树就把根节点置为右子树。3.没有该节点,返回null。

public class BinarySearchTree {

    //节点结构
    static class TreeNode {
        public TreeNode left;//左子树
        public TreeNode right;//右子树
        public int val;//值

        public TreeNode(int val) {
            this.val = val;//构造方法获取val的值
        }
    }

    //根节点
    public TreeNode root;

    //查找节点
    public TreeNode findNode(int key) {
        TreeNode node = root; //使node为根节点的一个代替
        if (node == null) { //根节点不为空
            return null;
        }
        while (node != null) { //根节点不为空
            if (node.val == key) { 
                return node;//找到根节点就返回node
            }else if (node.val > key) {
                node = node.left;//根节点的值大于查找的值
            }else {
                node = node.right;//根节点的值小于查找的值
            }
        }
        return null;//没有该节点返回null
    }
}

注意,我们直接使用根节点 root 进行操作的话会改变 root 的位置,这样在其他方法使用 root 结点时不能从最初的根节点(最顶层的根据)进行操作,因此我们可以创建一个代替根节点的代替值 node 来进行操作,这样无论如何 root 始终是在最初的根节点。


2.2 插入节点

插入节点,在满足二叉搜索树的性质情况下我们可以列出以下几种情况:

  1. 如果树是空树,则之间插入即可。
  2. 如果树不是空树,查找顺序确定插入的位置从而插入新节点。

(1)树是空树

我们直接插入一个新节点:

    if(root == null) {
        TreeNode node = new TreeNode(key);//新节点node
        root == node;//把新节点node赋值给root
        return;//结束程序
    }

以上代码中,key是我们要插入的值,因此我们要new一个新节点node来存放key值。然后直接将新节点node赋值给根节点root即可。


(2)树不是空树

按照顺序查找可以插入的位置,插入新节点。插入前的查找插入位置,跟上方的 findNode 方法是一样的,因此我们需要了解到的思想是如何进行插入这个环节。

在插入节点的操作时,我们要用一个 parent 来代表根节点的双亲结点,因为当根节点往后遍历走到空时我们无法确认要插入到哪个位置,这时可以使用 parent 这个结点作为根节点来插入新节点。

    TreeNode parent = root;//根节点的双亲结点
    TreeNode cur = root; //根节点的代表结点

当然,我们得使用一个根节点的代表结点 cur 来进行遍历。如何判断插入的位置是 parent 的左子树还是右子树呢?我们可以通过 parent 的 val 值与被插入值 key 进行比较,如果 key 小于 parent 的 val 值则插入到 parent 的左子树否则插入到右子树。因此,我们可以写出以下代码:

    TreeNode node = new TreeNode(key);//创建一个新结点node存放key值
    if(parent.val < key) {
        parent.right = node;//key值大于parent的val值,放在右子树
    }else {
        parent.left = node;//否则放在左子树
    }

把上述所有的代码综合起来,我们就能组成以下完整的代码。插入节点的方法名我定义为insertNode,当然你也可以根据自己的设计思想设定方法名以及变量名。

方法内的流程为:1.判断树是否为空,为空直接把插入的结点赋值给根节点。2.找到要插入的位置。3.插入到相应的位置。

 //插入节点
    public void insertNode(int val) {
        if (root == null) {
            root = new TreeNode(key);//新节点就是val值所在的节点
            return;//结束程序
        }
        TreeNode cur = root;//一个cur代替root根节点
        TreeNode parent = null;//根节点的上一个节点
        while(cur != null) {
            if (cur.val == key) {
                return;//如果这个节点存在直接结束程序
            }else if (cur.val > key) {
                parent = cur;
                cur = cur.left;//这个值小于根节点则往左子树走
            }else {
                parent = cur;
                cur = cur.right;//这个值大于根节点则往右子树走
            }
        }
        TreeNode node = new TreeNode(key);//使key值成为一个节点
        if (key > parent.val) {
            parent.right = node;//如果key值大于根节点值则把key值所在节点放在根节点右子树
        }else {
            parent.left = node;//否则放在左子树
        }
    }
}

2.3 删除结点

删除二叉搜索树的结点,我们必需在删除该结点后保证这个二叉树还是为二叉搜索树,因此这样的操作是比较难的。

二叉搜索树的难点(重点)就在于删除节点,我们可以设根节点为 root 待删除的节点为 cur,待删除的节点的双亲结点为 parent。因此会出以下情况:

  1. cur.left为null时
  2. cur.right为null时
  3. cur.left不为空并且cur.right不为空时:

(1).cur.left为null

当cur.left为null时分为3种情况:

情况1cur 是 root 时,需要将root=cur.right。

    if (cur.left == null) {
        if (cur == root) {
            root = cur.right;
        }
    }

情况2cur不是root,cur是parent.left时,我们需要将parent.left = cur.right。

    if (cur.left == null) {
        if (cur == parent.left) {
            parent.left = cur.right;
        }
    }

情况3cur不是root,cur是parent.right,我们需要将parent.right = cur.right。

    if (cur.left == null) {
        if (cur == parent.right) {
            parent.right = cur.right;
        }
    }

因此把cur.left为null的三种情况综合起来:

            if (cur.left == null) {
                if (cur == root) {
                    root = cur.right;
                }else if(cur == parent.left) {
                    parent.left = cur.right;
                }else {
                    parent.right = cur.right;
                }
             }

(2).cur.right为null

cur.right为null,也分为3种情况:

情况1cur是root,root=cur.left

    if(cur.right == null) {
        if(cur == root) {
            root = cur.left;
        }
    }

情况2cur不是root,cur是parent.left,parent.left = cur.left

    if(cur.right == null) {
        if(cur == parent.left) {
            parent.left = cur.left;
        }
    }

情况3cur不是root,cur是parent.right,parent.right = cur.left

    if(cur.right == null) {
        if(cur == parent.right) {
            parent.right = cur.left;
        }
    }

cur.right = null的三种情况综合起来,就能写出以下代码:

            if(cur.right == null) {
                if (cur == root) {
                    root = cur.left;
                }else if(cur == parent.left) {
                    parent.left = cur.left;
                }else {
                    parent.right = cur.left;
                }
             }

(3).cur.left不为空且cur.right不为空

当删除的结点左右子树都不为空的情况下,我们就得使用“替换法”来替换被删节点,被替换的节点为被删除节点的右子树的左侧子树的最后一个左子树是最适合用来替换被删节点的,无论哪种情况都是这样,大家可以自行画图测试一下。

目前我们知道了 cur 为要删除的结点,因此替换的结点我们设为 target ,替换节点的双亲结点为 targetParent ,设置替换节点双亲结点是为了当替换节点为空时,我们能找到替换节点!由于是从cur的右子树开始往下遍历,把 target = cur.right targetParent = cur


当 cur.right 的最左子树为 target 时

程序的结束条件为:target.left != null ,直接将该树的值赋值给cur的值即 cur.val = target.val。替换节点删除操作为:targetParent.left = target.right


cur.right 的最左侧不为 target 时

程序的结束条件为:target.left != null ,直接将该树的值赋值给cur的值即 cur.val = target.val。替换节点删除操作为:targetParent.right = target.right

 因此,综合上方把cur.left不为空且cur.right不为空可以写成以下代码:

TreeNode target = cur.right;
TreeNode targetParent = cur;
    while (target.left != null) {//终止条件为最左侧为空
        targetParent = target;
        target = target.left;
    }
        cur.val = target.val;//把cur的值替换为target的值
        if (target == targetParent.left) {
                targetParent.left = target.right;//最左侧值等于target
        }else {
                targetParent.right = target.right;//最左侧值不等于target
        }

把删除节点中所有代码综合起来,我们就能写出完整的删除操作。当然,我们得先找到删除的节点又使用到了 findNode 的操作,我把删除节点的操作封装到一个名为 removeNode 的方法里面,以下为完整代码:

//查找节点
    public void delNode(int key) {
        TreeNode cur = root;
        TreeNode parent = null;
        while(cur != null) {
            if (cur.val == key) {
                removeNode(cur,parent);//调用删除节点方法
            }else if(cur.val > key) {
                parent = cur;
                cur = cur.left;
                removeNode(cur,parent);//调用删除节点方法
            }else {
                parent = cur;
                cur = cur.right;
                removeNode(cur,parent);//调用删除节点方法
            }
        }
    }

    //删除节点
    public void removeNode(TreeNode parent,TreeNode cur) {
        while (cur != null) {
            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 target = cur.right;
                TreeNode targetParent = cur;
                while (target.left != null) {
                    targetParent = target;
                    target = target.left;
                }
                cur.val = target.val;
                if (target == targetParent.left) {
                    targetParent.left = target.right;
                }else {
                    targetParent.right = target.right;
                }
            }

        }
    }

🧑‍💻作者:程序猿爱打拳,Java领域新星创作者,阿里云社区博客专家。

🗃️文章收录于:数据结构与算法

🗂️JavaSE的学习:JavaSE

🗂️MySQL数据库的学习: MySQL数据库

本片博文到这里就结束了,感谢各位的阅读。

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

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

相关文章

Python基础之类

一&#xff1a;什么是类 类即类别/种类&#xff0c;是面向对象分析和设计的基石&#xff0c;如果多个对象有相似的数据与功能&#xff0c;那么该多个对象就属于同一种类。有了类的好处是&#xff1a;我们可以把同一类对象相同的数据与功能存放到类里&#xff0c;而无需每个对象…

【AI绘画】Midjourney的使用及程序示例

Midjourney 1.背景2.Midjourney的原理3.Midjourney的使用方法4.Midjourney的示例代码 1.背景 Midjourney 是一款基于深度学习的图像转换工具&#xff0c;其可以将一张图像转换成具有不同风格的图像&#xff0c;例如将一张照片转换成卡通风格的图像。Midjourney 基于 TensorFlow…

Jetson Nano一步到位打开USB摄像头(Rosmaster小车)

背景&#xff1a;我用的rosmaster r2小车配的摄像头是Astra pro&#xff0c;也就是下图这款&#xff1a; 1. 支持的摄像头 Jetson开发包有多个用于连接相机的接口&#xff0c;包括USB、以太网和MIPI CSI-2。流行的相机是现成的支持&#xff0c;而Jetson生态系统合作伙伴支持广…

基于树莓派的OpenEuler基础实验二

文章目录 基于树莓派的OpenEuler基础实验二一、ROS中间件介绍1. ROS话题通信与服务通信2. 常见的ROS终端命令 二、中间件基础实验1. ROS的移植2. ROS的安装和环境配置3. 第一个ROS实践之开启小海龟4. ROS话题实践1&#xff09;ROS工作区与软件包的创建2&#xff09;ROS的话题通…

可能是最强的Python可视化神器,建议一试

数据分析离不开数据可视化&#xff0c;我们最常用的就是Pandas&#xff0c;Matplotlib&#xff0c;Pyecharts当然还有Tableau&#xff0c;看到一篇文章介绍Plotly制图后我也跃跃欲试&#xff0c;查看了相关资料开始尝试用它制图。 Plotly Plotly是一款用来做数据分析和可视化的…

《商用密码应用与安全性评估》第二章政策法规2.4商用密码应用安全性评估工作

商用密码应用安全性评估体系发展历程 第一阶段&#xff1a;制度奠基期&#xff08;2007年11月至2016年8月&#xff09; 第二阶段&#xff1a;再次集结期&#xff08;2016年9月至2017年4月&#xff09; 第三阶段&#xff1a;体系建设期&#xff08;2017年5月至2017年9月&…

【Vue3】vue3中的watchEffect使用及其他的API

目录 一&#xff0c;watchEffect 二&#xff0c;生命周期 三&#xff0c;什么是hooks? 四&#xff0c;toRef 五&#xff0c;其他组合式API 5.1shallowReactive&shallowRef 5.2readonly&shallowReadonly 5.3.toRaw&markRaw 5.4自定义Ref-customRef ​5.5pr…

SSM框架整合之单表操作

1、Spring和Spring MVC父子容器 概念介绍 1.在Spring与SpringMVC进行整合的时候&#xff0c;一般情况下我们会使用不同的配置文件来配置Spring和SpringMVC&#xff0c;因此我们的应用中会存在至少2个ApplicationContext的实例&#xff0c;由于是在Web应用中&#xff0c;因此最…

基于Html+Css的图片展示26

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

AlgoC++第五课:基于矩阵的算法实现

目录 基于矩阵的算法实现前言1. 矩阵2. 矩阵求导推导3. 矩阵示例代码3.1 Matrix.hpp3.2 Matrix.cpp3.3 main.cpp3.4 拓展-cblas_sgemm3.5 拓展-LU分解 4. 多元线性回归5. 多元逻辑回归6. 最小二乘法7. 岭回归(L2)8. 多元牛顿法9. 高斯牛顿法10. Levenberg-Marquardt(修正牛顿法…

[Platforimio] LVGL +TFT_eSPI实现触摸功能

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; 本人持续分享更多关于电子通信专业内容以及嵌入式和单片机的知识&#xff0c;如果大家喜欢&#xff0c;别忘点个赞加个关注哦&#xff0c;让我们一起共同进步~ &#x…

Centos下环境变量

文章内容如下&#xff1a; 1&#xff09;什么是环境变量&#xff1b; 2&#xff09;如何通过程序获取环境变量&#xff1b; 3) 常识规律 一。环境变量的定义 环境变量就是指一段路径。 定义环境变量主要是为了方便的执行程序。添加环境变量的方法是export PATH$PATH:/A/B&…

医用IT隔离电源在医院特殊场所接地系统的应用

【摘要】我们国家大部分医院的临床救治和确诊都是利用了医疗电气类设备和医用的医疗仪器&#xff0c;因此这些地方的接地问题应该引起我们的高度的重视。IT系统主要是利用了中性点没有直接接地的方式&#xff0c;所以可以减少电压和电流&#xff0c;从而使人类触电的可能性小之…

搭建Serv-U FTP服务器共享文件外网远程访问「无公网IP」

文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 转载自内网穿透工具的文章&#xff1a;使用Serv-U搭建FTP服务器并公网访问【内网穿透】 1. 前言…

Midjourney教程(三)——Prompt常用参数

Midjourney教程——Prompt常用参数 为了提升prompt的准确度与输入效率&#xff0c;让midjourney能够生成我们理想中的图片&#xff0c;我们需要学习一下prompt的常用参数 Version version版本号&#xff0c;midjourney支持多种模型&#xff0c;我们可以通过version参数来选择…

字典树(Trie/前缀树)

目录 字典树的概念 字典树的逻辑 字典树的实现 字典树小结 例题强化 字典树的概念 字典树&#xff08;Trie&#xff09;是一种空间换时间的数据结构&#xff0c;是一棵关于“字典”的树&#xff0c;主要用于统计、排序和保存大量的字符串。字典树是通过利用字符串的公共前…

广域通信网 - 流量控制(停等协议、滑动窗口协议)

文章目录 1 概述2 流量控制协议2.1 停等协议2.2 滑动窗口协议 1 概述 #mermaid-svg-c9cNIYsOvLpoO4AV {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-c9cNIYsOvLpoO4AV .error-icon{fill:#552222;}#mermaid-svg-c9c…

[MLIR] CodeGen Pipeline总结

参考资料&#xff1a; [MLIR] CodeGen Pipeline总结 - 知乎 (zhihu.com) 本文主要以 tensorflow 为例&#xff0c;介绍了其接入 MLIR 后的 CodeGen 过程&#xff0c;以及简要分析了一些现在常用的 CodeGen pipeline。本文是本人在结合博客(Codegen Dialect Overview - MLIR - L…

隐私计算,联邦学习

隐私计算&#xff08;“隐私保护计算” Privacy-Preserving Computation&#xff09; 隐私计算是一类技术方案&#xff0c;在处理和分析计算数据的过程中能保持数据不透明、不泄露、无法被计算方法以及其他非授权方获取。 数据方是指为执行隐私保护计算过程提供数据的组织或个…

泰国五一游玩儿攻略

泰国五一游玩儿攻略 2023年4月27日1. 机场2. 酒店和夜市 2023年4月28日2023年4月29日2023年4月30日2023年5月1日2023年5月2日2023年5月3日 2023年4月27日 1. 机场 1.1 海关资料准备&#xff1a; 往返机票&#xff08;去程返程都得有&#xff0c;每人单独打印自己的&#xff0…