C++数据结构——二叉搜索树

news2025/2/24 7:16:43

二叉搜索树的概念

二叉树又称二叉排序树(BST,Binary Search Tree),它是一颗空树,也可以是一颗具有下列性质的二叉树:

1.假如它的左子树不为空,那么左子树上的结点值都小于根结点的值。

2.假如它的右子树不为空,那么右子树上的结点值都大于根结点的值。

3.它的左右子树都分别为二叉搜索树

可以看看下图的例子:

再看这颗二叉树搜索树:

 

假如我们中序遍历,那么就会得到:

1 3 4 6 7 8 10 13 14

 可以看到,对二叉树搜索树进行中序遍历,得到的结果为升序序列

二叉搜索树的实现

结点类

在一整颗树中,当然会有多个结点,那么我们就定义一个结点类,里面包含了结点的值,结点的左指针,结点的右指针。

//结点类
template<class T>
struct TreeNode
{

    T _val;//结点值
    TreeNode<T>* _left;//左树指针
    TreeNode<T>* _right;//右树指针

    //构造函数
    TreeNode(const T& val = 0)
        :_val(val)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

 二叉搜索类函数要素

在一整颗二叉树搜索树中,我们需要定义一个最初的根结点,假如这颗树为空树,那么我们就令根结点为空,所以我们定义一个成员变量_root作为成员变量。

template<class T>
class FindTree
{
public:
    typedef TreeNode<T> node;
    //构造函数
    FindTree()

    //拷贝构造副函数
    node* _copy(node* root)

    //拷贝构造主函数
    FindTree(const FindTree<T>& t)
    
    //赋值运算符重载函数(深拷贝)
    FindTree<T>& operator=(FindTree<T> t)

    //析构副函数
    void _destory(node* root)

    //析构主函数
    ~FindTree()

    //插入函数
    bool insert(const T& val)

    //插入副函数(递归实现)
    bool _insertR(node*& root, const T& val)

    //插入主函数(递归)
    bool insertR(const T& val)
    //删除函数
    bool erase(const T& val)
    
    //搜索函数
    node* find(const T& val)

    //搜索副函数(递归)
    node* _findR(node* root, const T& val)

    //搜索主函数(递归)
    node* findR(const T& val)

private:
    node* _root;//整个树的根节点
};

构造函数

构造函数很简单,只需要令根节点为空就好了,因为一开始啥也没有,是颗空树

    typedef TreeNode<T> node;//为了方便命名
    //构造函数
    FindTree()
        :_root(nullptr)//将最初的根结点初始化为空指针
    {}

拷贝构造函数

这里有一个很有意思的点,为什么要写一个副函数?

假如我们将所有代码都写在主函数,然后我们在main函数中调用的时候就会出现问题,我们如何将_root传给拷贝构造函数呢?_root是这个类的私有成员,在外部并不能调用,只能在类内进行调用,所以我们写了个主函数,主函数来将_root传给副函数,然后副函数进行操作,然后在外部调用主函数从而完成拷贝构造,这是一个很巧妙的方法。

 注意:这里的拷贝构造完成的是深拷贝

    //拷贝构造副函数
    node* _copy(node* root)
    {
        if (root == nullptr)
        {
            return nullptr;//如果为空则返回空
        }
        node* copynode = new node(root->key);//拷贝根节点
        copynode->_left = _copy(root->left);//进入左树递归拷贝
        copynode->_right = _copy(root->right);//进入右树递归拷贝
        return copynode;//返回拷贝后的树的根结点
    }

    //拷贝构造主函数
    FindTree(const FindTree<T>& t)
    {
        _root = _copy(t._root);//从根结点进入递归拷贝
    }

赋值运算符重载函数

这里直接将实参传给形参,然后用形参与左值调用swap函数进行调换,那么此时左值就完成了赋值,然后函数操作完后,形参自动就会析构。

    //赋值运算符重载函数(深拷贝)
    FindTree<T>& operator=(FindTree<T> t)
    {
        swap(_root, t._root);
        return *this;
    }

析构函数

这里采用副函数的意义也是和前面一样,为了能够调用到私有成员_root。

注意:这里的释放方式应该采用后序方法,假如先把根结点释放掉了,那么根结点的左指针与右指针都找不到了,就会析构失败,存在内存泄露的风险。

    //析构副函数
    void _destory(node* root)
    {
        if (root == nullptr)//空树不需要释放
        {
            return;
        }

        _destory(root->_left);//进入左树递归删除
        _destory(root->_right);//进入右树递归删除
        delete root;//释放根结点
    }

    //析构主函数
    ~FindTree()
    {
        _destory(_root);//从根结点开始释放
        _root = nullptr;//将跟节点置空,防止野指针
    }

插入函数

在前面已经了解到了二叉搜索树的性质,现在我们就可以理解一下插入函数了:

一.如果是空树,那么直接插入结点作为二叉搜索树的根结点。

二.如果不是空树,则按照二叉搜索树的性质进行插入:

1.假如插入的结点值小于根结点的值,那么需要将结点插入到左子树当中。(进入左子树继续搜索)

2.假如插入的结点值大于根结点的值,那么需要将结点插入到右子树当中。(进入右子树继续搜索)

3.假如插入的结点值等于根结点的值,那么就不需要插入了,插入失败。

然后不断循环,直到遇到一的情况,也就是遇到空树即可插入,如果遇到相同的值,那么就插入失败。

第一种方法是非递归

要先定义一个parent指针和cur指针,parent记录cur的父亲结点,cur记录当前需要对比的结点,因为当我们找到需要插入的节点时,需要连接父亲结点,这时就要用到parent指针了

    //插入函数
    bool insert(const T& val)
    {
        if (_root == nullptr)//找到空的地方将值插入
        {
            _root = new node(val);
            return true;
        }
        node* parent = nullptr;//记录父亲指针
        node* cur = _root;//记录当前指针
        while (cur)//不为空时候,一直循环查找
        {
            if (val < cur->_val)//插入的值比结点值小
            {
                parent = cur;//更新父亲结点
                cur = cur->_left;//令当前指针往左走
            }
            else if (val > cur->key)//插入的值比结点大
            {
                parent = cur;//更新父亲结点
                cur = cur->_right;//令当前指针往右走
            }
            else//插入的值与结点相同
            {
                return false;//不需要插入,插入失败
            }
        }
        //当结束循环,说明找到空树,那么可以进行插入
        cur = new node(val);
        if (val < parent->_val)//插入的值比父亲结点小
        {
            parent->_left = cur;//往左边插入
        }
        else//插入的值比父亲结点大
        {
            parent->_right = cur;//往右边插入
        }
        return true;//插入成功
    }

 第二种方法是递归法

当插入值比结点值小,那么进入左树递归。

当插入值比结点值大,那么进入右树递归。

直到遇到空树,那么此时就可以直接new一个新结点。

如果遇到相同的值,那么直接返回false结束递归。

这时又会有同学问:为什么这里的是node*& root,为什么不需要对new的结点连接?

首先,node*&的含义是:指针的引用,然后再看我们调用递归的两个函数,每次进入递归,root都是root->_left或者root->_right的指针的引用,所以这时候就可以理解了:此时我们new的结点就是new给root->_left或者root->_right,那么这时候就可以看成root->_left = new或者root->_right = new。

    //插入副函数(递归实现)
    bool _insertR(node*& root, const T& val)
    {
        if (root == nullptr)//找到空树,进行插入
        {
            root = new node(val);
            return true;
        }
        if (val < root->_val)//值比结点小
        {
            return _insertR(root->_left, val);//进入左树递归
        }
        else if (val > root->_val)//值比结点大
        {
            return _inserR(root->_right, val);//进入右树递归
        }
        else//值与结点相同
        {
            return false;//不插入
        }
    }

    //插入主函数(递归)
    bool insertR(const T& val)
    {
        return _inserR(_root, val);
    }

删除函数

要想删除某个结点,要先考虑删除的情况:

1.要删除的结点没有左右子树。

2.要删除的结点只有左子树。

3.要删除的结点只有右子树。

4.要删除的结点同时有左右子树。

在处理以上四种情况时,我们可以归类成三种,将1归类到2或者归类到3

因为在2情况中,删除后需要将父结点指向结点的左子树。在3情况中,删除后需要将父结点指向结点的右子树,也就是需要继承他们的孩子。那么1情况中,因为左右子树都为空,所以我们将父节点指向左右子树都行。

综上所述,情况分为了三种(去掉1)分析:

1.要删除的结点只有左子树。

先判断要删除的结点到底是父结点的左子树还是右子树,让父结点指向该结点的左子树,后删除该节点,先后顺序不可调换。

2.要删除的结点只有右子树。

先判断要删除的结点到底是父结点的左子树还是右子树,让父结点指向该结点的右子树,后删除该节点,先后顺序不可调换。

3.要删除的结点同时有左右子树。

同时拥有左右子树很难处理删除后父亲结点的继承问题,因为一个指针不能继承两个结点。这时需要采用替换法,先找到左子树中最大值,或者找到右子树的最小值,然后将两个值替换,然后删除原本中左子树中最大值的结点或者右子树的最小值的结点。

需要注意的是,在1和2的情况下,如果要删除的结点为整颗树的根结点,直接让根结点变成1和2情况下存在的子树根结点即可

在3的情况中,我们还需要定义一个minparent和mincur指针,minparent用来记录寻找左树最大结点的父节点或者寻找右树最小结点的父节点,mincur用来记录记录寻找左树最大的当前结点或者寻找右树最小结点的当前结点。

    //删除函数
    bool erase(const T& val)
    {
        node* parent = nullptr;//记录父亲结点
        node* cur = _root;//记录当前结点
        while (cur)
        {
            if (val < cur->_val)//值小,更新父亲结点,则左走
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (val > cur->_val)//值大,更新父亲结点,则右走
            {
                parent = cur;
                cur = cur->_right;
            }
            else//相同,则找到要删除的结点
            {
                if (cur->_left == nullptr)//左树为空
                {
                    if (cur == _root)//如果要删除的就是整棵树的根结点
                    {
                        _root = cur->_right;//根结点转变成右树
                    }
                    else
                    {
                        if (cur == parent->_left)//如果当前结点是父结点的左结点
                        {
                            parent->_left = cur->_right;
                        }
                        else//如果当前结点是父结点的右结点
                        {
                            parent->_right = cur->_right;
                        }
                    }
                    delete cur;//删除原结点
                    return true;
                }
                else if (cur->_right == nullptr)//右树为空
                {
                    if (cur == _root)//如果要删除的就是整棵树的根结点
                    {
                        _root = cur->_left;//根结点转变成右树;
                    }
                    else
                    {
                        if (cur == parent->_left)//如果当前结点是父结点的左树
                        {
                            parent->_left = cur->_right; //父结点的左树指向当前结点的右树
                        }
                        else//如果当前结点是父结点的右结点
                        {
                            parent->_right = cur->_right;//父结点的右树指向当前结点的右树
                        }
                    }
                    delete cur;//删除原结点
                    return true;
                }
                else//如果该结点同时拥有左右子树,可以用替换法删除。1.选取左子树最大的值替换。2.选取右子树最小的值替换
                    //这里采用2.右子树最小的值进行替换
                {
                    node* minparent = cur;//记录最小结点的父结点
                    node* mincur = cur->_right;//进入右子树
                    while (mincur->_left)//如果左子树不为空,一直循环,直到找到最小值
                    {
                        minparent = mincur;
                        mincur = mincur->_left;
                    }
                    cur->_val = mincur->_val;//交换要删除的值与找到的最小值结点
                    if (mincur = minparent->_left)//如果当前指针为父结点的左
                    {
                        minparent->_left = mincur->_right;
                    }
                    else //如果当前指针为父结点的右,也就是要删除的结点的右结点就是最小的(删除结点的右结点没有左结点)
                    {
                        minparent->_right = mincur->_right;
                    }
                    delete mincur; //释放最小结点的指针
                    return true;
                }
            }
        }
        return false;
    }

查找函数

这个函数比较简单,一直通过对比大小进入左或右子树,然后找到相同的就返回地址即可。

第一种方法,非递归

    //搜索函数
    node* find(const T& val)
    {
        node* cur = _root;
        while (cur)
        {
            if (val < cur->_val)//比结点小,往左走
            {
                cur = cur->_left;
            }
            else if (val > cur->_val)//比结点大,往右走
            {
                cur = cur->_right;
            }
            else//相同,找到,返回结点地址
            {
                return cur;
            }
        }
    }

第二种方法,递归

这里为了调用私有成员_root,也是通过主副函数进行操作。

    //搜索副函数(递归)
    node* _findR(node* root, const T& val)
    {
        if (root == nullptr)//没找到,返回空指针
        {
            return nullptr;
        }
        if (val < root->val)//比结点小。进入左树递归
        {
            return _findR(root->_left, val);
        }
        else if (val > root->val)
        {
            return _findR(root->_right, val);//比结点大,进入右树递归
        }
        else//相同,返回结点地址
        {
            return root;
        }
    }

    //搜索主函数(递归)
    node* findR(const T& val)
    {
        return _findR(_root, val);
    }

二叉搜索树的应用

K模型

K模型即只有ker作为关键码,结构中只需要存储key即可,关键码即为搜索到的值

 例如:给一个单词word,判断该单词是否拼写正确,具体判断方式如下:

1.以词库中的所有单词分别作为key,构建一颗二叉搜索树。

2.在二叉搜索树中搜索该单词是否存在,存在就正确,不存在就错误。

KV模型

每一个关键码key,都对应着值value,也就是<key,value>键值对

1.例如在英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与对应的中文<word,chinese>就构成一种键值对。

2.再例如统计单词次数,统计成功后,给定单词就可以快速找到其对应的次数,单词与其出现的次数<word,count>就是一种键值对。

二叉搜索树的性能分析

在进行插入和删除操作的时候,都需要先查找,查找效率就代表了二叉搜索树中各个操作的性能。

例如,对有n个结点的二叉搜索树,假如每个元素查找的概率相同,那么二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,也就是结点越深,比较次数越多。

但是对于同一个关键码集合,如果各个关键码插入的次序不同,那么得到的二叉搜索树的结构也不同,例如下列树:

最优情况下,二叉搜索树为完全二叉树或者接近完全二叉树,那么比较的平均次数为:logn,最长时间复杂度为O(logn)

最差情况下,二茬搜索树为单支树,或者接近单支树,那么比较的平均次数为:n/2,最长时间复杂度为O(n)

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

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

相关文章

K8S controller编写之Informer的原理+使用[drift]

概念 核心思想&#xff08;重点&#xff09;watch-list 机制 Watch 通过 HTTP 协议与 Kubernetes API Server 建立长连接&#xff0c;接收 Kubernetes API Server 发来的资源变更事件。Watch 操作的实现机制使用 HTTP 协议的分块传输编码——当 client-go 调用 Kubernetes API…

nacos(docker部署)+springboot集成

文章目录 说明零nacos容器部署初始化配置高级配置部分访问权限控制命名空间设置新建配置文件 springboot配置nacos添加依赖编写测试controller 说明 nacos容器部署采用1Panel运维面板&#xff0c;进行部署操作&#xff0c;简化操作注意提前安装好1Panel和配置完成docker镜像加…

三、VUE数据代理

一、初识VUE 二、再识VUE-MVVM 三、VUE数据代理 Object.defineProperty() Object.defineProperty() 静态方法会直接在一个对象上定义一个新属性&#xff0c;或修改其现有属性&#xff0c;并返回此对象。 Object.defineProperty() 数据代理 通过一个对象代理另一个对象中属…

CSS 06

精灵图 为什么要使用精灵图 一个网页中往往会应用很多小的背景图像作为修饰&#xff0c;当网页中的图像过多时&#xff0c;服务器就会频繁地接收和发送请求图片&#xff0c;造成服务器请求压力过大&#xff0c;这将大大降低页面的加载速度,因此&#xff0c;为了有效地减少服务…

Python来计算 1,2,3,4 能组成多少个不相同且不重复的三位数?

我们今天的例子是 有 1&#xff0c;2&#xff0c;3&#xff0c;4 四个数字&#xff0c;它们能组成多省个互不相同且无重复的三位数&#xff1f;都分别是多少&#xff1f; 话不多说&#xff0c;我们先上代码 num 0 # 我们写了三个for循环&#xff0c;表示生成的三位数 for i…

YOLOv5模型训练处理自己数据集(标签统计、数据集划分、数据增强)

上一节中我们讲到如何使用Labelimg工具标注自己的数据集&#xff0c;链接&#xff1a;YOLOv5利用Labelimg标注自己数据集&#xff0c;完成1658张数据集的预处理&#xff0c;接下来将进一步处理这批数据&#xff0c;通常是先划分再做数据增强。 目录 一、统计txt文件各标签类型…

在项目中添加日志功能-Python logging模块新手入门

Python Logging 日志模块新手入门 这也是规划里的一篇工具文章&#xff0c;在写项目代码的时候不但要考虑代码的架构代码的后期维护和调试等也是一个比较关键的问题&#xff0c;之前写代码的时候日志这块的代码直接是任务驱动简单搜了一下就用了&#xff0c;但是秉持着打好基础…

十八、Java解析XML文件

1、XML文档语法和DTD约束 1)XML定义 XML即可扩展的标记语言,可以定义语义标记(标签),是元标记语言。XML不像超文本标记语言HTML,HTML只能使用规定的标记,对于XML,用户可以定义自己需要的标记。 XML(Extensible Markup Language)和HTML(Hyper Text Markup Language)师出同…

智能体可靠性的革命性提升,揭秘知识工程领域的参考架构新篇章

引言&#xff1a;知识工程的演变与重要性 知识工程&#xff08;Knowledge Engineering&#xff0c;KE&#xff09;是一个涉及激发、捕获、概念化和形式化知识以用于信息系统的过程。自计算机科学和人工智能&#xff08;AI&#xff09;历史以来&#xff0c;知识工程的工作流程因…

救护员证学习笔记

第一节 红十字运动基础知识 红十字运动的优势 197个主权国家、191个红十字会 四次获得诺贝尔和平奖 红十字运动的组成 红十字运动七项准则 红十字运动的标志 新中国红十字运动宗旨 保护人的生命与健康 维护人的尊严 发扬人道主义精神 促进和平事业进步 红十字会的主要工作 …

VGG16简单部署(使用自己的数据集)

一.注意事项 1.本文主要是引用大佬的文章&#xff08;侵权请联系&#xff0c;马上删除&#xff09;&#xff0c;做的工作为简单补充 二.介绍 ①简介&#xff1a;VGG16是一种卷积神经网络模型&#xff0c;由牛津大学视觉几何组&#xff08;Visual Geometry Group&#xff09;开…

【错题集-编程题】组队竞赛(排序 + 贪心)

牛客对应题目链接&#xff1a;组队竞赛_牛客笔试题_牛客网 (nowcoder.com) 一、分析题目 运用 贪心 思想&#xff1a; 先将数组排好序。总和最大 -> 每个小组的分数尽可能大。最大的数拿不到&#xff0c;只能退而求其次拿到倒数第⼆个⼈的分数&#xff0c;再补上一个小的数…

shell脚本-监控系统内存和磁盘容量

监控内存和磁盘容量除了可以使用zabbix监控工具来监控&#xff0c;还可以通过编写Shell脚本来监控。 #! /bin/bash #此脚本用于监控内存和磁盘容量&#xff0c;内存小于500MB且磁盘容量小于1000MB时报警#提取根分区剩余空间 disk_size$(df / | awk /\//{print $4})#提取内存剩…

西圣发布全新磁吸无线充电宝:打破传统,让充电更加高效、便捷

手机作为日常生活中最不能离开的数码单品之一&#xff0c;出门在外&#xff0c;电量情况总是让人担忧&#xff0c;一款靠谱的移动电源简直就是救星&#xff01;近日&#xff0c;西圣品牌推出了一款集高效、安全、便携于一体的无线充电宝——西圣PB无线磁吸充电宝&#xff0c;以…

Maven解决找不到依赖项

报错如图 方案一&#xff1a;Maven的Setting文件中添加albaba的镜像文件 1.下载maven &#xff1a;Maven – Download Apache Maven 2. 配置镜像 更改成这个&#xff1a; <mirror> <id>alimaven</id> <name>aliyun maven</name> <url&g…

webpack 常用插件

clean-webpack-plugin 这个插件的主要作用是清除构建目录中的旧文件&#xff0c;以确保每次构建时都能得到一个干净的环境。 var { CleanWebpackPlugin } require("clean-webpack-plugin") const path require("path");module.exports {mode: "de…

第十五届蓝桥杯省赛第二场C/C++B组H题【质数变革】题解

解题思路 首先&#xff0c;我们考虑一下整个数组都是由质数构成的情况。 当我们要将质数 x x x 向后移 k k k 个时&#xff0c;如果我们可以知道质数 x x x 在质数数组的下标 j j j&#xff0c;那么就可以通过 p r i m e s [ j k ] primes[j k] primes[jk] 来获取向后…

牛客NC279 二叉树的下一个结点【中等 二叉树中序遍历 C++/Java/Go/PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e 思路 思路&#xff1a;我们首先要根据给定输入中的结点指针向父级进行迭代&#xff0c; 直到找到该树的根节点&#xff1b;然后根据根节点进行中序遍历&#xff0c;当遍历到和…

Java学习第01天-Java基本内容

目录 注释 注释 单行注释 public class note {public static void main(String[] args) {// 单行注释} }多行注释 public class note {public static void main(String[] args) {/* 多行注释多行注释*/} }文档注释&#xff08;GPT生成&#xff09; /*** 计算两个整数…

webpack3升级webpack4遇到的各种问题汇总

webpack3升级webpack4遇到的各种问题汇总 问题1 var outputNamecompilation.mainTemplate.applyPluginWaterfull(asset-path,outputOptions.filename,{......)TypeError: compilation.mainTemplate.applyPluginsWaterfall is not a function解决方法 html-webpack-plugin 版…