数据结构和算法之树形结构(2)

news2024/9/23 16:04:57

文章出处:数据结构和算法之树形结构(2)  

关注码农爱刷题,看更多技术文章!!

三、二叉查找树(接前篇)

      二叉查找树,又称二叉搜索树或二叉排序树,是在普通二叉树基础上为了实现快速查找而设计出来的一种树形结构。二叉查找树要求每个节点在树中的任意一个节点,其左子树中的每个节点的值都要小于这个节点的值,而右子树节点的值都大于这个节点的值。引进二叉查找树,是为了更高效地实现二叉树的查找和节点的增删,下图是典型的二叉查找树:

图片

      通过中序遍历,我们很容易得到一个按值大小排序的数据序列:13,16,17,18,25,33,34,50,51,58,66。正是这样一个按值大小排序的数据序列,让二叉树节点的查找和增删变得更加高效:当查询一个节点值时,可以从根节点开始比较。如果目标值小于当前节点值,则搜索左子树;如果目标值大于当前节点值,则搜索右子树。这样理论上,每次比较都可以排除掉一半的子树,而不需要遍历整个二叉树。特别是数据集足够大并且查询频繁发生,使用二叉查找树会显著提高性能,理想的情况下二叉查找树的时间复杂度可达到 O(log n)。接下来我们详细介绍二叉查找树的各种操作的实现:

        二叉查找树的查找操作

      二叉查找树的查找其实逻辑也很简单,如前文所述:从根节点开始查找,如果目标值小于根节点值,则搜索左子树;如果目标值大于根节点值,则搜索右子树,以此类推;下面代码通过循环和递归两种方式实现了查找逻辑:


public class BinaryTreeFind {

    private Node tree; //假设已初始化树及节点

    public Node find(int data) {
        Node p = tree;
        while (p != null) {
            if (data < p.data){
              p = p.leftNode;
            } else if (data > p.data) {
              p = p.rightNode;
            } else {
              return p;
            }
        }
        return null;
    }
    
    public Node find2(int data) {  
        if (tree != null){        
          if (data < tree.data) {
             tree = tree.leftNode;
             return find2(data);
          } else if (data > p.data) {
             tree  = tree.rightNode;
             return find2(data);
          } else {
             return p;
          }   
        }    
        return null;
    }


    class Node {
        private int data;
        private Node leftNode;
        private Node rightNode;

        public Node(int data) {
            this.data = data;
        }
    }
    
   
}
      
         二叉查找树的插入操作

        二叉查找树的插入,是以二叉查找树的查找为前提的,因为二叉查找树是一棵有顺序的树,你需要先找到要插入数据在二叉查找树的顺位。具体的逻辑是:从根节点点开始进行比较,小于根结点则与根结点的左子树进行比较,否则与右子树进行比较,直到左子树为空或右子树为空,则插入到相应为空的位置,代码如下:


/**
 * 插入操作
 *
 * @param data
 * @return
 */
public Boolean insert(int data) {
    if (tree == null) {
        tree = new Node(data);
        return true;
    }
    Node p = tree;
    while (p != null) {
        if (data > p.data) {
            // 插入右节点
            if (p.rightNode == null) {
                p.rightNode = new Node(data);
                return true;
            }
            p = p.rightNode;
        } else {
            // 插入 左节点
            if (p.leftNode == null) {
                p.leftNode = new Node(data);
                return true;
            }
            p = p.leftNode;
        }
    }
    return false;
}

        二叉查找树的删除操作

        二叉查找树的删除,相比二叉查找树查找和插入稍显复杂,需要区分三种情况,具体逻辑如下表:

图片

      上表中真正稍显复杂的是第三种场景:删除节点有两子节点的场景。这里稍作说明:二叉查找树的特点是,父节点的值大于左子树所有节点的值,少于右子树所有节点的值,用右子树的最小节点替代删除节点,符合二叉查找树的特点和规范;右子树最小节点既然替换到删除节点的位置,原本位置的最小节点自然要删除,而最小节点通常是子树最下层最左边的叶子节点,不再有子节点,因而直接把其父节点指向它的指针置为Null即完成了它的删除。代码如下:


/**
 * 删除
 * @param data
 */
public void delete(int data) {
    // p指向要删除的节点,初始化指向根节点
    Node p = tree;
    // pp记录的是p的父节点
    Node pp = null;

    // 查找要删除的节点位置,及其父节点
    while (p != null && p.data != data) {
        pp = p;
        if (data > p.data) {
            p = p.rightNode;
        } else {
            p = p.leftNode;
        }
    }
    if (p == null) {
        return;// 没有找到
    }
    // 要删除的节点有两个子节点
    if (p.leftNode != null && p.rightNode != null) {
        // 查找右子树中最小节点
        Node minp = p.rightNode;
        Node minpp = p; // minPP表示minP的父节点
        while (minp.leftNode != null) {
            minpp = minp;
            minp = minp.leftNode;
        }
        // 将 minp 的数据替换到 p 中
        p.data = minp.data;
        // 下面就变成了删除 minp 了
        p = minp;
        pp = minpp;
    }
    // 删除节点是叶子节点或者仅有一个子节点
    Node child; // p 的子节点
    if (p.leftNode != null) {
        child = p.leftNode;
    } else if (p.rightNode != null) {
        child = p.rightNode;
    } else {
        child = null;
    }
    if (pp == null) {
        // 删除的是根节点
        tree = child;
    } else if (pp.leftNode == p) {
        pp.leftNode = child;
    } else {
        pp.rightNode = child;
    }
}

//前述代码来源:https://www.cnblogs.com/xiexiandong/p/13060094.html

      关于二叉查找树的删除,有些文章建议不作物理删除,直接标记删除状态只做逻辑删除,这样处理虽然会额外占用一定内存但确实效率会更高,至于哪种方案更好还是需要结合具体的业务场景来判断。

      二叉查找树的时间复杂度分析

     文章前述内容提到理想情况下,二叉查找树的时间复杂度可以达到O(logn),那什么是最理想的情况,什么又是最坏的情况呢?我们先看正常情况下,二叉查找树的时间复杂度的推算过程:

图片

     上表中N代表二叉查找树的节点总数,K代表时间复杂度(时间复杂度本质上就是计算基本语句的执行次数,基本语句指算法中执行次数最多的语句,具体到这里指的是查询比较语句);而剩余待查节点数只会随着K的增大而逐步减少直到命中目标节点的上一层等于1,也就是待查剩余节点数=N/(2^K) >= 1,我们计算时间复杂度总是以最坏的情况计算,也就是查到剩余最后一个数才是我们想要的计算的场景,那么得出 N / (2^K) = 1,由此再推算出N = 2^K,再推出K =log2(N),即最后可推出二叉查找树剩余待查节点的查找时间复杂度为:O(log2(N)) => O(logN)。

     在上述推断的过程中, 其实隐含一个前提条件,那就是该二叉查找树是一棵满二叉树(关于满二叉树的知识,可参看前述文章 数据结构和算法之树形结构(1)),只有是在满二叉树的情况下,剩余待查节点数才会满足上表场景:第一次查询比较后是N/2,第二次查询比较后是N/(2^2) ......;我们假设另一种情况,假设二叉查找树是下列极端情况,如图:

图片

    这时再去推算它的查找时间复杂度则如下表:

图片

      因为此时二叉查找树基本已经退化成一个链表,每一层只有一个节点,每查询比较一次,剩余待查节点数减1,之前的 N / (2^K) 公式已经不再适用(自己细细琢磨);根据前述的推断逻辑,上表中N - K = 1可推出此时二叉查找树剩余待查节点的查找时间复杂度为:K = O(N),正是链表的时间复杂度(关于链表结构,可以参看前述文章 数据结构和算法之线性结构)。

      所以综上所述,二叉查找树时间复杂度最理想的情况是该二叉查找树是一棵满二叉树或者形态接近满二叉树,而最坏的情况是该二叉查找树退化成了一个链表。

      那讲到这里,你可能还会有疑惑?假设上面的满二叉查找树和已退化成链表的二叉查找树层数都为5,而要查询的节点都在第5层,按上面的公式两者命中的时间复杂度K不都等于5吗?那时间复杂度是O(N)和O(logN)又有什么区别呢?这里我想说的是,要比较时间复杂度的效率和优劣,应该是在处理同等数据规模的场景下去比较才有意义,例如我们把前面已经退化成链表的二叉查找树转化成下图的满二叉查树:

      这时两者节点数相等,我们再来查找同一节点5,你会发现满二叉查找树场景下命中的时间复杂度K为3,而链表场景下命中的时间复杂度K依然为5,从这个对比也不难看出,二叉查找树的时间复杂度和其树的层数或者高度成正比,而满二叉查找树之所以在同等数据规模下效率高于链表式二叉查找树,正是因为其树的层数或高度少于链表式二叉查找树。换一种说法就是,满二叉查找树的节点形态左右比较平衡,而链表式二叉查找树则极度不平衡,节点都偏离到了一条链上而导致树的层数或高度增加,从而效率低下。

      为了避免二叉查找树的极度不平衡带来的低效率,在数据结构和算法的实际应用中,又衍生出了平衡二叉树,而满二叉查找树正是平衡二叉树的一种特例。关于平衡二叉树的知识,我们将在后续章节继续介绍,特请关注!     

码农爱刷题

为计算机编程爱好者和从业人士提供技术总结和分享 !为前行者蓄力,为后来者探路!

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

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

相关文章

Fyne ( go跨平台GUI )中文文档-绘图和动画(三)

本文档注意参考官网(developer.fyne.io/) 编写, 只保留基本用法 go代码展示为Go 1.16 及更高版本, ide为goland2021.2 这是一个系列文章&#xff1a; Fyne ( go跨平台GUI )中文文档-入门(一)-CSDN博客 Fyne ( go跨平台GUI )中文文档-Fyne总览(二)-CSDN博客 Fyne ( go跨平台GUI…

继电器测试负载箱的维护和保养方法有哪些?

继电器测试负载箱是用于模拟各种电气负载的设备&#xff0c;广泛应用于继电器、接触器等电气元件的测试和校验。在日常使用中&#xff0c;为确保其正常运行和准确性&#xff0c;以下是一些常见的维护和保养方法&#xff1a; 1. 电源问题&#xff1a;如果电源电压不稳定或波动过…

PD 取电快充协议芯片 支持广泛应用,最高取电电压100W

XSP06是一款支持多协议的受电端取电快充芯片&#xff0c;支持PD2.0/3.0、QC2.0/3.0、华为FCP、三星AFC快充协议。它允许设备通过与快充适配器通信&#xff0c;有效地从适配器或车充等电源诱骗出所需要的电压为自身供电。 特性&#xff1a; 支持电压档位&#xff1a;XSP06支持触…

根据一级分类Id获取专辑标签(内连接,一对多)

文章目录 base_attributebase_attribute_value 1、BaseAttribute2、BaseAttributeValue3、BaseCategoryApiController --》findAttribute()4、BaseCategoryServiceImpl --》findAttribute()5、BaseAttributeMapper6、BaseAttributeMapper.xml 当选择完专辑分类之后&#xff0c;…

如何进行Ubuntu磁盘空间深度清理?

近期使用AutoDL算力云&#xff0c;发现系统盘只有30G&#xff0c;数据盘只有50G&#xff0c;跑一个稍微大一点的模型&#xff0c;马上空间就拉爆了&#xff0c;现在做一个磁盘深度清理操作&#xff0c;看看效果。 清理前磁盘占用如下&#xff1a; 在 Ubuntu 系统中进行磁盘深度…

二、MySQL环境搭建

文章目录 1. MySQL的卸载步骤1&#xff1a;停止MySQL服务步骤2&#xff1a;软件的卸载步骤3&#xff1a;残余文件的清理步骤4&#xff1a;清理注册表&#xff08;选做&#xff09;步骤5&#xff1a;删除环境变量配置 2. MySQL的下载、安装、配置2.1 MySQL的4大版本2.2 软件的下…

Linux环境的JDK安装

1.搜索可用的jdk yum search jdk/(或者是要安装的版本java-11)2.安装需要的版本 yum install java-11-openjdk.x86_643.验证是否安装成功 java -version4.配置环境变量 通过yum安装的默认路径为&#xff1a;/usr/lib/jvm cd /etc/profile.d/ touch java_home.sh vim java_…

Linux线程同步与互斥

&#x1f30e;Linux线程同步与互斥 文章目录&#xff1a; Linux线程同步与互斥 Linux线程互斥 线程锁       互斥量Mutex         初始化互斥量的两种方式         申请锁方式         解除与销毁锁 问题解决及线程饥饿       互斥锁的底…

线性调频信号脉冲压缩并非是一个门信号

如果是频域是门信号&#xff0c;时域是sinc信号&#xff0c;时间越长震荡只会越小。图象是线性卷积做的&#xff0c;肯定没错。

如何写出高收录词的listing文案,先做好这一点

在亚马逊上&#xff0c;关键词是连接买家搜索与产品之间的桥梁&#xff0c;超过80%的购买行为都是通过搜索关键词开始的。因此&#xff0c;文案中包含的精准关键词越多&#xff0c;Listing越能匹配买家的需求&#xff0c;从而提高自然排名并优化广告效果。 亚马逊的收录分为静…

【CSS Tricks】在css中尝试一种新的颜色模型HSL

目录 引言浏览器支持性HSL介绍HSL相较于RGB的优势在哪&#xff1f;HSL在网页设计的应用场景如何用代码转换hslRGB转HSLHSL转RGBHEX格式的互转 总结 引言 本篇不会对rgb颜色模型或是hsl颜色模型的显色原理进行深入的探究&#xff0c;仅从前端开发角度去论述在工作中选择哪种比较…

C/C++指针的前世今生

前言 老早之前就想写这个内容了&#xff0c;打了草稿后闲置了两个月&#xff0c;因为其他事就没再动过这个东西了&#xff0c;今天翻草稿箱的时候发现了它&#xff0c;就把它完善出来&#xff0c;顺便我也学习学习。 正文 指针的前世今生 前面先说一下&#xff0c;故事是随…

【第十七章:Sentosa_DSML社区版-机器学习之异常检测】

【第十七章&#xff1a;Sentosa_DSML社区版-机器学习之异常检测】 机器学习异常检测是检测数据集中的异常数据的算子&#xff0c;一种高效的异常检测算法。它和随机森林类似&#xff0c;但每次选择划分属性和划分点&#xff08;值&#xff09;时都是随机的&#xff0c;而不是根…

前端——实现时钟 附带小例子

创建日期对象 toLocaleDateString() 获取日期 console.log(date.toLocaleDateString()) toLocaleTimeString() 获取时间 console.log(date.toLocaleTimeString()) toLocaleString() 获取日期和时间 console.log(date.toLocaleString()) date.getDay() 获取星期几 周日为…

VisualStudio的“应用代码更改“按钮功能

无意发现这个按钮&#xff0c;因为开发这么多年也没专门尝试这个按钮&#xff0c;于是好奇它的功能。 光标放在按钮上面提示了“应用代码更改”&#xff0c;于是猜想应该是在调试不断开的情况下支持热应用更改。 经过验证&#xff0c;功能确实如同猜想的一样&#xff0c;具体验…

Leetcode 1039. 多边形三角形剖分的最低得分 枚举型区间dp C++实现

问题&#xff1a;Leetcode 1039. 多边形三角形剖分的最低得分 你有一个凸的 n 边形&#xff0c;其每个顶点都有一个整数值。给定一个整数数组 values &#xff0c;其中 values[i] 是第 i 个顶点的值&#xff08;即 顺时针顺序 &#xff09;。 假设将多边形 剖分 为 n - 2 个三…

力扣(leetcode)每日一题 1014 最佳观光组合

题干 1014. 最佳观光组合 给你一个正整数数组 values&#xff0c;其中 values[i] 表示第 i 个观光景点的评分&#xff0c;并且两个景点 i 和 j 之间的 距离 为 j - i。 一对景点&#xff08;i < j&#xff09;组成的观光组合的得分为 values[i] values[j] i - j &#…

windows 出现身份验证错误,要求的函数不受支持

现象环境&#xff1a; win10 mstsc内网远程server2016&#xff0c;出现错误代码&#xff1a; 远程桌面连接出现身份验证错误。要求的函数不受支持。这可能是由于CredSSP加密数据库修正 出现身份验证错误 原因&#xff1a; 系统更新&#xff0c;微软系统补丁的更新将 Cred…

<刷题笔记> 力扣236题——二叉树的公共祖先

236. 二叉树的最近公共祖先 - 力扣&#xff08;LeetCode&#xff09; 题目解释&#xff1a; 我们以这棵树为例&#xff0c;来观察找不同的最近公共祖先有何特点&#xff1a; 思路一&#xff1a; 除了第二种情况&#xff0c;最近公共祖先满足&#xff1a;一个节点在他的左边&am…

现代LLM基本技术整理

0 开始之前 本文从Llama 3报告出发&#xff0c;基本整理一些现代LLM的技术。基本&#xff0c;是说对一些具体细节不会过于详尽&#xff0c;而是希望得到一篇相对全面&#xff0c;包括预训练&#xff0c;后训练&#xff0c;推理&#xff0c;又能介绍清楚一些具体技术&#xff0…