【数据结构】6.3 二叉搜索树(C++)

2023-11-14

【数据结构】——6.3 二叉搜索树

一、二叉搜索树的概念

二叉搜索树(Binary Search Tree):也叫二叉排序树二叉查找树

二叉查找树有以下特性:

  1. 左子树的所有值比当前节点小,右子树的所有值比当前节点大
  2. 左右子树都是二叉搜索树
  3. 空树或仅有一个结点的树也是一个二叉搜索树

以下便是一个二叉搜索树:

二叉搜索树

注意:在二叉搜索树中,左子树中所有节点的值都比当前节点小,右子树中所有节点的值都比当前节点大,而不是仅有左孩子比当前节点小,右孩子比当前节点大

如以下所示:17是右子树的节点,但是却比20小,所以该树不是二叉搜索树

非二叉搜索树

二叉搜索树的每个节点左孩子一定比当前节点小,右孩子一定比当前节点大。

因此它在查找值的时候,不需要遍历整个二叉树,而是只需要判断查找值是否比当前节点小,如果是则走左边走,如果不是,则往右边走,极大得提高了查找的效率,二叉搜索树因此而得名。

在以下二叉树中查找值为17的节点:

二叉搜索树的查找

二叉搜索树在查找结点时,最坏的结果只需要遍历树的深度个节点,不需要遍历整个树的所有节点,而在二叉搜索树接近平衡时,查询的时间复杂度仅有 O ( l o g 2 n ) O(log_2n) O(log2n)

而且二叉树因为这个特性,它的中序遍历结果是有序的。并且不能存储重复的数据,通常也被用来去重排序

二、二叉搜索树的实现

1. 存储结构和实现接口

对于二叉搜索树,我们使用二叉树来存储

  1. 在二叉搜索树中,存储的数据不允许重复,它们被称为键(Key)。但是库里面有可重复的二叉搜索树供我们使用,只需要将重复数据统一放在当前节点的左边或右边即可,我们不实现可重复的版本
  2. 由于我们需要二叉搜索树中可以存储任意类型的数据,因此我们使用模板编程
  3. 对于增、删、查操作,我们还会给出递归和非递归的实现版本

二叉搜索树的接口非常少,我们只实现如下所示的接口:

namespace Name
{
    // 节点类型
    template<class K>
	struct BSTNode
	{
		BSTNode* _left;			// 左孩子指针
		BSTNode* _right;		// 右孩子指针
		K _key;					// 键
        
        // 构造函数
        BSTNode(const K& key)
			: _key(key)
			, _left(nullptr)
			, _right(nullptr)
		{
			;
		}
	};

    // 二叉搜索树的实现
	template<class K>
	class BSTree
	{
		typedef BSTNode<K> Node;	// 将节点类型重命名为Node
        
	public:
        bool Insert(const K& key);	// 插入
        bool Erase(const K& key);	// 删除
        bool Find(const K& key);	// 查找
        void InOrder(void);			// 中序遍历
        
        bool InsertR(const K& key);	// 插入,递归版本
        bool EraseR(const K& key);	// 删除,递归版本
        bool FindR(const K& key);	// 查找,递归版本
        
    private:
        Node* _root;	// 根节点指针
    };
}

2. 方法的实现

2.1 默认成员函数

(1)构造函数

只需要将头指针置为空即可

// 默认构造函数
BSTree(void)
    : _root(nullptr)
{
	;
}

(2)析构函数

  1. 搜索二叉树的析构过程使用后序遍历,依次释放节点
  2. 我们使用递归的方式遍历树,析构函数中不允许有参数,因此我们将后序序遍历的析构过程封装为一个_Destroy函数,再让析构构造去调用它。为了不让外界用户调用_Destroy函数,我们可以将其设为私有
// 析构函数
~BSTree(void)
{
    _Destroy(_root);
    _root = nullptr;
}

private:
// 后序遍历释放节点
void _Destroy(Node* root)
{
    if (root == nullptr)
    {
        return;
    }

    _Destroy(root->_left);
    _Destroy(root->_right);
    delete root;
}

(3)拷贝构造

  1. 拷贝二叉搜索树是将被拷贝的树进行先序遍历,依次拷贝该节点。
  2. 我们使用递归的方式遍历树,但是拷贝构造的参数中不能再传入根节点指针。
  3. 因此我们将先序遍历的拷贝过程封装为一个_Copy函数,再让拷贝构造去调用它。为了不让外界用户调用_Copy函数,我们可以将其设为私有
// 拷贝构造
BSTree(const BSTree& t)
{
    _root = _Copy(_root, t._root);
}

private:
// 先序遍历递归拷贝节点
Node* _Copy(Node* root, const Node* src)
{
    if (src == nullptr)
    {
        return nullptr;
    }

    root = new Node(src->_key);
    root->_left = _Copy(root->_left, src->_left);
    root->_right = _Copy(root->_right, src->_right);
    return root;
}

(4)赋值重载

  1. 我们依然是使用临时对象拷贝构造被复制对象,然后让自己夺取临时对象的数据
  2. 如果直接使用赋值获取临时对象的内容,临时对象析构时会释放自己的空间,导致内存泄漏。因此使用swap交换,让临时对象析构时顺便释放掉自己原来的空间
// 赋值重载函数
BSTree& operator=(const BSTree& t)
{
    if (this != &t)
    {
        BSTree temp(t);
        std::swap(temp._root, _root);
    }
    return *this;
}

2.2 查找find

  1. 当二叉树非空时,判断当前节点的大小,比该节点小的往左走,比该节点大的往右走,直到找到该节点。
  2. 找到该节点返回true,没找到返回false。查找只是看该key值是否存在,二叉搜索树如果允许被随意修改会破坏二叉搜索树的存储结构

(1)非递归实现:

bool Find(const K& key)
{
    Node* cur = _root;
    while (cur != nullptr)
    {
        if (key < cur->_key)
        {
            // 比当前节点小,往左走
            cur = cur->_left;
        }
        else if (key > cur->_key)
        {
            // 比当前节点大,往右走
            cur = cur->_right;
        }
        else
        {
            // 与当前节点相等,返回true
            return true;
        }
    }

    return false;
}

(2)递归实现

由于根节点指针_root是类的私有变量,所以用户无法使用对象传入根节点指针,而递归需要根节点指针作为参数,所以我们将递归过程封装成一个_FindR函数,并将其设为私有函数,让FindR函数调用它来完成递归。

// 查找key,递归实现
bool FindR(const K& key)
{
    return _FindR(_root, key);
}

private:
// 查找的递归过程
bool _FindR(Node* root, const K& key)
{
    if (root == nullptr)
    {
        return false;
    }

    if (key < root->_key)
    {
        return _FindR(root->_left, key);
    }
    else if (key > root->_key)
    {
        return _FindR(root->_right, key);
    }
    else
    {
        return true;
    }
}

2.3 插入insert

  1. 当二叉搜索树为空时,直接将节点挂到_root指针上
  2. 当二叉树非空时,判断当前节点的大小,比该节点小的往左走,比该节点大的往右走,直到走到根节点,插入指定位置。
  3. 如果查找过程出现与插入值一样的节点,则返回false

(1)非递归实现

bool Insert(const K& key)
{
    Node* newNode = new Node(key);

    if (_root == nullptr)
    {
        _root = newNode;
    }
    else
    {
        // 查找插入位置
        Node* parent = nullptr, * cur = _root;
        while (cur != nullptr)
        {
            if (key < cur->_key)
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (key > cur->_key)
            {
                parent = cur;
                cur = cur->_right;
            }
            else
            {
                return false;
            }
        }

        // 插入节点
        if (key < parent->_key)
        {
            parent->_left = newNode;
        }
        else if (key > parent->_key)
        {
            parent->_right = newNode;
        }
    }
    return true;
}

(2)递归实现

当遍历到根节点时,我们需要在根节点处插入新节点。因此我们要用指针记录被插入的根节点,可以让当前节点的左右孩子为空作为终止条件。

但是我们现在使用的是C++语法,就多了一个选择,将root指针设置为指针的引用类型,那么我们直接对参数root做出修改就可以将新节点连接到二叉搜索树中

// 插入的递归实现
bool InsertR(const K& key)
{
    return _InsertR(_root, key);
}

private:
// 插入的递归过程
bool _InsertR(Node*& root, const K& key)	// root的类型为指针的引用类型
{
    if (root == nullptr)
    {
        // 遍历到根节点,创建节点并插入
        root = new Node(key);		// 可以直接改变二叉搜索树的内容
        return true;
    }

    // 查找插入位置
    if (key < root->_key)
    {
        return _InsertR(root->_left, key);
    }
    else if (key > root->_key)
    {
        return _InsertR(root->_right, key);
    }
    else
    {
        return false;
    }
}

2.4 删除erase

(1)实现思路

  1. 当被删除的节点是叶子节点时,直接删除就可以了
  2. 当被删除的节点只有一个孩子时,直接删除它,并让它的父节点领养它的孩子
  3. 当被删除的节点有2个孩子时,直接删除是比较麻烦的,我们可以在它的左右子树里找一个叶子节点替换它,然后删除这个叶子节点即可

那么哪些叶子节点能与被删除节点进行交换呢?

  1. 左子树最大值,在左子树的最右边位置:

左子树最大值

  1. 右子树最小值,在右子树的最左边位置:

右子树最小值

(2)非递归实现

bool Erase(const K& key)
{
    // 查找被删除节点
    Node* cur = _root, *parent = _root;
    while (cur != nullptr)
    {
        if (key < cur->_key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (key > cur->_key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else
        {
            // 删除key
            if (cur->_left == nullptr)
            {
                // 没有左子树,继承右子树
                if (_root == cur)
                {
                    // 被删除节点为根节点
                    _root = cur->_right;
                }
                else
                {
                    // 被删除节点为非根节点
                    if (parent->_left == cur)
                    {
                        parent->_left = cur->_right;
                    }
                    else
                    {
                        parent->_right = cur->_right;
                    }
                }
                delete cur;
            }
            else if (cur->_right == nullptr)
            {
                // 只有左子树,继承左子树
                if (_root == cur)
                {
                    // 被删除节点为根节点
                    _root = cur->_left;
                }
                else
                {
                    // 被删除节点为非根节点
                    if (parent->_left == cur)
                    {
                        parent->_left = cur->_left;
                    }
                    else
                    {
                        parent->_right = cur->_left;
                    }
                }
                delete cur;
            }	
            else
            {
                // 左右子树都有,选右边最小
                Node* minParent = cur, * min = cur->_right;
                while (min->_left != nullptr)
                {
                    minParent = min;
                    min = min->_left;
                }
                cur->_key = min->_key;

                if (min == minParent->_right)
                    minParent->_right = min->_right;
                else
                    minParent->_left = min->_right;
                delete min;
            }

            return true;
        }
    }
    return false;
}

(3)递归实现

  1. 这里的递归过程依然要提取成一个函数,并且参数用引用类型
  2. 递归删除被交换的叶子节点时,要从被删节点的位置重新向下找,因为使用局部变量找到的叶子节点指针是局部变量,参数中的引用类型会改变局部变量的值,而不会改变二叉搜索树中的内容。
bool _EraseR(Node*& root, const K& key)
{
    if (root == nullptr)
    {
        return false;
    }

    // 查找被删节点
    if (key < root->_key)
    {
        return _EraseR(root->_left, key);
    }
    else if (key > root->_key)
    {
        return _EraseR(root->_right, key);
    }
    else
    {
        Node* del = nullptr;
        if (root->_left == nullptr)
        {
            // 只有左孩子
            del = root;
            root = root->_right;
        }
        else if (root->_right == nullptr)
        {
            // 只有右孩子
            del = root;
            root = root->_left;
        }
        else
        {
            // 有两个孩子(交换左子树最大值)
            Node* max = root->_left;
            while (max->_right != nullptr)
            {
                max = max->_right;
            }
            std::swap(max->_key, root->_key);
            
            // return _EraseR(max, key);	max是函数的局部变量,参数中的引用类型更改的不是原树中的指针
            return _EraseR(root->_left, key);	// 继续向下遍历删除被交换过的叶子节点
        }
        delete del;
        return true;
    }
}

2.5 中序遍历

中序遍历的递归过程也是要独立封装成函数

// 中序遍历
void InOrder(void)
{
    _InOrder(_root);
    std::cout << std::endl;
}

// 中序遍历的递归过程
void _InOrder(Node* root)
{
    if (root == nullptr)
    {
        return;
    }

    _InOrder(root->_left);
    std::cout << root->_key << " ";
    _InOrder(root->_right);
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【数据结构】6.3 二叉搜索树(C++) 的相关文章

  • 模板类包装任意类型/非类型模板类

    假设我有一个模板类base和一个班级wrapper其中包含一个实例化成员base 我想定义班级wrapper这样它依赖于模板参数包 该参数包只是 传递 给实例化成员base 例如 考虑下面的代码 它工作得很好 include
  • 从 SQL 数据库获取日期时间

    我的数据库表中有一个 DateTime 记录 我编写一个查询从数据库中获取它 string command2 select Last Modified from Company Data where Company Name Descrip
  • .NET 可移植类库中的 .ToShortDateString 发生了什么

    我想知道为什么没有 ToShortDateString在 NET 可移植类库中 我有 2 个项目 Silverlight 和常规 NET 类库 使用相同的代码 并且代码涉及调用 ToShortDateString on a DateTime
  • 我应该在单元测试中使用 AutoMapper 吗?

    我正在为 ASP NET MVC 控制器方法编写单元测试 这些控制器依赖于IMapper 我创建的用于抽象 AutoMapper 的接口 使用 Castle Windsor 通过构造函数注入传入 动作方法使用IMapper从领域对象映射到
  • 使用 C# 使用应用程序密码登录 Office 365 SMTP

    在我们的 Office 365 公司帐户中实施两步身份验证之前 我的 C WPF 程序已成功进行身份验证并发送邮件 我使用了 SmtpClient 库 但现在我必须找到另一个解决方案 因为它不再起作用 我找不到任何使用 O365 应用程序密
  • 当我单击 GridView 项时返回 ImageView 实例

    当我点击GridView项时如何返回ImageView实例 我为 ItemClick 创建自定义绑定事件 public class ItemClickSquareBinding MvxBaseAndroidTargetBinding pri
  • 字节到二进制字符串 C# - 显示所有 8 位数字

    我想在文本框中显示一个字节 现在我正在使用 Convert ToString MyVeryOwnByte 2 但是 当字节开头有 0 时 这些 0 就会被删除 例子 MyVeryOwnByte 00001110 Texbox shows g
  • 如何检测斑点并将其裁剪成 png 文件?

    我一直在开发一个网络应用程序 我陷入了一个有问题的问题 我会尝试解释我想要做什么 在这里您看到第一个大图像 其中有绿色形状 我想要做的是将这些形状裁剪成不同的 png 文件 并使它们的背景透明 就像大图像下面的示例裁剪图像一样 第一张图像将
  • 推送 Lua 表

    我已经创建了一个Lua表C 但我不知道如何将该表推入堆栈顶部 以便我可以将其传递给 Lua 函数 有谁知道如何做到这一点 这是我当前的代码 lua createtable state libraries size 0 int table i
  • 如何在不使用reinterpret_cast的情况下使用dlsym()加载函数?

    我正在尝试使用 clang tidy 来强制执行 C 核心指南 虽然它确实有很多有效点 但有一件事我无法真正解决 dlsym 返回一个void 我需要以某种方式将其转换为正确的函数指针 为此 我使用reinterpret cast 由于指南
  • 在 C# 中赋值后如何保留有关对象的信息?

    我一直在问我的想法可能是解决方案 https stackoverflow com questions 35254467 is it possible in c sharp to get the attributes attached to
  • C++ Primer 5th Edition 错误 bool 值没有指定最小大小?

    bool 的最小大小不应该是 1 个字节吗 这有点学术性的东西 尽管它们会转换为数字 并且 与其他所有事物一样 它们最终将基本上由计算机内存中的数字表示 但布尔值不是数字 你的bool可以取值true 或值false 即使您确实需要至少 1
  • 使用 AutoMapper 进行 LINQ GroupBy 聚合

    试图让查询工作 但老实说不确定如何 或者是否可能 进行它 因为我尝试过的一切都不起作用 共查询6个表 Person PersonVote PersonCategory Category City FirstAdminDivision Per
  • 如何从枚举中选择随机值?

    给定 C 中的任意枚举 如何选择随机值 我没有找到这个非常基本的问题 我会在一分钟内发布我的答案作为任何人的参考 但请随意发布你自己的答案 Array values Enum GetValues typeof Bar Random rand
  • C# ToString("MM/dd/yy") 删除前导 0 [重复]

    这个问题在这里已经有答案了 可能的重复 格式化 NET DateTime Day 不带前导零 https stackoverflow com questions 988353 format net datetime day with no
  • 通过 MSBuild 调用 cl.exe 时无限期挂起

    我正在尝试在我的 主要是 C 项目上运行 MSBuild 想象一下一个非常庞大的代码库 Visual Studio 2015 是有问题的工具集 Windows 7 SP1 和 VS 2015 更新 2 即使使用 m 1 从而迫使它仅使用一个
  • 改进C++逐行读取文件的能力?

    我正在解析大约 500GB 的日志文件 我的 C 版本需要 3 5 分钟 我的 Go 版本需要 1 2 分钟 我正在使用 C 的流来流式传输文件的每一行以进行解析 include
  • SSBO 是更大的 UBO?

    我目前正在 OpenGL 4 3 中使用 UBO 进行渲染 以将所有常量数据存储在 GPU 上 诸如材料描述 矩阵等内容 它可以工作 但是 UBO 的小尺寸 我的实现为 64kB 迫使我多次切换缓冲区 减慢渲染速度 我正在寻找类似的方法来存
  • 在 C# 中读取/写入命令行程序

    我正在尝试与 C 的命令行程序进行对话 它是一个情绪分析器 它的工作原理如下 CMD gt java jar analyser jar gt Starting analyser 这是我想从我的 C 程序插入内容的地方 例如 I love y
  • 最后从同一类中的其他构造函数调用构造函数

    我在这里读到可以调用另一个构造函数从同一类中的另一个构造函数调用构造函数 https stackoverflow com questions 829870 calling constructor from other constructor

随机推荐

  • SQL server 导入Excel数据

    SQL server 导入Excel数据 编辑 洪伟富 2018 06 07 第一步 对表格数据的处理 这一列数据中有数字 又有中文 如果不做处理 导入数据库会默认为float 从而导致 公教楼201 等字符全部为null 解决办法 用筛选
  • 从键盘上输入身份证号, 判断出生日期,性别

    从键盘上输入身份证号 判断出生日期 性别 倒数第二位是奇数表示男 偶数代表女 public class IdNumber public static void main String args 1 键盘输入身份证号 Scanner intp
  • qt 导出word中插入图片

    QAxObject selection m word gt querySubObject Selection QVariantList params params append 6 params append 0 selection gt
  • 手把手教YOLO系列算法部署之安卓部署

    前言 首先我的yolov5的版本是v6 1 我的部署方式是将模型先转为tflite然后部署到安卓上 大家一般是使用自己的训练模型权重文件来部署 所以我直接讲述自定义模型的部署检测 链接 https pan baidu com s 1bskq
  • shouldComponentUpdate有什么作用

    shouldComponentUpdate有什么作用 shouldComponentUpdate是生命周期之一 是不常用的一个方法 能影响组件是否重新渲染 在更新阶段 当有了new props或者调用setState 方法 在render方
  • Mathorcup数学建模竞赛第三届-【妈妈杯】B题:关于三维健康评分模型的研究(附带赛题解析&获奖论文)(一)

    赛题描述 由于现代社会的生活节奏加快 生活工作压力增大 拥有一个健康的身体显得尤为重要 健康的标准有很多 但是如何量化这些标准 评价一个人的健康状况是一个比较困难的问题 世界卫生组织给出的健康标准有如下几个 1 精力充沛 能从容不迫地应付日
  • 【react】props的基本使用

    props可以用来组件传值 render里面的this指向Person的实例对象 而实例对象上有一个属性props 可以用他来给组件传值 class Person extends React Component render const n
  • SpringCloud——Gateway和过滤器和跨域问题的解决

    介绍 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目 该项目是基于 Spring 5 0 Spring Boot 2 0 和 Project Reactor 等响应式编程和事件流技术开发的网关 它旨在
  • LeetCode(20):有效的括号

    描述 给定一个只包括 的字符串 判断字符串是否有效 有效字符串需满足 左括号必须用相同类型的右括号闭合 左括号必须以正确的顺序闭合 注意空字符串可被认为是有效字符串 示例 输入 输出 true 输入 输出 true 输入 输出 true 输
  • 从ubuntu18 升级到ubuntu20 墙倒屋塌,重新开始

    升级到20 发现了各种不便 最主要是我的机器就是这个运行水平 不能再往上跟了 运行慢将不可容忍 启动慢 不可容忍 点击icon 有几秒种不响应期 难以容忍 与vmware虚拟机15不能很好兼容 并进一步不能与win7兼容 win7我认为是最
  • 基于线性回归对神经网络的解释以及梯度下降鞍点与局部最优的产生原理

    首先 机器学习的本质是让计算机找到一个函数来解决问题 这种函数非常复杂以至于人类无法直接手写出来 本文参考李宏毅教授视频ML 2021 Spring 神经网络是解决线性不可分问题 你可以引入多条线来分割当然我们也可以引入激活函数 非线性函数
  • 计算机视觉小项目—基于RGB颜色特征的火焰识别

    提出问题 随着计算机视觉及图像处理技术的发展 基于计算机视觉的火焰检测技术逐渐取代了传统的火灾检测 由于火焰最显著静态特质是其颜色 火焰识别算法主要利用视频图像中颜色与亮度的相关信息 所以对火焰颜色的特征提取是火焰识别过程的关键 在有关火焰
  • Spring Cloud Ribbon无法将服务名转换为地址

    试了许多网上说的 都不管用 后来发现是spring cloud starter eureka依赖项的版本问题 将版本降低后正常运行
  • fileclude(文件包含漏洞及php://input、php://filter的使用)

    先介绍一些知识 1 文件包含漏洞 和SQL注入等攻击方式一样 文件包含漏洞也是一种注入型漏洞 其本质就是输入一段用户能够控制的脚本或者代码 并让服务端执行 什么叫包含呢 以PHP为例 我们常常把可重复使用的函数写入到单个文件中 在使用该函数
  • Shell编程学习(一)简单介绍和执行shell脚本的方式

    Shell编程是什么 为什么要学习Shell 首先我们要知道 我们的Java项目一般都是部署到Linux系统上的 这是由于Linux相对于Windows而言呢有一下几点 开源 可以修改定制和再发布 安全性更高 虽然windows有很友好的可
  • [Spring学习]01 Spring简介

    目录 一 Spring介绍 二 Spring 官网介绍 三 Spring框架特征 四 Spring核心技术 一 Spring介绍 Spring是一个分层的轻量级开源J2EE框架 由Rod Johnson创建 Spring是一个开源容器框架
  • 【前端关于JSON.parse解析报错处理方案】

    项目场景 vue的移动端项目 ios 解析返回的json报错 JSON parse解析特殊字符报错的解决办法 问题描述 JSON parse 解析该字符串 则会出现报错 安卓可能并不会 原因分析 对于深度嵌套的JSON字符串 使用 JSON
  • 程序员网页版工具集合

    Overview 本文主要收集一下作为程序员经常会用到的一些小工具网站 crontab表达式 https tool lu crontab struct和json互转 https json2struct mervine net json转义工
  • 接口自动化实现图片上传(selenium/RF)

    最近做自动化碰到一个问题 就是带图片上传的不知道怎么实现自动化 整理了下实现如下 上传图片postman 结果请求如下 上传图片后返回一个图片地址 post请求 body 是form data 而不是json fiddler抓取如下 sel
  • 【数据结构】6.3 二叉搜索树(C++)

    数据结构 6 3 二叉搜索树 目录 一 二叉搜索树的概念 二 二叉搜索树的实现 1 存储结构和实现接口 2 方法的实现 2 1 默认成员函数 2 2 查找find 2 3 插入insert 2 4 删除erase 2 5 中序遍历 一 二叉