【追求卓越11】算法--二叉树

引导

        接下来的几节我们开始介绍非线性的数据结构--树。树的内容比较多也比较复杂。本节,我们只需要了解关于树的一些基本概念。以及再进一步了解树的相关内容--搜索二叉树。该类型二叉树在工作中,是我们常接触的。该节我们介绍关于搜索二叉树的相关操作:查找,插入,删除。

什么是树?

关于树的定义:(摘自百度百科)

  1. 每个元素称为结点(node)
  2. 有一个特定的结点被称为根结点或树根(root)
  3. 除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)

说实话,这段定义我看了好几遍才能理解,对于刚接触的同学,结合图来理解是最好的了。

树中有很多需要我们了解的名词,我们结合下面的树来介绍:

        在上图中,A节点就是B节点的父节点,B节点是A节点的子节点。B,C,D这三个节点有共同的父节点,他们是兄弟节点。我们把没有父节点的节点称作为根节点,也就是图中的E节点。我们把没有子节点的的节点称作叶子节点或叶节点。

此外还有三个相似的概念:高度,深度,层。

节点的高度:节点到叶子节点最长路径。比如,上图中A节点的高度就是2.

节点的深度:根节点到节点的所经历的边数。比如,上图中B节点的深度是2.

节点的层:节点的深度+1。比如,上图中B子节点的层是3.

树的高度:根节点的高度。比如,上图中的树的高度是3.

以上就是关于树的部分名词定义了。但是我们工作中常接触的是二叉树。接下来,我们来正式进入今天的主题。

二叉树

        二叉树顾名思义,每个节点最多有两个分支,即仅有两个子节点。同理,有四个,八个分支的树就是四叉树,八叉树。是不是很容易理解?如图:

        上图中三个树都是二叉树,但是有两个树有比较特殊,需要注意。

        其中2号树中,叶子节点都在最低层,并且除了叶子节点,每个节点都有左右两个字节点。这种二叉树称作为满二叉树

        其中3号树中,叶子节点在最底下两层,最后一层的叶子节点都是靠左排列,并且除了最后一层,其他层的节点个数达到最大数,这种树称作完全二叉树。这个解释我觉得还不是很清晰,摘自百度百科:对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1n的结点一一对应时称之为完全二叉树

        到了这里,你可能会有疑问:满二叉树很容易理解,是二叉树的一种特例。但是完全二叉树存在的意义是什么

二叉树的存储方式

        我们常见的存储方式是基于指针或者引用的二叉链式存储法。他比较简单和直观,每个节点有一个数据段和两个左右指针。如图:

        另一种就是基于数组的顺序存储法。我们把根节点存储在下标i=1的位置,那左子节点存储的下标2 * i=2的位置,右子节点存储的下标2 * i+1的位置。

        根据这样的存储方式,刚刚的完全二叉树仅仅会浪费下标未0的空间,若不是完全二叉树就会出现浪费内存空间的现象。如图:

        所以对于完全二叉树,使用数组进行存储是最省内存的一种方式。因为顺序存储方式比链式存储方式更生内存,它不需要每个节点保存两个指针变量。

二叉树的遍历

对于二叉树的遍历,这个也是非常常见的面试题。常见的遍历方式是前序遍历,中序遍历,后序遍历。

前序遍历:对树中任意节点,先打印这个节点,然后打印它的左子树,最后打印右子树

中序遍历:对于树中任意节点,先打印这个节点的左子树,然后打印该节点,最后打印右子树

后序遍历:对于树中任意节点,先打印这个节点的左子树,然后打印右子树,最后打印改节点

对于遍历,实现方式也很简单,这里仅仅给出伪代码

void preOrder(Node* root) {
  if (root == null) return;
  print root //
此处为伪代码,表示打印root节点
  preOrder(root->left);
  preOrder(root->right);
}
void inOrder(Node* root) {
  if (root == null) return;
  inOrder(root->left);
  print root // 此处为伪代码,表示打印root节点
  inOrder(root->right);
}
void postOrder(Node* root) {
  if (root == null) return;
  postOrder(root->left);
  postOrder(root->right);
  print root // 此处为伪代码,表示打印root节点
}

每个节点最多被访问两次,故时间复杂度是O(n)。

搜索二叉树

        搜索二叉树又称为二叉查找树。顾名思义,二叉查找树是为了快速查找而诞生的。它不仅仅支持快速查找,还支持插入,删除。

定义:二叉查找树要求,在树中任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值。而右子树节点的值都大于这个节点的值

可参考下图:

搜索二叉树的查找

  1. 先一个节点(一般是root节点),判断是否是我们要查找的数据?
  2. 如果要查找的数据比该节点的大,我们就在右子树中继续查找
  3. 如果要查找的数据比该节点的小,我们就在左子树中继续查找
  4. 若未查找到,则返回空

从上面的思路可以看出,我们可以用递归或者循环来实现,下面贴上代码(由于环境原因,代码都没有运行,不过代码逻辑应该是这样的):

typedef strcut node{
    int data;
    struct node left;
    struct node right;
}Node;
/
*while
循环*
/
Node * BinarySearchTree(Node* root , int target)
{
    Node * p = root;
    while(p != NULL)
    {
        if(p.data == target)
            return p;
        else if (p.data > target)
            p = p.left;
        else
            p = p.right;
    }
    return NULL;
}
/
*递归*
/
Node * BinarySearchTree(Node* root , int target)
{
    if(root == NULL)
        return NULL;

    if(root.data == target)
    {
        return root;
    }
    else if(root.data > target)
    {
        return BinarySearchTree(root.left,target);
    }
    else if(root.data < target)
    {
        return BinarysearchTree(root.right,target);
    }
}

  1. 搜索二叉树的插入

        插入操作和查找操作类似,新插入的数据一般都是保存在叶子节点上的。这句话,我刚开始是抱有怀疑态度,通过自己的思考也就理解了。建议大家先理解这句话的意思。

若知道了新插入数据都是保存在叶子节点上的,那么插入操作的思路也就简单了。

思路:

  1. 先获取一个节点,和插入数据比较哪个要大?
  2. 若要插入的数据比节点的值大,并且右子树为空,即直接将数据插入到该节点的右子节点中。若不为空,继续遍历右子树
  3. 若要插入的数据比节点的值小,并且左子树为空,即直接将数据插入到该节点的左子节点中。若部位空,继续遍历左子树

typedef strcut node{
    int data;
    struct node left;
    struct node right;
}Node;
int BinarySearchInsert(Node * root , int target)
{
    Node * p = root ;
    while(p != NULL)
    {
        if(p.data < target)
        {
            if(p.right == NULL)
            {
                p.right = newNode(target);
                return 0;
            }
            p = p.right;
        }
        else
        {
            if(p.left == NULL)
            {
                p.left = newNode(target);
                return 0;
            }
            p = p.left;
        }
    }
    return -1;
}

搜索二叉树的删除操作

搜索二叉树的删除操作就比较复杂了,因为删除之后,你必须要保证剩下的树结构满足搜索二叉树定义。其情况大致可以分为以下三种:

  1. 删除节点没有子节点。删除一个节点,我们需要先找到这个节点,我们可以使用查找操作的逻辑。若删除节点没有子节点,也就不会影响树的结构。直接删除即可。将其父节点指向该节点的指针,指向NULL;
  2. 删除的节点有一个子节点。将删除节点的父节点指向该节点的指针修改为指向子节点即可。
  3. 删除节点有两个子节点。将删除节点的右子树中最小节点删除,并替换为该删除节点即可。这个可能比较抽象,可参数下图,删除18这个节点:

typedef strcut node{
    int data;
    struct node left;
    struct node right;
}Node;
int BinarySearchDelete(Node * root , int target)
{
    Node * p = root;
    Node * father = NULL

    Node * minfather = NULL;
    Node * min = NULL;
    while(p != NULL && p.data != target)
    {
        father = p; /
*保存父节点*
/
        if(p.data < target)
            p = p.left;
        else
            p = p.right;
    }
    if(p == NULL)
        return -1;/
*not find target*
/

    /
*删除节点有两个子节点*
/
    if(p.left != NULL && p.right != NULL)
    {
        minfather = p;
        min = p.right;

        while(min.left != NULL )
            minfather = min;
            min = min.left;

        /
*至此找到删除节点右子树中的最小节点*
/
        p.data = min.data; /
*这里我觉得使用memcpy(p,min,sizeof(Node))较好*
/

        /
*将删除的点变为只有一个或没有子节点*
/
        p = min;
        father = minfather;
    }

    /
*至此,p表示删除的节点,father表示删除节点的父节点,并且删除节点不会有两个子节点*
/
    Node * child = NULL;
    if(p.left != NULL)
        child = p.left;
    else if(p.right != NULL)
        child = p.right;

    if(father == NULL)/
*删除的是root节点*
/
        memcpy(root,child,sizeof(Node));
    else if(father.left == p)
        father.left = child;
    else
        father.right = child;

    free(p);
}

搜索二叉树的中序遍历

        搜索二叉树有一个非常强大的特性,那就是通过中序遍历,即可输出有序的数据序列,时间复杂度是O(n),非常高效。

如何支持重复数据

上面的操作,我们都是假设没有重复数据,但是显示工作中我们总会遇到相同key的节点(key表示作为搜索二叉树依据)。针对相同键值的节点,我们又有哪些方式处理呢?

  1. 利用数组或链表进行扩容,将相同键值的数据储存到同一个节点上。
  2. 每个节点只保存一个数据。在插入的过程中,如果遇到一个节点的值,与要插入的值相同。我们就将这个要插入的数据放到这个节点的右子树。也就是将这个新插入的数据当作大于这个节点的值来处理。后续的删除都是按照这个逻辑处理

二叉查找树的时间复杂度

实际上二叉树的形态各式各样,即使同一组数据,也可以有不同的形态。如图:

第一种情况完全退化成了链表,时间复杂度为O(n),第三种的效果应该会好一些。其实,我们可以知道时间复杂度其实跟树的高度成正比。也就是O(height)。根据完全二叉树可知,L 的范围是[log2(n+1), log2n +1]。这样就极大的提高了效率。因此我们能在实际工作中需要无论怎么删除或增加,都可以保证树的平衡。红黑树就是这样的一种树。时间复杂度为O(logn)

搜索二叉树相较于散列表的优点

虽然散列表在的时间复杂度达到了O(1),而查找二叉树在理想情况下,也是O(logn)。那么为什么,我们工作中,树的使用要高于散列表呢?

  1. 散列表是无序存储,如果要输出有序的数据,需要先进行排序。而对于二叉树查找而言,我们仅需中序遍历就可以得到有序序列。
  2. 散列表扩容是耗时较多(虽然我们有一些优化手段,分时),而且当遇到散列冲突时,性能不稳定。虽然搜索二叉树也不稳定,但我们在工作中常使用的是平衡二叉查找树。
  3. 因为散列冲突的存在,散列表的时间复杂度并不一定比logn小。实际查找速度不一定比O(logn)快。另外还有哈希函数的耗时。
  4. 综上几点,树在某些方面还是优于散列表的。

总结

        本节开始接触树的内容,了解到父节点,子节点,兄弟节点,根节点,叶子节点,高度,深度,层等概念

        树的种类有很多,工作中,我们常接触的就是二叉树。我们也介绍了完全二叉树和满二叉树的概念。以及二叉树存储的方式有基于指针的链式存储方式和基于数组的顺序存储方式。其中完全二叉树使用数组是最节省内存的方式。

        绍了什么是搜索二叉树,以及搜索二叉树的删除,增加,查找的的逻辑。

        也简单介绍了,当有相同的键值时,我们该如何处理:利用数组或链表扩容。或者默认为大于该节点。

        最后我们讲解了二叉树的三种遍历方式:前序遍历,中序遍历,后序遍历。以及代码的是实现方式。树结构相对于散列表的优势。

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

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

相关文章

1992-2021年省市县经过矫正的夜间灯光数据(GNLD、VIIRS)

1992-2021年省市县经过矫正的夜间灯光数据&#xff08;GNLD、VIIRS&#xff09; 1、时间&#xff1a;1992-2021年3月&#xff0c;其中1992-2013年为年度数据&#xff0c;2013-2021年3月为月度数据 2、来源&#xff1a;DMSP、VIIRS 3、范围&#xff1a;分区域汇总&#xff1a…

opencv-图像金字塔

图像金字塔是一种图像处理技术&#xff0c;它通过不断降低图像的分辨率&#xff0c;形成一系列图像。金字塔分为两种类型&#xff1a;高斯金字塔和拉普拉斯金字塔。 高斯金字塔&#xff08;Gaussian Pyramid&#xff09;&#xff1a; 高斯金字塔是通过使用高斯滤波和降采样&a…

计算机网络之概述

一、概述 1.1因特网概述 定义 网络(Network)由若干结点(Node)和连接这些结点的链路(Link)组成。多个网络还可以通过路由器互连起来&#xff0c;这样就构成了一个覆盖范围更大的网络&#xff0c;即互联网&#xff08;或互连网&#xff09;因此&#xff0c;互联网是“网络的网络…

企业该怎么选择IP证书

IP证书是一种数字证书&#xff0c;它由权威的数字证书颁发机构&#xff08;CA&#xff09;颁发&#xff0c;部署在只有公网IP地址的站点上&#xff0c;用于在网络中验证身份和保护信息安全。IP证书可以在各种场景下保护网站的信息安全&#xff0c;比如网站vip登录&#xff0c;线…

【docker】docker总结

一、Docker简介 Docker是开源应用容器引擎&#xff0c;轻量级容器技术。基于Go语言&#xff0c;并遵循Apache2.0协议开源Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的Linux系统上&#xff0c;也可以实现虚拟化容…

【计算机基础】通过插件plantuml,实现在VScode里面绘制状态机

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

DockerHub 无法访问 - 解决办法

背景 DockerHub 镜像仓库地址 https://hub.docker.com/ 突然就无法访问了,且截至今日(2023/11)还无法访问。 这对我们来说,还是有一些影响的: ● 虽然 DockerHub 页面无法访问,但是还是可以下载镜像的,只是比较慢而已 ● 没法通过界面查询相关镜像,或者维护相关镜像了…

2023年最佳Aspera替代方案,选择适合的Aspera替代方案

查找当前可用的Aspera替代方案。比较 2023年Aspera替代方案的评级、评论、定价和功能。列出了市场上最好的Aspera替代方案&#xff0c;它们提供与 IBM Aspera 类似的竞争产品。对下面的Aspera替代方案进行排序&#xff0c;以根据需求做出最佳选择。 1、镭速 镭速&#xff08;私…

【追求卓越02】数据结构--链表

引导 今天我们进入链表的学习&#xff0c;我相信大家对链表都很熟悉。链表和数组一样&#xff0c;作为最基础的数据结构。在我们的工作中常常会使用到。但是我们真的了解到数组和链表的区别吗&#xff1f;什么时候使用数组&#xff0c;什么时候使用链表&#xff0c;能够正确的选…

笔记本外接显示器的一些基本操作

1>&#xff0c;安装问题直接问客服&#xff0c;正常情况是将显示屏接上电源&#xff0c;然后用先将显示屏和笔记本的HDMI接口连接即可。 按下组合键 win p ,选择 “复制”。 2>&#xff0c;接上显示屏后&#xff0c;原笔记本无声音&#xff1f; 1、找到笔记本电脑右下…

通过Everything 建立HTTP服务器时指定文件夹共享

在局域网传输文件&#xff0c;高效传输&#xff0c;不限文件大小 1、安装Everything 2、在Everything开启HTTP服务 【工具】—>>【选项】—>>【HTTP服务】启用HTTP服务器&#xff0c;设置HTTP服务器用户名和密码 3、查看网络信息 打开服务端电脑的【命令提示…

低权限(无权限)时如何在mysql客户端控制台的大量输出中快速定位mysql死锁或慢sql

查看mysql的查看死锁的方式很多&#xff0c;但很多时候我们普通开发者的权限比较低&#xff0c;无法执行某命令。比如本次就准备使用 SHOW ENGINE INNODB STATUS;命令&#xff0c;但客户端提示权限不够。后来本人找到了另一条低权限的命令 show full PROCESSLIST;但是show fu…

CST同轴馈电步骤

CST同轴馈电步骤 算例1. 同轴内芯2. 填充材料3. 外皮4. GND减去一个圆形&#xff0c;使EMWAVE可以通过5. 添加端口6. 结果比较 算例 cst模型库中的一个圆贴片 1. 同轴内芯 2. 填充材料 他这里直接使用和介质基板一样的材料并且进行了合并&#xff0c;我就懒得再改了&#x…

Matplotlib颜色条的配置_Python数据分析与可视化

Matplotlib颜色条配置 基本颜色颜色条选择配色方案颜色条刻度的限制与扩展功能的设置离散型颜色条 基本颜色 Matplotlib提供了8种指定颜色的方法&#xff1a; 在[0&#xff0c;1]中的浮点值的RGB或RGBA元组&#xff08;例如 (0.1, 0.2, 0.5) 或&#xff08;0.1&#xff0c; 0.…

3D电路板在线渲染案例

从概念上讲,这是有道理的,因为PCB印制电路板上的走线从一个连接到下一个连接的路线基本上是平面的。 然而,我们生活在一个 3 维世界中,能够以这种方式可视化电路以及相应的组件,对于设计过程很有帮助。本文将介绍KiCad中基本的3D查看功能,以及如何使用NSDT 3DConvert在线…

PHP中间件实现

目录 1、简单中间实现 2、使用闭包函数实现中间件 在PHP中&#xff0c;中间件是一种常用的设计模式&#xff0c;用于处理请求和响应&#xff0c;它可以在请求到达目标处理程序之前或响应发送给客户端之前执行一些特定的逻辑。中间件提供了一种灵活的方式来修改或扩展应用程序的…

数据库数据恢复—MongoDB数据库文件拷贝出现错误的数据恢复案例

MongoDB数据库数据恢复环境&#xff1a; 一台Windows Server操作系统的虚拟机&#xff0c;虚拟机上部署有MongoDB数据库。 MongoDB数据库故障&检测&#xff1a; 在未关闭MongoDB服务的情况下&#xff0c;工作人员将MongoDB数据库文件拷贝到其他分区&#xff0c;然后将原数…

Dubbo从入门到上天系列第十八篇:Dubbo引入Zookeeper等注册中心简介以及DubboAdmin简要介绍,为后续详解Dubbo各种注册中心做铺垫!

文章目录 一&#xff1a;Dubbo注册中心引言 1&#xff1a;什么是Dubbo的注册中心&#xff1f; 2&#xff1a;注册中心关系图解 3&#xff1a;引入注册中心服务执行流程 4&#xff1a;Dubbo注册中心好处 5&#xff1a;注册中心核心作用 二&#xff1a;注册中心实现方案 …

bclinux aarch64 ceph 14.2.10 云主机 性能对比FastCFS vdbench

部署参考 ceph-deploy bclinux aarch64 ceph 14.2.10-CSDN博客 ceph-deploy bclinux aarch64 ceph 14.2.10【3】vdbench fsd 文件系统测试-CSDN博客 ceph 14.2.10 aarch64 非集群内 客户端 挂载块设备-CSDN博客 FastCFS vdbench数据参考 bclinux aarch64 openeuler 20.03 …

每日一题:LeetCode-589.N叉树的前序遍历序列构造二叉树

每日一题系列&#xff08;day 01&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50e…