【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现

news2024/11/28 9:34:43

高阶数据结构相关知识点可以通过点击以下链接进行学习一起加油!

本章是高阶数据结构笔记的第一篇文章,将分享二叉搜索树的进阶概念及其高效实现的相关知识,欢迎大家阅读!

请添加图片描述
Alt
🌈个人主页:是店小二呀
🌈C语言专栏:C语言
🌈C++专栏: C++
🌈初阶数据结构专栏: 初阶数据结构
🌈高阶数据结构专栏: 高阶数据结构
🌈Linux专栏: Linux

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅 请添加图片描述

文章目录

  • 一、二叉搜索树概念
  • 二、二叉搜索树的创建
    • 2.1 二叉搜索树的基本单位
    • 2.2 实现二叉搜索树的基本框架
    • 2.3 二叉搜索树的查找
    • 2.4 二叉搜索树的插入
    • 2.5 二叉搜索树的删除(难点)
      • 2.5.1 删除该子树根节点情况分析
      • 2.5.2 删除第一、二情况节点
      • 2.5.3 第三种情况(替换法)
    • 2.6 采用中序遍历二叉搜索树
  • 三、改造二叉搜索树,进行实际应用
  • 四、二叉搜索树的性能分析
  • 六、Binary_Search_Tree.h

一、二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

  • 它的左右子树也分别为二叉搜索树

  • 现阶段二叉搜索树没有重复的数据

在这里插入图片描述

二、二叉搜索树的创建

2.1 二叉搜索树的基本单位

template<class K>
    struct  BSTreeNode
    {
        BSTreeNode(const K& key = K())
            :_left(nullptr)
                , _right(nullptr)
                , _key(key)
            {}

        BSTreeNode<K>* _left;
        BSTreeNode<K>* _right;
        K _key;

    };

2.2 实现二叉搜索树的基本框架

template<class K>
    class  BSTree
    {
        public:
        //类型名字太长,不方便
        typedef BSTreeNode<K> Node;

        private:
        Node* _root = nullptr;
    };

在这里插入图片描述

上面图示以物理结构数组int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13}创建出来的逻辑结构二叉搜索树的数据结构。

2.3 二叉搜索树的查找

二叉搜索树查找步骤:

  • 规定一个关键值key
  • 从根开始开始比较查找,key比根大则往右边走查找,key比根小则往左边走查找
  • 最多查找高度次,走到到空,还没有找到,这值不存在
  • 在插入接口中,虽然查找合适位置代码逻辑差不多,但是存在个别逻辑差异,注意识别
bool Find(const K& key)
{
    Node* cur = _root->_key;
    while (cur)
    {
        if (key < cur->_key)
        {
            cur = cur->_left;
        }
        else if(key > cur->_key)
        {
            cur = cur->_right;
        }
        else
        {
            return true;
        }
    }
    return  false;
}

2.4 二叉搜索树的插入

插入具体过程:

  • 树为空,则直接新增节点,赋值给root指针
  • 树不为空,按二叉搜索树性质插入位置,插入新节点

在这里插入图片描述

bool Insert(const K& key)
{
    if (_root == nullptr)
    {
        _root = new Node(key);
        return true;
    }

    Node* parent = nullptr;
    //这里cur是临时变量
    Node* cur = _root;

    while (cur)
    {
        if (cur->_key < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_key > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
           return false;
        }
    }

    cur = new Node(key);

    if (parent->_key < key)
    {
        parent->_right = cur;
    }
    else if (parent->_key > key)
    {
        parent->_left = cur;
    }
    return true;
}

插入具体过程细节处理:

  • 需要判断树是否为空树,如果为空,创建节点赋值给_root
  • 创建两个指针parent和cur保证节点的连接
  • 通过不同比较大小,直到cur找到为空的位置,创建节点
  • 该节点需要满足二叉搜索树的特性,需要再次判断,选择连接

2.5 二叉搜索树的删除(难点)

2.5.1 删除该子树根节点情况分析

首先查找元素是否在二叉搜索树中,如果不存在则返回false,否则要删除的节点可能分为下面三种情况。先到需要被删除的节点,这里就不重复实现了。

删除节点情况划分:

  1. 要删除的节点无孩子节点
  2. 要删除的节点只有一个孩子节点
  3. 要删除的节点有左、右孩子节点

2.5.2 删除第一、二情况节点

这里第一种和第一种情况可以归类为同一种情况。无论被删除节点是否有无真实存在的孩子节点,都可以看成要删除的节点只有一个孩子节点,将第一种情况看成第二种情况,被删除节点有空孩子节点。

在这里插入图片描述

if (cur->_left == nullptr)
{
    if (parent->_left == cur)
    {
        parent->_left = cur->_right;
    }
    else if (parent->_right == cur)
    {
        parent->_right = cur->_right;
    }
    delete cur;
}
else if (cur->_right == nullptr)
{
    if (parent->_left == cur)
    {
        parent->_right = cur->_left;
    }
    else if (parent->_right == cur)
    {
        parent->_left = cur->_left;
    }
    delete cur;
}

在这里插入图片描述

关于数据结构学习,我们需要借助具体的逻辑结构去实现"抽象"的物理结构。接下我也希望你们可以借助图和文字进行对代码的解读。

  1. 第一个判断分支决定,parent指向另外一个可能为空的节点。

在这里插入图片描述

  1. 第二个分支判断被删除节点相对parent节点的位置

判断结束后,parent节点进行连接操作进行删除操作。

  1. 小总结:判断被删除节点位置与被删除节点可能不为空孩子位置,进行连接即可。

草稿说明,上面是优化版本说明:

有了上面两个信息的话,比如通过parent->_left == cur需要被删除的节点是左节点,并且cur->_left == nullptr该节点左孩子节点为空,那么parent->_left = cur->_right;parent->_left 是根据第一个条件,该parent->_left需要重新连接新节点,那么新节点是谁?通过cur->_left == nullptr判断,该左孩子为空,肯定连接右孩子节点。

2.5.3 第三种情况(替换法)

使用替换法删除,简单回顾

  • 左子树上所有节点的值都小于根节点的值
  • 右子树上所有节点的值都大于根节点的值

在这里插入图片描述

被替换的节点需要满足左子树最大节点或者右字数的最小节点其中之一即可。比如满足左子树最大节点,进行交换,该节点满足比左子树都要大,比右子树都要小。

替换法删除的具体流程:

  • 先找到需要被删除节点和被替换节点,进行swap交换数字
  • 通过第一、二种情况进行删除操作
  • 那么需要设置两个指针去需要被替换节点
Node* RightMinParent = cur;
Node* RightMin = cur->_right;

//找到右子树最小的值
while (RightMin->_left)
{
    RightMinParent = RightMin;
    RightMin = RightMin->_left;
}
//找到
swap(cur->_key, RightMin->_key);

if (RightMinParent->_left == RightMin)
{
    RightMinParent->_left = RightMin->_right;
}
else
{
    RightMinParent->_right = RightMin->_right;
}
delete RightMin;
}
return true;

实现该逻辑的具体细节:

  • 这里我选择找到右子树的最小节点,那么只需要关注左边的情况就行了,这也是为什么是while (RightMin->_left)
  • 首先就是第一、二种情况删除的做法
  • RightMinParent不能设置为空指针当删除根节点就会有问题,直接设置为cur

以上三种情况都需要考虑需要被删除节点为根节点

在这里插入图片描述

2.6 采用中序遍历二叉搜索树

void InOrder()
{
    _InOrder(_root);
    cout << endl;
}

private:
void _InOrder(Node* root)
{
    if (root == nullptr)
    {
        return;
    }

    _InOrder(root->_left);
    cout << root->_key << " ";
    _InOrder(root->_right);
}

中序遍历能够按顺序输出树中所有节点的值。从根节点开始,中序遍历的顺序是【左子树 , 根节点 , 右子树】这一过程在BST中恰好能保证节点按从小到大的顺序排列。将内部实现放在private部分,可以避免外部代码错误地调用内部方法,导致程序行为不可预测或出错。通过控制访问权限。外部代码不应该直接操作树的节点,而应该通过公开的接口方法来访问和操作树。

三、改造二叉搜索树,进行实际应用

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

使用场景:判断单词是否拼写正确

  • 将词库中所有单词集合中的每个单词作为key,构建一颗二叉搜索树
  • 在二叉搜索树中查找单词是否存在,存在则为正确,否则错误

KV模型:每一个关键码key,都有与之对应的值Value,即<K,Value>的键值对

使用场景:翻译语言(底层主要还是B树)

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对
// 改造二叉搜索树为KV结构
template<class K, class V>
    struct BSTNode
    {
        BSTNode(const K& key = K(), const V& value = V())
            : _pLeft(nullptr) , _pRight(nullptr), _key(key), _Value(value)
            {}
        BSTNode<T>* _pLeft;
        BSTNode<T>* _pRight;
        K _key;
        V _value
    };
template<class K, class V>
    class BSTree
    {
        typedef BSTNode<K, V> Node;
        typedef Node* PNode;
        public:
        BSTree(): _pRoot(nullptr){}
        PNode Find(const K& key);
        bool Insert(const K& key, const V& value)
            bool Erase(const K& key)
            private:
        PNode _pRoot;
    };
void TestBSTree3()
{
    // 输入单词,查找单词对应的中文翻译
    BSTree<string, string> dict;
    dict.Insert("string", "字符串");
    dict.Insert("tree", "树");
    dict.Insert("left", "左边、剩余");
    dict.Insert("right", "右边");
    dict.Insert("sort", "排序");
    // 插入词库中所有单词
    string str;
    while (cin>>str)
    {
        BSTreeNode<string, string>* ret = dict.Find(str);
        if (ret == nullptr)
        {
            cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;
        }
        else
        {
            cout << str << "中文翻译:" << ret->_value << endl;
        }
    }
}
void TestBSTree4()
{
    // 统计水果出现的次数
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
                    "苹果", "香蕉", "苹果", "香蕉" };
    BSTree<string, int> countTree;
    for (const auto& str : arr)
    {
        // 先查找水果在不在搜索树中
        // 1、不在,说明水果第一次出现,则插入<水果, 1>
        // 2、在,则查找到的节点中水果对应的次数++
        //BSTreeNode<string, int>* ret = countTree.Find(str);
        auto ret = countTree.Find(str);
        if (ret == NULL)
        {
            countTree.Insert(str, 1);
        }
        else
        {
            ret->_value++;
        }
    }
    countTree.InOrder();
}

四、二叉搜索树的性能分析

插入和删除操作都必须先查找的,查找效率代表了二叉搜索树中各个操作的性能

对于对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜

在这里插入图片描述

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树)
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支)

如果退化成单支树,二叉搜索树的性能就失去了 ,我们后续章节学习的AVL树和红黑树就可以上场了

六、Binary_Search_Tree.h

#pragma once
#include <string>

template<class K>
    struct  BSTreeNode
    {
        BSTreeNode(const K& key = K())
            :_left(nullptr)
                , _right(nullptr)
                , _key(key)
            {}

        BSTreeNode<K>* _left;
        BSTreeNode<K>* _right;
        K _key;
    };

template<class K>
    class  BSTree
    {
        public:

        typedef BSTreeNode<K> Node;

        //插入操作
        bool Insert(const K& key)
        {
            if (_root == nullptr)
            {
                _root = new Node(key);
                return true;
            }

            Node* parent = nullptr;
            Node* cur = _root;

            while (cur)
            {
                if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else
                {
                    return false;
                }
            }

            cur = new Node(key);

            if (parent->_key < key)
            {
                parent->_right = cur;
            }
            else if (parent->_key > key)
            {
                parent->_left = cur;
            }
            return true;
        }

        bool Erase(const K& key)
        {
            Node* parent = nullptr;
            Node* cur = _root;

            while (cur)
            {
                if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else
                {
                    //找到位置
                    //删除
                    //先判断谁为空
                    if (cur->_left == nullptr)
                    {
                        if (cur == _root)
                        {
                            _root = cur->_right;
                        }
                        else
                        {
                            if (parent->_left == cur)
                            {
                                parent->_left = cur->_right;
                            }
                            else if (parent->_right == cur)
                            {
                                parent->_right = cur->_right;
                            }
                        }
                        delete cur;
                    }
                    else if (cur->_right == nullptr)
                    {
                        if (cur == _root)
                        {
                            _root = cur->_right;
                        }
                        else
                        {
                            if (parent->_left == cur)
                            {
                                parent->_right = cur->_left;
                            }
                            else if (parent->_right == cur)
                            {
                                parent->_left = cur->_left;
                            }
                        }
                        delete cur;
                    }
                    //替换法实现
                    else
                    {
                        Node* RightMinParent = cur;
                        Node* RightMin = cur->_right;

                        //找到右子树最大的值
                        while (RightMin->_left)
                        {
                            RightMinParent = RightMin;
                            RightMin = RightMin->_left;
                        }
                        //找到
                        swap(cur->_key, RightMin->_key);

                        if (RightMinParent->_left == RightMin)
                        {
                            RightMinParent->_left = RightMin->_right;
                        }
                        else
                        {
                            RightMinParent->_right = RightMin->_right;
                        }
                        delete RightMin;
                    }
                    return true;
                }
            }
            return false;
        }

        bool Find(const K& key)
        {
            Node* cur = _root;
            while (cur)
            {
                if (cur->_key < key)
                {
                    cur = cur->_right;
                }
                else if (cur->_key > key)
                {
                    cur = cur->_left;
                }
                else
                {
                    return false;
                }
            }
            return true;
        }

        void InOrder()
        {
            _InOrder(_root);
            cout << endl;
        }

        private:
        void _InOrder(Node* root)
        {
            if (root == nullptr)
            {
                return;
            }

            _InOrder(root->_left);
            cout << root->_key << " ";
            _InOrder(root->_right);
        }
        private:
        Node* _root = nullptr;
    };

感谢大家的观看!以上就是本篇文章的全部内容。我是店小二,希望这些高阶数据结构笔记能为你在学习旅途中提供帮助!
请添加图片描述

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

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

相关文章

五子棋双人对战项目(5)——对战模块

目录 一、需求分析 二、约定前后端交互接口 三、实现游戏房间页面&#xff08;前端代码&#xff09; game_room.html game_room.css srcipt.js 四、实现后端代码 GameAPI Room Mapper 五、线程安全问题 一、需求分析 在对局中&#xff0c;玩家需要知道实时对局情况&…

高阶数据结构-------图

文章目录 图图的基本概念图的存储结构邻接矩阵邻接表 图的遍历广度优先遍历深度优先遍历 最小生成树Kruskal算法Prim算法 最短路径单源最短路径-Dijkstra算法单源最短路径-Bellman-Ford算法多源最短路径-Floyd-Warshall算法 图 图的基本概念 图的基本概念 图是由顶点集合和边的…

【10】纯血鸿蒙HarmonyOS NEXT星河版开发0基础学习笔记-泛型基础全解(泛型函数、泛型接口、泛型类)及参数、接口补充

序言&#xff1a; 本文详细讲解了关于ArkTs语言中的泛型&#xff0c;其中包含泛型函数、泛型接口、泛型约束、泛型类及其中参数的使用方法&#xff0c;补充了一部分接口相关的知识&#xff0c;包括接口的继承和具体实现&#xff0c;也写到了一些边边角角的小知识&#xff0c;剩…

【Linux】进程替换、命令行参数及环境变量(超详解)

目录 进程替换 替换函数的含义 命令行参数 环境变量 PATH 进程替换 我们先看代码&#xff1a; 1 #include<stdio.h>2 #include<unistd.h>3 int main()4 {5 printf("process...begin!\n");6 7 execl("/usr/bin/ls","ls"…

前端面试如何说解vue项目性能优化,你确定不来看看吗?

文末有福利 面试时&#xff0c;很经常会说对某某项目进行了性能优化&#xff0c;使性能有很大的提高之类的话。如果面试官问&#xff0c;来讲讲做了那些优化&#xff0c;这时候你就要很清晰地把你做过的优化一一说出来。 本文谨以自己的Vue项目经验来教你怎么在面试中说优化&am…

【算法与图】通向高效解决方案的钥匙

文章目录 遍历算法BFS&#xff08;广度优先遍历&#xff09;1. 什么是 BFS&#xff1f;2. 特点和应用3. BFS 示例 DFS&#xff08;深度优先搜索&#xff09;1. 什么是 DFS&#xff1f;2. DFS 的基本步骤3. 特点4. DFS 的应用5. DFS 示例 最小生成树问题1. 什么是最小生成树&…

【算法笔记】双指针算法深度剖析

【算法笔记】双指针算法深度剖析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;算法笔记 文章目录 【算法笔记】双指针算法深度剖析前言一.移动零1.1题目1.2思路分析1.3代码实现二.复写零2.1题目2.2思路分析2.3代码实现 三.快乐数3.1题目3…

微服务实战——ElasticSearch(保存)

商品上架——ElasticSearch&#xff08;保存&#xff09; 0.商城架构图 1.商品Mapping 分析&#xff1a;商品上架在 es 中是存 sku 还是 spu &#xff1f; 检索的时候输入名字&#xff0c;是需要按照 sku 的 title 进行全文检索的检索使用商品规格&#xff0c;规格是 spu 的…

基于Springboot+Vue的小区停车场管理系统登录(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 在这个…

uniapp 微信发布注意事项

uniapp的微信播放不支持本地文件&#xff0c;起始微信原生语言是支持的 所以在编写uniapp代码时 要写两套逻辑 // #ifdef MP-WEIXIN 微信原封不变的自己写法 //#endif // #ifndef MP-WEIXIN 其他写法 //#endif 这样可实现 发布到微信后 微信原封不动的使用自己写…

初识算法 · 双指针(3)

目录 前言&#xff1a; 和为s的两数之和 题目解析&#xff1a; ​编辑 算法原理&#xff1a; 算法编写&#xff1a; 三数之和 题目解析 算法原理 算法编写 前言&#xff1a; 本文通过介绍和为S的两数之和&#xff0c;以及三数之和&#xff0c;对双指针算法进行深一步…

进度条(倒计时)Linux

\r回车(回到当前行开头) \n换行 行缓冲区概念 什么现象&#xff1f; 什么现象&#xff1f;&#xff1f; 什么现象&#xff1f;&#xff1f;&#xff1f; 自己总结&#xff1a; #pragma once 防止头文件被重复包含 倒计时 在main.c中&#xff0c;windows.h是不可以用的&…

Windows 环境搭建 CUDA 和 cuDNN 详细教程

CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由NVIDIA公司推出的一个并行计算平台和编程模型&#xff0c;它允许开发者使用NVIDIA GPU进行通用计算&#xff08;即GPGPU&#xff09;&#xff0c;从而加速各种计算密集型任务。CUDA提供了一套基于C/C…

linux文件编程_线程

1. 基本概念 1.1. 进程与线程的概念 典型的UNIX/linux进程可以看成是只有一个控制线程&#xff0c;一个进程在同一时刻只做一件事情&#xff0c;有了多个控制线程后&#xff0c;在程序设计时可以把进程设计成在同一时刻做不止一件事&#xff0c;每个线程各自处理独立的任务。…

Web安全 - 文件上传漏洞(File Upload Vulnerability)

文章目录 OWASP 2023 TOP 10导图定义攻击场景1. 上传恶意脚本2. 目录遍历3. 覆盖现有文件4. 文件上传结合社会工程攻击 防御措施1. 文件类型验证2. 文件名限制3. 文件存储位置4. 文件权限设置5. 文件内容检测6. 访问控制7. 服务器配置 文件类型验证实现Hutool的FileTypeUtil使用…

STM32使用Keil5 在运行过程中不复位进入调试模式

一、选择Options for Target进入设置 二、选择所使用的调试器&#xff0c;这里以ST-Link为例。取消勾选Load Application at Startup 可以在进入调试模式的时候不会从新加载程序&#xff01;从而不破坏现场 三、点击Setting进入 四、取消勾选Reset after Connect 使得调试器连接…

探索 aMQTT:Python中的AI驱动MQTT库

文章目录 探索 aMQTT&#xff1a;Python中的AI驱动MQTT库背景介绍aMQTT是什么&#xff1f;如何安装aMQTT&#xff1f;简单库函数使用方法场景应用常见问题及解决方案总结 探索 aMQTT&#xff1a;Python中的AI驱动MQTT库 背景介绍 在物联网和微服务架构的浪潮中&#xff0c;MQ…

Redis:string类型

Redis&#xff1a;string类型 string命令设置与读取SETGETMSETMGET 数字操作INCRINCRBYDECRDECRBYINCRBYFLOAT 字符串操作APPENDSTRLENGETRANGESETRANGE 内部编码intembstrraw 在Redis中&#xff0c;字符串string存储的是二进制&#xff0c;以byte为单位&#xff0c;输入的二进…

ICPC-day1(NTT)

NTT经典例题 CCPC-Winter-Camp-day6-A——NTT经典例题 对于上面格式&#xff0c;如果想求出每个i的值可以使用卷积求出&#xff0c;因为阶乘j和阶乘i-j相乘的值为(i(i-j))i 补充一个二次剩余定理 P5491 【模板】二次剩余 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) //#in…