二叉搜索树实现

news2024/9/22 15:43:42

树的导览

树由节点(nodes)和边(edges)构成,如下图所示。整棵树有一个最上端节点,称为根节点(root)。每个节点可以拥有具有方向的边(directed edges),用来和其他节点相连。相连的节点之中,在上者称为父节点(parent),在下者称为子节点(child)。

一些基本概念:

  • 节点的度:一个节点含有的子树的个数,如果最多只允许两个子节点,即为二叉树
  • 叶节点:无子节点,即度为 0 的节点称为叶节点
  • 兄弟节点:不同的节点拥有相同的父节点
  • 路径长度:根节点到任何节点之间有唯一路径,路径所经过的边数
  • 节点的深度:根节点到任一节点的路径长度,根节点的深度永远为 0
  • 节点的高度:某节点到其叶节点的路径长度

树的导览

二叉搜索树的概念

二叉搜索树可以提供对数时间的元素插入和访问,是一种特殊的二叉树,具有以下规则:

  • 任何节点的键值一定大于其左子树中的每一个节点的键值
  • 任何节点的键值一定小于其右子树中的每一个节点的键值

因此二叉搜索树的最左节点是最小元素,最右节点是最大元素。

二叉搜索树

二叉搜索树的实现

节点的定义

该节点设计很简单:

  • 包含三个成员变量:节点值、左孩子指针、右孩子指针
  • 构造函数:将成员变量初始化
/// @brief 二叉树的节点
/// @tparam K 节点值的类型
template<class K>
struct BSTreeNode {
	BSTreeNode(const K& key = K()) 
		: _key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}

	K _key;					// 节点值
	BSTreeNode<K>* _left;	// 左指针
	BSTreeNode<K>* _right;	// 右指针
};

接口总览

文章完整代码:BinarySearchTree · 秦国维/data-structure

template<class K>
class BSTree {
	typedef BSTreeNode<K> Node;
public:
	Node* Find(const K& key);
	Node* _FindR(Node* _root, const K& key);
	Node* FindR(const K& key);

	bool Insert(const K& key);
    bool _InsertR(Node*& _root, const K& key);
	bool InsertR(const K& key);
    
	bool Erase(const K& key);
	bool _EraseR(Node*& root, const K& key);
	bool EraseR(const K& key);
private:
	Node* _root = nullptr;
};

查找

根据二叉搜索树的特性,可以在二叉搜索树快速的找到指定值:

  • 若 key 值大于当前节点的值,在当前节点的右子树中查找
  • 若 key 值小于当前节点的值,在当前节点的左子树中查找
  • 若 key 值等于当前节点的值,返回当前节点的地址
  • 若找到空,查找失败,返回空指针

非递归

/// @brief 查找指定 key 值
/// @param key 要查找的 key
/// @return 找到返回节点的指针,没找到返回空指针
Node* Find(const K& key) {
    Node* cur = _root;
    while (cur != nullptr) {
        // key 值与当前节点值比较
        if (key > cur->_key) {
            cur = cur->_right;
        } else if (key < cur->_key) {
            cur = cur->_left;
        } else {
            return cur;
        }
    }
    return nullptr;
}

递归

Node* _FindR(Node* root, const K& key) {
    if (root == nullptr) {
        return nullptr;
    }

    // key 值与当前节点值比较
    if (key > root->_key) {
        // key 值大于当前节点的值,递归到右子树查找
        return _FindR(root->_right, key);
    } else if (key < root->_key) {
        // key 值小于当前节点的值,递归到左子树查找
        return _FindR(root->_left, key);
    } else {
        return root;
    }
}

Node* FindR(const K& key) {
    return _FindR(_root, key);
}

插入

根据二叉搜索树的性质,插入操作也很简单:

  • 如果是空树,将插入的节点作为根节点
  • 如果不是空树,利用性质找到该插入的位置,将节点插入

插入新元素时,从根节点开始,遇到键值较大的就向左,遇到键值较小的就向右,一直到尾端,即为插入点。

二叉树插入

非递归

使用非递归插入函数时,需要定义一个 parent 指针,该指针用来指示插入节点的父节点,以便将新节点插入。

/// @brief 在二叉搜索树中插入指定节点
/// @param key 节点的 key 值
/// @return 成功返回 true,失败返回 false
bool Insert(const K& key) {
    if (_root == nullptr) {
        // 第一个插入的节点,构建为根
        _root = new Node(key);
        return true;
    }

    // 先找到要插入的位置
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur != nullptr) {
        if (key > cur->_key) {
            parent = cur;
            cur = cur->_right;
        } else if (key < cur->_key) {
            parent = cur;
            cur = cur->_left;
        } else {
            // 已经有该值了,插入失败
            return false;
        }
    }

    // 创建要插入的节点
    cur = new Node(key);
    // 看插入节点在父节点哪边
    if (key > parent->_key) {
        parent->_right = cur;
    } else {
        parent->_left = cur;
    }
    return true;
}

递归

递归版本的插入相对于非递归版本更简单,要注意的是 Node* 参数一定要传引用,这样才能改变父亲指针的指向。

bool _InsertR(Node*& _root, const K& key) {
    if (_root == nullptr) {
        // 因为传递的是引用,所以可以直接改变指向
        _root = new Node(key);
        return true;
    }

    // 判断 key 与当前节点值大小关系
    if (key > _root->_key) {
        // key 更大就递归到右子树插入
        return _InsertR(_root->_right, key);
    } else if (key < _root->_key) {
        // key 更小就递归到左子树插入
        return _InsertR(_root->_left, key);
    } else {
        return false;
    }
    return false;	// 为了消除编译警告
}

bool InsertR(const K& key) {
    return _InsertR(_root, key);
}

删除

删除相对与插入就复杂的多了,需要考虑三种情况:

  • 待删除节点没有子树
  • 待删除节点有一个子树
  • 待删除节点有两个子树

下面就来讨论这三种情况:

待删除节点没有子树

这种情况比较简单,直接将指向该节点的指针置空即可。

待删除节点有一个子树

如果节点 Q 只有一个子节点,就直接将 Q 的子节点连至 Q 的父节点,并将 Q 删除。

删除1

待删除节点有两个子树

这时就比较复杂了,需要使用替换法。如果 Q 有两个节点,就以右子树的最小节点取代 Q(左子树的最大节点也可以)。

右子树最小节点获取方法:从右子节点开始,一直向左找到底。

为什么右子树最小节点可以替换当前节点?

因为右子树最小节点一定大于当前节点左子树中的所有节点,又一定小于右子树中的其他节点,故不会破坏二叉搜索树的规则。

删除2

非递归

该实现版本同样需要定义一个 parent 指针,以便将其孩子托付给父亲。

/// @brief 在二叉搜索树中删除指针节点
/// @param key 删除节点的 key
/// @return 删除成功返回 true,失败返回 false
bool Erase(const K& key) {
    Node* parent = nullptr;
    Node* cur = _root;
    // 先找要删除节点的位置
    while (cur != nullptr) {
        if (key > cur->_key) {
            // 大于就到右子树中找
            parent = cur;
            cur = cur->_right;
        } else if (key < cur->_key) {
            // 小于就到左子树中找
            parent = cur;
            cur = cur->_left;
        } else {
            // 找到要删除的节点了
            if (cur->_left == nullptr) {
                // cur 左孩子为空,即只有一个右孩子或没有孩子
                if (parent == nullptr) {
                    // 如果要删除的是根节点,更换新根
                    _root = _root->_right;
                } else {
                    if (parent->_left == cur) {
                        parent->_left = cur->_right;
                    } else {
                        parent->_right = cur->_right;
                    }
                }

                // 释放掉 cur 指向的空间
                delete cur;
                cur = nullptr;
            } else if (cur->_right == nullptr) {
                // 此时说明只有左孩子
                if (parent == nullptr) {
                    // 如果要删除的是根节点,更换新根
                    _root = _root->_left;
                } else {
                    if (parent->_left == cur) {
                        parent->_left = cur->_left;
                    } else {
                        parent->_right = cur->_left;
                    }
                }

                // 释放掉 cur 指向的空间
                delete cur;
                cur = nullptr;
            } else {
                // 此时有左右孩子,需要使用替换法删除
                Node* minParent = cur;
                Node* min = cur->_right;
                // 先找到右子树的最左节点
                while (min->_left != nullptr) {
                    minParent = min;
                    min = min->_left;
                }

                // 将最小节点的值替换到 cur 上
                cur->_key = min->_key;
                if (minParent->_left == min) {
                    // 最小节点位于父节点的左边,将它的右子树托付给父节点
                    minParent->_left = min->_right;
                } else {
                    // 最小节点位于父节点的右边,这时就是 cur 的右子树没有左子树
                    // minNode->_left 一定为空,写成这样比较对称
                    minParent->_right = min->_left;
                }

                delete min;
                min = nullptr;
            } // end of if (cur->_left == nullptr)
            return true;
        } // end of if (key > cur->_key)
    } // end of while (cur != nullptr)
    // 没找到要删除的节点,返回 false
    return false;
}

递归

递归版本的删除相对于非递归版本更简单,要注意的是 Node* 参数一定要传引用,这样才能改变父亲指针的指向,将节点删除。

bool _EraseR(Node*& root, const K& key) {
    if (root == nullptr) {
        // 没找到要删除的节点返回 false
        return false;
    }

    if (key > root->_key) {
        // key 更大就到右子树中删除
        _EraseR(root->_right, key);
    } else if (key < root->_key) {
        // key 更小就到左子树中删除
        _EraseR(root->_left, key);
    } else {
        Node* del = root;
        if (root->_left == nullptr) {
            // 此时左为空或左右为空,只需要将右子树给父节点即可
            // 左右为空时,右子树为空,不会违反规则
            root = root->_right;
        } else if (root->_right == nullptr) {
            // 此时右为空,只需要将左子树给父节点
            root = root->_left;
        } else {
            // 有两个孩子,需要替换法删除
            Node* min = root->_right;
            while (min->_left != nullptr) {
                min = min->_left;
            }
            // 把最小的节点的值换上
            root->_key = min->_key;
            // 递归到右子树,将 minnode 删掉
            // 一定要 return,否则会重复释放
            return _EraseR(root->_right, min->_key);
        }

        delete del;
        del = nullptr;
        return true;
    }
    return false;	// 为了消除编译警告
}

bool EraseR(const K& key) {
    return _EraseR(_root, key);
}

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

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

相关文章

第51天|LeetCode503.下一个更大元素 II、LeetCode42. 接雨水

1.题目链接&#xff1a;下一个更大元素 II 题目描述&#xff1a; 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序&#…

Android kotlin 组件间通讯 - LiveEventBus 及测试(更新中)

<<返回总目录 文章目录 一、LiveEventBus是什么二、测试一、LiveEventBus是什么 LiveEventBus是Android中组件间传递消息,支持AndroidX,Event:事件,Bus:总线 范围全覆盖的消息总线解决方案 进程内消息发送App内,跨进程消息发送App之间的消息发送更多特性支持 免配…

进制转换(二进制,八进制,十进制,十六进制)涵盖整数与小数部分,内容的图片全为手写【详细图解】

各种进制之间的相互转换1. 各进制表示数1.1 数码1.2 基数1.3 位权2. 十进制转换为其他进制2.1 整数部分2.2 小数部分3. 其他进制转换为十进制4. 二进制转换为八进制5. 二进制转换为十六进制6. 八进制转换为十六进制1. 各进制表示数 二进制&#xff1a;0&#xff0c;1逢二进一 八…

Java企业开发学习笔记(5下)采用注解方式使用AOP

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【http://t.csdn.cn/FBkpc】 文章目录二、采用注解方式使用AOP2.1 创建所需自包2.2 创建杀龙任务2.3 创建勇敢骑士类2.4 创建吟游诗人切面2.5 创建Spring配置类2.6 创建骑士测试类2.7 运行测试方法testBraveKnight()&…

【学习总结】Kalibr标定相机与IMU

本文仅用于记录自己学习过程。 使用方法 Kalibr包括&#xff1a;相机内参&#xff0c;多相机外参&#xff0c; (已知IMU和相机内参的)相机与IMU标定&#xff0c;以及扩展Kalibr支持IMU内参标定。 当已知IMU内参和相机内参后&#xff0c;使用按照指定方式录制的rosbag&#x…

西电数据库简答题核心考点汇总(期末真题+知识点)

文章目录前言一、关系代数1.1 真题一1.2 真题二二、SQL语句2.1 真题一2.2 真题二三、事务3.1 真题一四、关系理论4.1 真题一4.2 真题二五、数据库设计5.1 样例一5.2 考题二前言 主要针对西安电子科技大学《数据库系统》的概念核心考点进行汇总&#xff0c;包含核心考点。 【期…

第14天-ElasticSearch环境配置,构建检索服务及商品上架到ES库

1.ElasticSearch概念 官网介绍&#xff1a;https://www.elastic.co/cn/what-is/elasticsearch/ 官网学习文档&#xff1a;https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html 1.1.ElasticSearch与MySQL的比较 MySQL有事务性,而ElasticSearch没有…

GEBCO海洋数据下载

一、数据集简介 GEBCO&#xff08;General Bathymetric chart of the Oceans&#xff09;旨在为世界海洋提供最权威的、可公开获取的测深数据集。 目前的网格化测深数据集&#xff0c;即GEBCO_2022网格&#xff0c;是一个全球海洋和陆地的地形模型&#xff0c;在15角秒间隔的…

GAN | 代码简单实现生成对抗网络(GAN)(PyTorch)

2014年GAN发表&#xff0c;直到最近大火的AI生成全部有GAN的踪迹&#xff0c;快来简单实现它&#xff01;&#xff01;&#xff01;GAN通过计算图和博弈论的创新组合&#xff0c;他们表明&#xff0c;如果有足够的建模能力&#xff0c;相互竞争的两个模型将能够通过普通的旧反向…

GMSL相机的相关配置(1)

文章目录一&#xff1a;GMSL相机的信息二&#xff1a;相关配置1.emmc系统下运行upgrade文件2.连接GMSL相机3.给ui可执行文件赋权限4.进入图为GMSL相机配置ui图形界面5.运行程序&#xff0c;打开摄像头一&#xff1a;GMSL相机的信息 我选择相机适配于基于Jetson AGX Orin的图为…

Docker在Windows环境的搭建和使用

文章目录安装WSL安装Docker安装Docker镜像下载Docker镜像启动gpu启动传送文件训练yolov5安装WSL Windows10和11支持Docker的安装&#xff0c;安装需要用到WSL。所以&#xff0c;我们先安装WSL。 参考文章&#xff1a;旧版 WSL 的手动安装步骤 以管理员身份打开powershell, 执行…

matplotlib综合学习

1.arange函数arange函数需要三个参数&#xff0c;分别为起始点、终止点、采样间隔。采样间隔默认值为1看例子&#xff1a; import numpy as np #import matplotlib.pyplot as plt xnp.arange(-5,5,1) print(x)2.绘制sin(x)曲线import numpy as np import matplotlib.pyplot as …

Python jieba分词如何添加自定义词和去除不需要长尾词

Python jieba分词如何添加自定义词和去除不需要长尾词 作者&#xff1a;虚坏叔叔 博客&#xff1a;https://xuhss.com 早餐店不会开到晚上&#xff0c;想吃的人早就来了&#xff01;&#x1f604; 通过如下代码&#xff0c;读取一个txt的高频词汇&#xff1a; # 找到高频词汇t…

苹果触控笔有必要买吗?开学季性价比电容笔推荐

Apple Pencil的性能的确不错&#xff0c;但是由于它的售价实在是太高了&#xff0c;一般人还是舍不得花那么多钱买下来。目前市场上有很多平替的电容笔&#xff0c;不仅价格便宜&#xff0c;而且使用方便。那么&#xff0c;我们应该选择那个牌子的平替电笔呢&#xff1f;在购买…

“智能”创造未来:PDU智能化全面提升IDC数据中心用电能效!

一个月前&#xff0c;万众期盼的《流浪地球2》如期上映&#xff0c;无论是剧情还是特效&#xff0c;让广大观众享受到一次久违的来自中国科幻的震撼&#xff0c;时至今日仍是大家茶余饭后津津乐道的热点谈资。说起这部片子里&#xff0c;最让人紧张的部分&#xff0c;还得数为了…

解决MySQL的 Row size too large (> 8126).

&#x1f4e2;欢迎点赞 &#xff1a;&#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原创&#x1f4e2;作者格言&#xff1a;无尽的折腾后&#xff0c;终于又回到…

电脑系统崩溃怎么修复教程

系统崩溃了怎么办? 如今的软件是越来越复杂、越来越庞大。由系统本身造成的崩溃即使是最简单的操作&#xff0c;比如关闭系统或者是对BIOS进行升级都可能会对PC合操作系统造成一定的影响。下面一起来看看电脑系统崩溃修复方法步骤。 工具/原料&#xff1a; 系统版本&#xf…

LeetCode-47. 全排列 II

目录题目思路回溯法拓展题目来源 47. 全排列 II 题目思路 这道题目和46.全排列的区别在与给定一个可包含重复数字的序列&#xff0c;要返回所有不重复的全排列。 强调的是去重一定要对元素进行排序&#xff0c;这样我们才方便通过相邻的节点来判断是否重复使用了。 我以示例中…

CC2530+ESP8266使用MQTT协议上传阿里云的问题

ATMQTTPUB<LinkID>,<"topic">,<"data">,<qos>,<retain>LinkID: 当前只支持 0 topic: 发布主题, 最长 64 字节 data: 发布消息, data 不能包含 \0, 请确保整条 ATMQTTPUB 不超过 AT 指令的最大长度限制 qos: 发布服务质量, 参…

项目管理软件排行榜!盘点前十名!

项目管理软件排行榜&#xff01;盘点前十名&#xff01; 如今企业规模不断扩大&#xff0c;业务逐渐复杂化&#xff0c;项目管理已经成为现代企业管理中不可或缺的一环。作为协调管理者、团队成员和客户之间交流的工具&#xff0c;项目管理软件不仅可以提高工作效率&#xff0…