C++数据结构:二叉树之二(二叉搜索树)

news2024/12/24 11:25:42

文章目录

  • 前言
  • 一、二叉搜索树的概念
  • 二、代码详解
    • 1、构建节点
    • 2、构建二叉树类
    • 3、插入方法
    • 4、删除方法
    • 5、四种遍历方法
    • 6、测试代码
  • 总结


前言

前文已经讲了二叉树概念,并搞出一个数组存储的没写具体实用意义的二叉树,这篇文章将讲解二叉树的另一种存储方式:链式存储,它和链表的存储方式有点像,这也是前面用了很多篇幅写链表的原因,实现了链表再来看本文会容易很多。因为光写链式存储水一篇文章也太没意义了,所以本文将实现一个链式存储基本有序的二叉搜索树,并实现四种遍历方式,它在排序搜索方面有一定的实用意义。


一、二叉搜索树的概念

我们先给出测试数据 int arr[10] = {5, 3, 6, 8, 9, 2, 4, 7, 10, 1};,还是以一张图来看,比较直观好懂,这张图是最后实现后的效果:
在这里插入图片描述
二叉搜索树(BST)是一种基于节点的二叉树数据结构,它具有以下性质:节点的左子树只包含键值小于节点键值的节点;节点的右子树只包含键值大于节点键值的节点;每个节点的左右子树也必须是二叉搜索树。

二叉搜索树支持多种操作,包括搜索、插入和删除。搜索操作通过比较要搜索的值和当前节点的键值来进行。如果要搜索的值小于当前节点的键值,则在左子树中继续搜索;如果要搜索的值大于当前节点的键值,则在右子树中继续搜索。插入操作类似于搜索操作,不同之处在于当找到一个空位置时,新节点将被插入到这个位置。删除操作相对复杂一些,需要考虑被删除节点的子节点数量和位置。下面给出实现代码:

二、代码详解

代码比较长,分段来说吧:

1、构建节点

#include <iostream>
#include <queue>

using std::cout;
using std::endl;
using std::queue;

template <typename T> class Btree;    //仅声明,为了下面节点定义友元

template <typename T> class Node{
    private:
        T val;
        Node<T>* left;
        Node<T>* right;

    public:
        friend class Btree<T>;
        Node(){
            left = nullptr;
            right = nullptr;
        }

        Node(T data){
            val = data;
            left = nullptr;
            right = nullptr;
        }
};

以上是节点类,这里直接在节点中写了赋值,比较方便。顺便声明了二叉树类,方便在节点类中写友元。

2、构建二叉树类

template<typename T> class Btree{
    private:
        Node<T>* root;

    public:
        Btree(){
            root = nullptr;
        }

        Btree(T data){
            root = new Node<T>(data);
        }

        ~Btree(){
            deleteTree(root);  
        }

        void deleteTree(Node<T>* node) {
            if (node != nullptr) {
                deleteTree(node->left);
                deleteTree(node->right);
                delete node;
            }
        }

以上是二叉树类的构造、析构函数。因为在节点中写了赋值,这里构造很简单,析构方法调用了一个函数,直接在析构里写循环也是一样的。

3、插入方法


        bool insert(T v){
            if (!root){
                root = new Node<T>(v);
                return true;
            }
            Node<T>* curr = root;
            while (curr){
                if (v < curr->val){
                    if (!curr->left){
                        curr->left = new Node<T>(v);
                        return true;
                    }else{
                        curr = curr->left;
                    }   
                }else{
                    if (!curr->right){
                        curr->right = new Node<T>(v);
                        return true;
                    }else{
                        curr = curr->right;
                    }
                }
            }
            return false;
        }

以上是插入函数,这个方法逻辑也不复杂,先判断有无根节点,没有就先建立根节点,后面插入的值就是比大小了,小于根节点的放左边,大于根节点的放右边。最终按示例给出的数据,会形成如图所示的结构。仔细想想,并不难理解。

4、删除方法

        void remove(T v){
            root = remove(root, v);
        }

        Node<T>* remove(Node<T>* node, T v){
            if (node == nullptr) return node;
            if (v < node->val) {
                node->left = remove(node->left, v);
            } else if (v > node->val){
                node->right = remove(node->right, v);
            } else{
                if (node->left == nullptr){
                    Node<T>* tmp = node->right;
                    delete node;
                    return tmp;
                } else if (node->right == nullptr){
                    Node<T>* tmp = node->left;
                    delete node;
                    return tmp;
                }
                Node<T>* tmp = min(node->right);
                node->val = tmp->val;
                node->right = remove(node->right, tmp->val);
            }
            return node;
        }

        Node<T>* min(Node<T>* node){
            Node<T>* current = node;
            while (current && current->left != nullptr)
                current = current->left;
            return current;
        }

以上是删除方法,这个写法有点绕~ remove这个方法有一个重载,这是因为我们要删除某个节点,却只知道某节点的值,并不知道具体是哪个节点,那就只能从根节点开始找。删除节点有三种情况:

  • 如果要删除的节点是叶子节点(没有子节点),则直接删除该节点,这个最简单了。
  • 如果要删除的节点只有一个子节点,则用该子节点替换要删除的节点。
  • 如果要删除的节点有两个子节点,则用其右子树中的最小值(或左子树中的最大值)替换要删除的节点,然后在右子树(或左子树)中递归删除最小值(或最大值)。这里是以右子树最小值来实现的,这二种方法基本上是一样的逻辑实现,因为这个二叉树是有序的,找起来也很方便。右子树最小值就是该节点的右边的最左边,左子树中的最大值就是该节点的左边的最右边那个节点。

root = remove(root, v); 这行代码调用了它的重载 remove 函数,该函数接受两个参数:一个 Node* 类型的指针和一个 T 类型的值。在这行代码中,第一个参数是 root,表示从树的根节点开始找;第二个参数是 v,它是要删除的节点的值。

remove 函数会返回一个 Node* 类型的指针,表示删除节点后新的根节点。在这行代码中,我们将返回的新根节点赋值给了 root 变量(如果删除的是主根节点),以更新树的根节点。

如果删除的不是主根节点,比如想要删除值为6的节点,那么 remove 函数将首先比较6和根节点的值。如果6大于根节点的值,则函数将在右子树中递归调用自身,以在右子树中查找并删除值为6的节点。

当找到值为6的节点时,函数将检查该节点是否有左子节点。如果没有左子节点,则函数将返回该节点的右子节点,并删除该节点。在这种情况下,返回的 tmp 将是值为6的节点的右子节点。

这样写的目的是为了能够递归地删除节点。在 remove 函数中,我们首先检查要删除的值是否小于当前节点的值,如果是,则在左子树中递归删除;否则,如果要删除的值大于当前节点的值,则在右子树中递归删除。当找到要删除的节点时,我们按前面提到的三种情况之一来删除该节点,并返回新的根节点。

根节点5的右子节点指针在 remove(Node<T>* node, T v) 函数递归返回时被更新。

remove(Node<T>* node, T v) 函数递归调用自身以在右子树中查找并删除值为6的节点时,它将返回一个新的子树的根节点。在这种情况下,返回的新根节点是值为6的节点的右子节点,即值为8的节点。

当递归返回到根节点时,我们将更新根节点的右子节点指针,使其指向新的子树的根节点。在这种情况下,我们将根节点的右子节点指针更新为指向值为8的节点。

if (v < node->val){
    node->left = remove(node->left, v);
} else if (v > node->val){
    node->right = remove(node->right, v);
}

就是这段代码,我们递归地调用 remove 函数,并将返回的新子树的根节点赋值给当前节点的左子节点或右子节点指针。

5、四种遍历方法


        void preOrder(Node<T>* p = nullptr){
            Node<T>* curr;
            if (!p) curr = root;
            else curr = p;
            if (curr){
                cout << curr->val << " ";
                if (curr->left) preOrder(curr->left);
                if (curr->right) preOrder(curr->right);
            }
        }

        void inOrder(Node<T>* p = nullptr){
            Node<T>* curr;
            if (!p) curr = root;
            else curr = p;
            if (curr){
                if (curr->left) inOrder(curr->left);
                cout << curr->val << " ";
                if (curr->right) inOrder(curr->right);
            }
        }

        void postOrder(Node<T>* p = nullptr){
            Node<T>* curr;
            if (!p) curr = root;
            else curr = p;
            if (curr){
                if (curr->left) postOrder(curr->left);
                if (curr->right) postOrder(curr->right);
                cout << curr->val << " ";
            }
        }

        void layOrder(){
            Node<T>* curr = root;
            queue<Node<T>*> q;
            int count = 0;
            q.push(curr);
            count++;
            while (q.front()){
                curr = q.front();
                cout << curr->val << " ";
                q.pop();
                if (curr->left){
                    q.push(curr->left);
                    count++;
                }
                if (curr->right){
                    q.push(curr->right);
                    count++;
                }
            }
            //cout << endl << count << endl;       //统计节点数
        }
};

上面的代码是四种遍历方式,也是比较好理解的。值得一提的是中序遍历 inOrder 它的输出结果是:

1 2 3 4 5 6 7 8 9 10 

也即是说,这个二叉树有排序功能。它的排序效率是O(n)。这个在数据插入的时候就已经排好了。当然这个二叉树主要不是用来排序的,这只是顺便的,它主要用于搜索。因为分了大小,只要根节点选得好,它的效率还是比较高的。但是这个数据结构的效率不稳定,在最差情况下这个结构会变成单向链表。这里笔者是故意把5放在最前面做根节点,演示了这个结构效率比较高的情况。

6、测试代码

int main(){
    int arr[10] = {5, 3, 6, 8, 9, 2, 4, 7, 10, 1};
    Btree<int> t;
    for (int i=0; i<10; ++i) t.insert(arr[i]);
    t.remove(6);
    t.preOrder();
    cout << endl;
    t.inOrder();
    cout << endl;
    t.postOrder();
    cout << endl;
    t.layOrder();

    return 0;
}

按以上代码测试后的结果:

5 3 2 1 4 6 8 7 9 10 
1 2 3 4 5 6 7 8 9 10 
1 2 4 3 7 10 9 8 6 5 
5 3 6 2 4 8 1 7 9 10 
//6被删除后:
5 3 2 1 4 8 7 9 10 
1 2 3 4 5 7 8 9 10 
1 2 4 3 7 10 9 8 5 
5 3 8 2 4 7 9 1 10 

总结

好了,这篇文章太长了~ 好想拆成两篇啊。好像忘记写查找方法了哈…不过那个逻辑很简单,随便想想也就搞定了,有需要的读者自己搞定吧,预告一下:下一篇会写有序二叉树的扩展功能,比如搜索父节点、子节点、前驱后续节点等。

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

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

相关文章

限量内测名额释放:微信云开发管理工具新功能

我们一直收到大家关于云数据库管理、快速搭建内部工具等诉求&#xff0c;为了给大家提供更好的开发体验&#xff0c;结合大家的诉求&#xff0c;云开发团队现推出新功能「管理工具」&#xff0c;现已启动内测&#xff0c;诚邀各位开发者参与内测体验。 什么是「管理工具」 管…

当节点内存管理遇上 Kubernetes:自动调度与控制

原理 在现代的容器化环境中&#xff0c;节点资源的管理是一个重要的任务。特别是对于内存资源的管理&#xff0c;它直接影响着容器应用的性能和可用性。在 Kubernetes 中&#xff0c;我们可以利用自动调度和控制的机制来实现对节点内存的有效管理。本文将介绍一种基于 Bash 脚…

EM中等效原理

EM中等效原理 一、基本简介 电磁等效定理对于简化许多问题的解是有用的。此外&#xff0c;它们还提供了对麦克斯韦系统电磁场行为的物理见解。它们与唯一性定理和惠更斯原理密切相关。一个应用是它们在研究来自孔径天线或来自激光腔输出的辐射中的用途。 等效源原理&#xf…

3.2 掌握RDD算子

一、准备工作 &#xff08;一&#xff09;准备文件 1、准备本地系统文件 2、把文件上传到HDFS &#xff08;二&#xff09;启动Spark Shell 1、启动HDFS服务 2、启动Spark服务 3、启动Spark Shell 二、掌握转换算子 &#xff08;一&#xff09;映射算子 - map() …

Sketch在线版免费使用,Windows也能用的Sketch!

Sketch 的最大缺点是它对 Windows/PC 用户不友好。它是一款 Mac 工具&#xff0c;无法在浏览器中运行。此外&#xff0c;使用 Sketch 需要安装其他插件才能获得更多响应式设计工具。然而&#xff0c;现在有了 Sketch 网页版工具即时设计替代即时设计&#xff01; 即时设计几乎…

通达信凹口平量柱选股公式,倍量柱之后调整再上升

凹口平量柱是一组量柱形态&#xff0c;表现为量柱两边高、中间扁平或圆底的形态。如下图所示&#xff0c;左右各有一根高度持平的高量柱&#xff0c;中间夹杂着三五根甚至更多根低量柱。 凹口平量柱选股公式需要结合量柱以及K线&#xff0c;主要考虑以下三点&#xff1a; 1、倍…

git各阶段代码修改回退撤销操作

git push origin master 的含义是将本地当前分支的提交推送到名为 origin 的远程仓库的 master 分支上。 各阶段代码修改回退撤销的操作 case1 git checkout -- . 修改了文件内容但没还有git add 或git commit时撤销当前目录下所有文件的修改 case2 当完成了git add 之后&a…

项目管理:面对未知的挑战时,如何获取和使用信息?

一项实验展示了人们在面对未知的挑战时&#xff0c;对信息的获取和使用的影响。在下面的实验中&#xff0c;三组人被要求步行到十公里外的三个村庄。 第一组人没有任何信息&#xff0c;只跟着向导走。他们在走了短短的两三公里后就开始抱怨和情绪低落&#xff0c;同时感到疲惫…

2022年天府杯全国大学生数学建模竞赛E题地铁线路的运营与规划解题全过程文档及程序

2022年天府杯全国大学生数学建模竞赛 E题 地铁线路的运营与规划 原题再现&#xff1a; 地铁是一种非常绿色快捷的交通出行方式&#xff0c;全国各大城市也都在如火如荼地进行地铁线路建设与规划。但乘坐地铁有时候会感觉特别拥挤&#xff0c;这一时期我们称为高峰期。如何合理…

sqlserver中的merge into语句

merge into语句是用来合并两张表的数据的&#xff0c;比如我们想把一张表的数据批量更新到另外一张表&#xff0c;就可以用merge into语句。具体有哪些业务场景呢&#xff1f; 1.数据同步 2.数据转换 3.基于源表对目标表进行增&#xff0c;删&#xff0c;改的操作。 实践步骤…

JavaScript了解调用带参函数,无参函数的代码

以下为JavaScript了解调用带参函数&#xff0c;无参函数的程序代码和运行截图 目录 前言 一、带参函数 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 二、无参函数 2.1 运行流程及思想 2.2 代码段 2.3 JavaScript语句代码 2.4 运行截图 前言…

让代码创造童话,共建快乐世界:六一儿童节特辑

让代码创造童话&#xff0c;共建快乐世界&#xff1a;六一儿童节特辑 六一儿童节即将来临&#xff0c;这是一个属于孩子们的快乐节日。为了让这个节日更加有趣&#xff0c;我们发起了“让代码创造童话&#xff0c;共建快乐世界”六一活动。在这个活动中&#xff0c;我们邀请您…

使用Tensorrt对YOLOv5目标检测的代码进行加速

文章目录 1. 前言2. 官网3. 安装依赖3.1. 安装OpenCV3.1.1. 安装3.1.2. 添加环境变量3.1.3. 查看版本 3.2. 安装TensorRT3.2.1. 下载3.2.2. 安装3.2.3. 添加环境变量 4. 下载项目5. 生成WTS模型6. cmake6.1. 生成Makefile6.1.1. 配置CMakeLists.txt6.1.1.1. 修改编译依赖的路径…

通过python采集1688商品评论数据封装接口、1688评论数据接口

1688商品评论数据是指在1688网站上对商品的评价和评论信息。这些信息包括买家对商品的使用、品质、包装、服务等方面的评价和意见&#xff0c;可以帮助其他用户更好地了解商品的优缺点和性能&#xff0c;从而做出更明智的购买决策。 1688网站是中国最大的B2B电子商务网站之一&…

RK3566调试EC20

参考博客&#xff1a;RK3568开发笔记-buildroot移远EC20模块调试记录 一、内核配置 cd 到kernel目录&#xff0c;执行make ARCHarm64 menuconfig&#xff0c; Device Drivers >USB support > USB Serial Converter support 选中 USB driver for GSM and CDMA modems选…

04.hadoop上课笔记之java编程和hbase

1.win查看服务 netstat -an #linux也有#R数学建模语言 SCALAR 2.java连接注意事项,代码要设置用户 System.setProperty("HADOOP_USER_NAME", "hadoop");3.伪分布式的好处(不用管分布式细节,直接连接一台机器…,适合用于学习) 4.官方文档 查看类(static |…

5个UI设计师必备的Figma汉化插件

即时设计插件广场提供了许多有用的 UI 插件&#xff0c;帮助优化产品设计过程。其中&#xff0c;产品组件库 Figma 汉化插件对常用的 PC 端和移动端组件进行了筛选&#xff0c;使其更加聚焦和精简。PC 端组件包括基础、按钮、菜单和其他元素&#xff0c;移动端组件包括基础、按…

电子阅读器calibre的使用技巧

十条calibre使用技巧&#xff1a; 1. 添加电子书&#xff1a;可以单独添加文件、添加文件夹、添加zipped书籍&#xff0c;或者通过网络链接直接添加。 2. 转换电子书格式&#xff1a;可以将电子书转换为不同的格式&#xff0c;如AZW3、EPUB、MOBI、PDF等。 3. 修改元数据&am…

文件夹加密超级大师的金钻加密和闪电加密有什么区别?

作为一款专业的文件夹加密软件&#xff0c;文件夹加密超级大师提供了5种文件加密类型&#xff0c;其中金钻加密和闪电加密在加密后效果看似差不多&#xff0c;那么它们有什么区别呢&#xff1f;下面我们就来了解一下吧。 闪电加密更快速 当我们想要加密那些超级庞大的文件夹时…

Docker安装kafka可视化管理工具 - Kafka Manager

说明&#xff1a;此处是在前面使用Docker安装kafka的基础之上&#xff0c;再来使用Docker安装kafka-manager 第一步&#xff1a;使用下述命令从Docker Hub查找镜像&#xff0c;此处我们要选择的是sheepkiller所构建的kafka-manager镜像 docker search kafka-manager 第二步&a…