c++ 实现二叉搜索树

news2024/11/29 20:40:08

二叉搜索树的概念

二叉搜索树 (BST,Binary Search Tree),也称二叉排序树或二叉查找树。它要么是一颗空树,要么是满足以下性质的二叉树:

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

在这里插入图片描述

如上图,左侧的二叉树不是一颗二叉搜索树,右侧的二叉树是一颗二叉搜索树。

**为什么叫二叉搜索树(二叉查找树)呢?**因为二叉搜索树擅长搜索和查找。如下图的二叉搜索树,我们要查找 10,首先与根节点比较,10 大于 9,那么就去他的右子树查找。右子树的根节点为 13 大于 10,那么就去他的左子树查找。左子树的根节点为 10 刚好等于 10,查找成功!如果节点不存在那么就是查找到树的叶节点还没有找到目标节点。

在这里插入图片描述

理想状态下,二叉搜索树查找的时间复杂度为:O(logN)。但是理想很丰满,现实很骨干。有一种情况能使得二叉搜索树变成链表,从而使得查找的时间复杂度变为 O(N)。

如下图,当依次向一颗空的二叉搜索树中插入有序的节点。那么这颗二叉搜索树就会变成链表。

在这里插入图片描述

**为什么叫二叉排序树呢?**那是因为一颗二叉搜索树的中序遍历的结果是有序的!中序遍历就是在递归遍历二叉树的时候先访问左子树,在访问根节点,最后才是右子树。不会中序遍历不要紧,等我们实现了一颗二叉搜索树,中序遍历一次你就知道他为啥叫二叉排序树了!

二叉搜索树的实现

二叉搜索树的基本结构

定义二叉搜索树的基本结构:

  • 首先,我们需要定义一个节点的类,表示二叉树的一个节点,成员变量就是左子树的节点指针,右子树的节点指针以及当前节点存储的值。构造函数的话,就是传入一个 key 用来初始化节点的 _key 就行啦!指针全部初始化为 nullptr
  • 然后就要定义二叉搜索树的类啦,里面会封装各种操作的函数。至于成员变量,当然就是根节点的指针啦!至于构造函数,一开始是一颗空的二叉搜索树嘛,将根节点的指针初始化为 nullptr 就行啦!
template<class K>
struct BSTNode
{
    BSTNode<K>* _left;
    BSTNode<K>* _right;
    K _key;

    BSTNode(const K& key)
        :_key(key)
        ,_left(nullptr)
        ,_right(nullptr)
    {}
};

template<class K>
class BSTree
{
    typedef BSTNode<K> Node;
public:
    BSTree()
        :_root(nullptr)
    {}

private:
    Node* _root;
};

bool insert(const K& key)

在我们实现的二叉搜索树中,一颗树中是没有相同节点的。

插入的过程其实很简单呢?将待插入的节点 key 与根节点的 _key 作比较:

  • 如果待插入节点的 key 大于根节点的 _key,那么继续与右子树根节点的 _key 比较。
  • 如果带插入节点的 key 小于根节点的 _key,那么继续与左子树根节点的 _key 比较。
  • 如果遇到根节点为 nullptr,那么直接插入这个节点,从而完成二叉搜索树的插入操作。

在这里插入图片描述

bool insert(const K& key)
{
    Node* newNode = new Node(key); //构造新插入的节点
    if(_root == nullptr) //如果是一颗空的二叉搜索树,修改根节点就行了
    {
        _root = newNode;
    }
    else
    {
        Node* cur = _root; //新插入的节点每次都与 cur 比较,判断其插入位置
        Node* parent = nullptr; //记录上父节点,方便最后的插入
        while(cur)
        {
            if(key > cur->_key) //key 大于 cur->_key 去右子树
            {
                parent = cur;
                cur = cur->_right;
            }
            else if(key < cur->_key) //key 小于 cur->_key 去左子树子树
            {
                parent = cur;
                cur = cur->_left;
            }
            else //相等的情况不存在,我们规定二叉搜索树中不存在 _key 值相同的节点,插入失败
            {
                return false; 
            }
        }
        //确定插入的位置
        if(key > parent->_key)
            parent->_right = newNode;
        else
            parent->_left = newNode;
    }
    //插入成功
    return true;
}

为了方便测试插入的结果是否正确,我们需要写一个中序遍历的函数,中序遍历在 C 语言阶段都是写过了的!忘记了的 uu 可以去复习复习。

C语言数据结构初阶(10)----二叉树的实现-CSDN博客

写成成员函数,通过类的对象调用,我们需要传参根节点的指针,但是这个成员变量是私有的!外面拿不到,你可以写一个函数获取根节点的指针,但是这里会有一个更加优雅的写法:

void inorder()
{
    _inorder(_root);
    cout << endl;
}

void _inorder(Node* root)
{
    if(root == nullptr)
        return;
    _inorder(root->_left);
    cout << root->_key << " ";
    _inorder(root->_right);
}

中序遍历写好了,就方便我们测试一颗二叉树是不是二叉搜索树啦!二叉搜索树的中序遍历是有序的,中序遍历有序的树也是二叉搜索树。

#include<iostream>
using namespace std;
#include"BSTree.h"

int main()
{
    BSTree<int> bst;
    int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
    for(auto e : a)
        bst.insert(e);
    bst.inorder();
    return 0;
}

在这里插入图片描述

我们看到中序遍历的结果的确是有序的呢!那我们的插入函数就是没有问题的!

bool find(const K& key)

find 写起来比 insert 还简单哈!

  • 如果待查找的 key 比根节点的 _key 大,那么就去右子树查找。
  • 如果待查找的 key 比根节点的 _key 小,那么就去左子树查找。
  • 如果待查找的 key 与根节点的 _key 相等,那么查找成功。
  • 如果根节点为 nullptr 还没有查找成功,那么查找失败。
bool find(const K& key)
{
    Node* cur = _root;
    while(cur)
    {
        if(key > cur->_key)
            cur = cur->_right;
        else if(key < cur->_key)
            cur = cur->_left;
        else
            return true;
    }
    return false;
}

void erase(const K& key)

erase 接口是二叉搜索树中最难实现的接口呢!

删除二叉搜索树中的节点可以分为以下情况:

  • 如果删除节点的度为 0,即删除的节点没有左右孩子。那么直接删除这个节点,然后改变父节点指针的指向就可以啦!

    在这里插入图片描述

  • 如果删除节点的度为 1,即删除的节点有一个孩子。那么,删除这个节点之后,令父节点相应的指针指向该删除节点的孩子即可。

    什么是父节点的相应指针?

    • 如果待删除的节点是其父节点的右孩子,那么删除这个节点之后,令父节点的 _right 指针指向该删除节点的孩子即可。
    • 如果待删除的节点是其父节点的左孩子,那么删除这个节点之后,令父节点的 _left 指针指向该删除节点的孩子即可。

    在这里插入图片描述

  • 如果待删除节点的度为 2,即待删除的节点有两个孩子。此时我们选择使用替换法,即选择待删除节点的左右子树中的某一个节点来替代待删除节点的位置,最后删除那个被选择用来替代删除节点的节点即可!

    选择哪一个节点来替代待删除的节点呢?选择的依据就是,替代之后的树应满足二叉搜索树嘛!因此就会有两种选择的方式

    • 选择待删除节点的左子树中最大的那个节点。

    • 选择待删除节点的右子树中最小的那个节点。

      在这里插入图片描述

    例如:如上图,我们要删除 3 这个节点,可以选择左子树中最大的节点 1 来代替 3,或者选择右子树中最小的节点 4 来代替 3。这两种选法均可使得删除后的二叉树满足二叉搜索树的性质。

    在这里插入图片描述

    替换之后呢?要删除的节点的特性要么满足情况 1,要么满足情况 2。就很好办啦!

    怎么找到二叉搜索树中的最大节点与最小节点呢?

    • 二叉搜索树的最大节点位于整棵树的最右侧。
    • 二叉搜索树的最小节点位于整棵树的最左侧。

其实经过仔细的观察,我们发现第一种情况和第二种情况是可以合并的!第一种情况是删除后父节点指针置空;第二种情况是删除后,父节点指向不为空的那个节点!

那么合并之后就是:如果待删除的节点的左孩子为空,那么令父节点指向右孩子就可以啦,否则,令父节点指向左孩子。或者如果待删除的节点的右孩子为空,那么令父节点指向左孩子,否者指向右孩子。

一定要看代码中的注释哦!还有一部分的细节在注释里面提到了。

bool erase(const K& key)
{
    Node* prev = nullptr;
    Node* cur = _root;
    //找到要被删除的那个节点
    while(cur)
    {
        if(key > cur->_key)
        {
            prev = cur;
            cur = cur->_right;
        }
        else if(key < cur->_key)
        {
            prev = cur;
            cur = cur->_left;
        }
        else //找到了那个要被删除的节点
        {   
            //左子树为空,链接右子树
            if(cur->_left == nullptr)
            {
                //这里是删除根节点的特殊情况
                if(cur == _root)
                    _root = cur->_right;
                else
                {
                    //确定删除的节点位于其父节点的哪个位置
                    if(prev->_right == cur)
                        prev->_right = cur->_right;
                    else
                        prev->_left = cur->_right;
                }

            }
            else if(cur->_right == nullptr) //右子树为空链接左子树
            {
                //处理删除根节点的特殊情况
                if(cur == _root)
                    _root = cur->_left;
                else
                {
                    //确定删除的节点位于其父节点的哪个位置
                    if(prev->_right == cur)
                        prev->_right = cur->_left;
                    else
                        prev->_left = cur->_left;
                }
            }
            else
            {
                //找到左子树中较大的那个节点,作为替换的节点
                Node* leftMax = cur->_left;
                Node* parent = cur;
                while(leftMax->_right)
                {
                    parent = leftMax;
                    leftMax = leftMax->_right;
                }
                //交换
                swap(leftMax->_key, cur->_key);
                //处理特殊情况,左子树的最大节点不一定是其父节点的右孩子
                //当删除的节点的左孩子就是左子树的最大值,这就是那个特殊情况
                if(parent->_left == leftMax)
                {
                    parent->_left = leftMax->_left;
                }
                else
                {
                    parent->_right = leftMax->_left;
                }
                cur = leftMax;
            }
            //删除节点
            delete cur;
            return true;
        }   
    }
    return false;
}

在这里插入图片描述

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

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

相关文章

css图片保持比例and图片占满整个div

一、非背景图 ①保持宽度固定 img { width: 200px; height: auto; } ②保持高度固定 img { height: 300px; width: auto; } ③保持比例 /* 比例不变 */ img { max-width: 100%; height: auto; } /* 垂直居中 */ img { max-width: 100%; height: auto; display: block; margin:…

都2023年了,不会还有人不会设计软件测试用例叭?不会吧不会吧

一、概念 测试用例的基本概念&#xff1a; 测试用例&#xff08;Test Case&#xff09;是为了实施测试而向被测试的系统提供的一组集合&#xff0c;这组集合包含&#xff1a;测试环境、操作步骤、测试数据、预期结果等要素 。 主要步骤&#xff1a; 测试环境——测试步骤—…

有关YOLOV5在测试时,图片大小被调整的问题

执行detect.py文件&#xff0c;在运行栏中出现以下&#xff1a; detect: weightsyolov5s.pt, sourcedata\images, datadata\coco128.yaml, imgsz[640, 640], conf_thres0.25, iou_thres0.45, max_det1000, device, view_imgFalse, save_txtFalse, save_confFalse, save_cropFa…

JDK 新特性深度分析,但我用Java 8

官方文档链接&#xff1a;https://openjdk.org/projects/jdk/21/ 下载链接&#xff1a;https://www.oracle.com/cn/java/technologies/downloads/#jdk21-windows 1、介绍 JDK21 是2023.09.19发布的正式版 其他版本的含义&#xff1a; Alpha&#xff1a;软件或系统的内部测试版…

SpringBoot源码透彻解析—自动装配

花点时间找到程序入口&#xff1a; 整个自动装配的流程总结如下&#xff1a; bean工厂后置处理器(ConfigurationClassPostProcessor) 扫描spring.factories和spring-autoconfigure-metadata.properties两个文件&#xff0c;将文件中的自动装配类信息抽象成Con…

Kubernetes-网络插件

目录 一、flannel网络插件 二、calico网络插件 1、部署 2、网络策略 &#xff08;1&#xff09;限制pod流量 &#xff08;2&#xff09;限制namespace流量 &#xff08;3&#xff09;同时限制namespace和pod &#xff08;4&#xff09;限制集群外部流量 一、flannel网络…

518抽奖软件,安全稳定,不怕手抖误按键

518抽奖软件简介 518抽奖软件&#xff0c;518我要发&#xff0c;超好用的年会抽奖软件&#xff0c;简约设计风格。 包含文字号码抽奖、照片抽奖两种模式&#xff0c;支持姓名抽奖、号码抽奖、数字抽奖、照片抽奖。(www.518cj.net) 防误按功能 入口&#xff1a; 主界面上点右…

node复制当前目录下的文件夹到另一层目录(包含多层文件夹嵌套)

前段时间在跟进node项目时有个node项目的需求&#xff0c;然后上线流程是把前端build后的文件夹放到后端仓库的静态资源目录下&#xff0c;再把后端代码发布上线。这样做的好处是在前端页面调用接口时&#xff0c;可以直接 /xxx来调用&#xff08;浏览器会自动把域名补全&#…

Spring系统之IOC与AOP

前言 Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。 IOC 1.IOC的概念 控制反转(IoCInversion of Control)IoC&#xff0c;用白话来讲&#xff0c;就是由容器控制程序之间的&#xff08;依赖&#xff09;关系&#xff0c;而非传统实现中&#xff0c;由程序代码…

Locust单机多核压测,以及主从节点的数据通信处理

一、背景 这还是2个月前做的一次接口性能测试&#xff0c;关于locust脚本的单机多核运行&#xff0c;以及主从节点之间的数据通信。 先简单交代下背景&#xff0c;在APP上线之前&#xff0c;需要对登录接口进行性能测试。经过评估&#xff0c;我还是优先选择了locust来进行脚…

2023我和云栖有个约会

时间&#xff1a;2023.11.1 地点&#xff1a;云栖小镇 事件&#xff1a;约会 昨天刚在网上看到了有阿姨在云栖大会给自己女儿相亲的照片&#xff0c;今天直接就赶了过去。约会了一整天&#xff0c;虽然很累&#xff0c;但真的很值得。由于是第一次和云栖约会&#xff0c;那就…

“深入理解Nginx的负载均衡与动静分离“

目录 引言一、Nginx简介1. Nginx的基本概念2. Nginx的特点3. Nginx的安装配置 二、Nginx搭载负载均衡三、前端项目打包四、Nginx部署前后端分离项目&#xff0c;同时实现负载均衡和动静分离总结 引言 在现代互联网应用中&#xff0c;高性能和可扩展性是至关重要的。Nginx作为一…

初出茅庐的小李博客之STCW15408AS单片机串口1使用记录

STCW15408AS单片机串口1使用记录 资源介绍&#xff1a; STC15W401AS系列单片机是STC生产的单时钟/机器周期(1T)的单片机&#xff0c;是宽电压/高可靠/低功耗/超强抗干扰的新一代8051单片机&#xff0c;采用STC第九代加密技术&#xff0c;无法解密&#xff0c; 代码完全兼容传…

node使用fs模块(四)—— 文件夹的使用(创建、读取、删除)

文章目录 前言一、文件的创建1.参数2.基本使用 二、文件的读取1.参数2.读取文件的基本使用3.读取文件的递归使用&#xff08;option中添加recursive为true&#xff09; 三、文件的删除1. 参数2. 基本使用&#xff08;删除文件夹&#xff09;3. 递归删除文件夹 前言 创建、读取…

学习时遇到的错误

1. pycharm中使用ssh远程连接的jupyter时&#xff0c;出现***端口已经被占用的情况 办法一&#xff1a;更换端口&#xff0c;将端口更换为其他 办法二&#xff1a;重启远程终端服务器 2. 关于wandb&#xff0c;在pycharm中调用了wandb.init()初始化函数&#xff0c;中途关闭…

MolFormer分子预训练模型

Large-scale chemical language representations capture molecular structure and properties&#xff08;2022&#xff0c;NMI&#xff09; 和原本transformer encoder的不同&#xff1a; 采用linear attention mechanismrotary positional embedding 模型 transformer e…

x3daudio17.dll丢失是什么原因?x3daudio1_7.dll怎么解决?(修复教程分享)

在我多年的电脑使用经历中&#xff0c;我曾经遇到过x3daudio1_7.dll缺失的问题。这个问题让我苦恼了很久&#xff0c;但最终我还是找到了4种有效的修复方法。今天&#xff0c;我想和大家分享一下这些方法&#xff0c;希望对你们有所帮助。 首先&#xff0c;我要讲述一下我遇到…

跟着Nature Communications学作图:纹理柱状图+添加显著性标签!

&#x1f4cb;文章目录 复现图片设置工作路径和加载相关R包读取数据集数据可视化计算均值和标准差 计算均值和标准差方差分析组间t-test 图a可视化过程图b可视化过程合并图ab 跟着「Nature Communications」学作图&#xff0c;今天主要通过复刻NC文章中的一张主图来巩固先前分享…

基础课15——语音标注

语音数据标注是对语音数据进行处理和分析的过程&#xff0c;目的是让人工智能系统能够理解和识别语音中的信息。这个过程包括了对语音信号的预处理、特征提取、标注等步骤。 在语音数据标注中&#xff0c;标注员需要对语音数据进行分类、切分、转写等操作&#xff0c;让人工智…

C++ 多线程之OpenMP并行编程使用详解

C 多线程之OpenMP并行编程使用详解 总结OpenMP使用详解本文转载自&#xff1a;https://blog.csdn.net/AAAA202012/article/details/123665617?spm1001.2014.3001.5506 1.总览 OpenMP(Open Multi-Processing)是一种用于共享内存并行系统的多线程程序设计方案&#xff0c;支持…