【C++】二叉搜索树的原理及实现

news2024/9/19 9:44:39

简介

  二叉搜索树(Binary Search Tree,BST)是一种常用的数据结构,本文将介绍二叉搜索树的原理与特性,并给出C++代码实现,最后对其性能进行详细的分析。 

 

文章目录

简介

一、二叉搜索树的概念

二、二叉搜索树的操作及实现

2、1 二叉搜索树的插入

2、1、1 插入的原理

2、1、2 插入的代码实现

2、2 二叉搜索树的查找

2、2、1 查找的原理

2、2、2 查找的代码实现

2、3 二叉搜索树的删除

2、3、1 删除的原理

2、3、2 删除的代码实现

2、4 二叉搜索树的中序遍历

2、5 递归实现二叉树的操作

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


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:C++  👀

💥 标题:二叉搜索树💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️ 

 

一、二叉搜索树的概念

  二叉搜索树又称二叉排序树,二叉搜索树是一种二叉树,其中每个节点的值大于其左子树中的任何节点,并且小于其右子树中的任何节点。这个特性使得二叉搜索树具有高效的查找、插入和删除操作。下图即为二叉搜索树:

 

二、二叉搜索树的操作及实现

  由于二叉搜索树的特性,使得二叉搜索树具有高效的查找、插入和删除操作。在我们分析各个操作的效率和实现原理之前,我们先把二叉树的大体结构列出,代码如下:

template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

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

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

2、1 二叉搜索树的插入

2、1、1 插入的原理

  插入一个新的值时,我们需要遵守二叉搜索树的特性。首先,我们从根节点开始找到合适的插入位置。具体操作是,将新值与当前节点的值比较,若新值小于当前节点的值,则往左子树方向找到合适的叶子节点进行插入;反之,若新值大于当前节点的值,则往右子树方向找到合适的叶子节点进行插入

  合适的叶子节点指的是一直往下查找,直到该位置为空(nullptr)时,此时新值就应该插入该位置。即使我们找到了合适的位置,如果不知道该位置的父节点的话,似乎并不能连接到该树中。所以在查找合适位置的同时,还需要维护一个父节点。但是我们需要注意,二叉搜索树中没有重复的值。如果插入重复的值,那么就会插入失败

  同时,我们再插入前,要判断该树是否为空。否则就会出现意想不到的bug。

2、1、2 插入的代码实现

  我们看代码实现:

    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
		{
			parent->_left = cur;
		}

		return true;
	}

2、2 二叉搜索树的查找

2、2、1 查找的原理

  其实在上述的插入中,我们不就进行了查找吗?!为了查找一个特定的值,我们从根节点开始向下遍历二叉树,根据当前节点的值与目标值的大小关系来选择往左子树或者右子树进行遍历。如果找到目标值,则返回成功;否则,如果遍历到叶子节点还未找到目标值,则返回失败。

2、2、2 查找的代码实现

	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 true;
			}
		}

		return false;
	}

2、3 二叉搜索树的删除

2、3、1 删除的原理

  删除操作是相对复杂的,因为我们需要处理不同的情况。具体步骤如下:

  1. 如果要删除的节点没有子节点,直接删除即可。
  2. 如果要删除的节点只有一个子节点,将子节点替换为要删除的节点即可。
  3. 如果要删除的节点有两个子节点,需要用其右子树中最小的节点替换要删除的节点,并且删除右子树中最小的节点。

  对上述的情况在进行分析和总结,一共可分为如下情况:

  1. 要删除的结点只有左孩子结点 。删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除。
  2. 要删除的结点只有右孩子结点 。删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除。
  3. 要删除的结点有左、右孩子结点。在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

  为什么是上述的三种情况呢?我们详细分析一下是为什么。

  假如我们要删除的节点没有子节点,我们可以把这种情况看成要删除的结点只有左孩子结点或者只有右孩子结点。把另一个存在的孩子看成空(nullptr)。这样删除后,直接可让其父节点指向空(nullptr),而不是野指针。

  要删除的结点只有左孩子结点或者要删除的结点只有右孩子结点是两种不同的情况。因为他们的操作是不同的。

  要删除的结点有左、右孩子结点这种情况较为复杂。首先我们应该找到能够填充该位置的节点。根据二叉搜索树的特性每个节点的值大于其左子树中的任何节点,并且小于其右子树中的任何节点,我们找到的值应该也满足此特点。有两个节点的只满足该情况:该节点左子树的最大值、该节点右子树的最小值。本篇文章讲述的是左子树的最大值。找左子树的最大值,就是该子树最右边的节点。找到后交值换再删除。

  要删除的结点有左、右孩子结点这种情况,在找左子树的最大值时也应该维护一个父节点。为什么呢?因为我们找到左子树的最大值时,与要删除的节点的值交换后,要删除该节点(交换前的左子树最大值的节点)。此时该节点的右节点一定为空(nullptr),只需要关心左节点就行。

2、3、2 删除的代码实现

	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->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}// 右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}					
				} // 左右都不为空 
				else
				{
					// 找替代节点
					Node* parent = cur;
					Node* leftMax = cur->_left;
					while (leftMax->_right)
					{
						parent = leftMax;
						leftMax = leftMax->_right;
					}

					swap(cur->_key, leftMax->_key);

					if (parent->_left == leftMax)
					{
						parent->_left = leftMax->_left;
					}
					else
					{
						parent->_right = leftMax->_left;
					}

					cur = leftMax;
				}

				delete cur;
				return true;
			}
		}

		return false;
	}

2、4 二叉搜索树的中序遍历

  二叉搜索树又称二叉排序树,为什么又名二叉排序树呢?二叉搜索树的中序遍历的结果就是一个有序的结果。代码如下:

public:
    Inorder()
    {
        _Inorder(_root);
    }
private:   
    void _Inorder(Node* root)                                                                                                                                
    {    
        if(root==nullptr)    
        {    
            return ;    
        }    
    
        _Inorder(root->left);    
        cout<<root->_key<<" ";    
        _Inorder(root->right);    
    } 

2、5 递归实现二叉树的操作

  我们上述讲解的是非递归形式的二叉搜索树的各个操作。当我们了解非递归形式的二叉搜索树的各个操作后,我们下面给出递归形式的二叉搜索树的各个操作的代码,思路就不在讲解:

public:
    bool eraseR(const K& key)
    {
        return _eraseR(_root,key);
    }
                                                                                                                                                             
    bool insertR(const K& key)
    {
        return _insertR(_root,key);
    }

    bool findR(const K& key)
    {
        return _findR(_root,key);
    }
private:
    bool _findR(Node* root,const K& key)
    {
        if(root==nullptr)
        {                                                                                                                                                    
            return false;
        }

        
        if(root->_key>key)
        {
            _findR(root->left,key);
        }
        else if(root->_key<key)
        {
            _findR(root->right,key);
        }
        else
        {
            return true;
        }
    }

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

        if(root->_key>key)
        {
            _eraseR(root->left,key);
        }
        else if(root->_key<key)
        {
            _eraseR(root->right,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)
                {
                    min=min->left;
                }

                swap(root->_key,min->_key);
                                                                                                                                                             
                return _eraseR(root->right,key);
            }

            delete del;
            return true;
        }
                
    }

    bool _insertR(Node*& root,const K& key)
    {
        if(root==nullptr)
        {
            root=new Node(key);
            return true;
        }

        if(root->_key>key)
        {
            _insertR(root->left,key);
        }                                                                                                                                                    
        else if(root->_key<key)
        {
            _insertR(root->right,key);
        }
        else
        {
            return false;
        }
    }

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

  插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

  通过上述我们也发现,二叉搜索树的性能主要取决于树的平衡度。最理想的情况下,树是完全平衡的,即左子树节点数目和右子树节点数目相差不超过1。在这种情况下,查找、插入和删除操作的平均时间复杂度为 O(log n)。但是,最坏情况下,树可能变得非平衡,导致这些操作的时间复杂度退化为O(n),其中n是树中节点的总数。

  为了避免二叉搜索树在使用过程中出现不平衡的情况,可以使用自平衡的二叉搜索树,如红黑树或AVL树。这些树通过旋转、调整节点颜色等策略来保持树的平衡度,从而提高了整体性能。

  总结起来,二叉搜索树在C++编程语言中的实现非常灵活且易于理解。但需要注意的是,对于大型数据集合,建议使用自平衡的二叉搜索树,以确保操作的效率和性能。

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

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

相关文章

01|Oracle学习(监听程序、管理工具、PL/SQL Developer、本地网络服务介绍)

基础概念 监听程序&#xff1a;运行在Oracle服务器端用于侦听客户端请求的程序。 相当于保安&#xff0c;你来找人&#xff0c;他会拦你&#xff0c;问你找谁。他去帮你叫人过来。 配置监听程序应用场景 Oracle数据库软件安装之后没有监听程序&#xff08;服务&#xff09;…

pdf阅读器哪个好用?这个阅读器别错过

pdf阅读器哪个好用&#xff1f;PDF是一种流行的文件格式&#xff0c;可以保留文档的原始格式、布局和字体。与其他文档格式相比&#xff0c;PDF在不同设备和操作系统上的显示效果更为一致&#xff0c;确保文档内容的准确性和可读性。在阅读一些PDF文件的时候&#xff0c;使用一…

文本怎么用手机生成二维码?二维码在线文本码制作技巧

现在二维码可以展示的内容越来越丰富&#xff0c;比如文本就是很常见的一种形式。编辑好文本内容之后&#xff0c;将文字内容添加到二维码中&#xff0c;其他人扫码就可以获取到文字内容&#xff0c;那么文本二维码该如何制作呢&#xff1f;想要制作二维码&#xff0c;那么可以…

全网最牛,postman接口测试-高级应用实战(总结)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 流程控制 流程控…

【Excel自动化办公】openpyxl如何实现Excel超链接批量化设置?

Python是一种高效的编程语言&#xff0c;它可以方便地处理各种文件格式&#xff0c;包括Excel。 在Excel中&#xff0c;超链接是一种非常有用的功能&#xff0c;它可以让用户快速跳转到其他工作表或其他文件中的特定单元格。 在本文中&#xff0c;我们将介绍如何使用Python来…

openpnp - Enable Visual Homing

文章目录 openpnp - Enable Visual Homing概述笔记视觉归零设置END openpnp - Enable Visual Homing 概述 设备已经完全通过openpnp的校验了. 当时发现一个问题, 视觉归零如果使能, 就会使主校准点和次校准点的位置发生变化, 导致顶部相机十字不能准确的落在主/次校准点上. 当…

【计算机网络】HTTP 协议详解

文章目录 1. HTTP 协议介绍2. HTTP 协议的工作过程3. Fiddler 抓包工具介绍3.1 抓包工具的使用3.2 抓包结果3.3 抓包工具原理 4. HTTP 协议格式总览5. HTTP 请求&#xff08;Request&#xff09;5.1 认识 URLURL 基本介绍URL 基本格式URL 参数介绍URLencode 介绍 5.2 认识“方法…

同一数据集(相同路径)的 FID 为负数

公众号&#xff1a;EDPJ 先说结论&#xff1a;这是算法中对复数取实部的结果&#xff0c;对 FID 的影响不大。 FID是从原始图像的计算机视觉特征的统计方面&#xff0c;来衡量两组图像的相似度&#xff0c;是计算真实图像和生成图像的特征向量之间距离的一种度量。 这种视觉特…

CASS数据带属性转GIS的shp数据教程

一、数据&#xff1a;DWG文件中含有JZD&#xff08;宗地层&#xff09;&#xff0c;JZP&#xff08;界址点层&#xff09;&#xff0c;其中JZP中含有界址点号&#xff0c;实现JZD层转成ZD的shp数据&#xff1b;JZP转成JZD点的shp数据&#xff0c;并带出界址点号。 二、实现原理…

flutter 导出iOS问题2

问题1:The Swift pod FirebaseCoreInternal depends upon GoogleUtilities, which does not define modules. To opt into those targets generating module maps (which is necessary to import them from Swift when building as static libraries) 参考 正如上图报错第三方…

基于P2P技术的远距离传输大型文件解决方案

随着互联网的普及和数据存储的快速发展&#xff0c;远距离传输大型文件成为一种常态。传统的文件传输方式&#xff0c;如邮件、FTP等&#xff0c;通常只适用于小型文件的传输&#xff0c;对于大型文件的传输则效率低下并且很容易遇到传输失败的问题。面对这个问题&#xff0c;P…

一起学算法(位运算篇)

1.位运算 1.二进制数值表示 在计算机中&#xff0c;我们可以用单纯的0和1来表示数字&#xff0c;一般不产生歧义&#xff0c;我们会在数字的右下角写上它的进制&#xff0c;例如&#xff1a;1010&#xff08;10&#xff09;其表示的是1010&#xff0c;1010&#xff08;2&#…

职工管理系统C++

1、管理系统需求 职工管理系统可以用来管理公司内所有员工的信息 本教程主要利用C来实现一个基于多态的职工管理系统 公司中职工分为三类&#xff1a;普通员工、经理、老板&#xff0c;显示信息时&#xff0c;需要显示职工编号、职工姓名、职工岗位、以及职责 普通员工职责…

一文了解JavaScript 与 TypeScript的区别

TypeScript 和 JavaScript 是两种互补的技术&#xff0c;共同推动前端和后端开发。在本文中&#xff0c;我们将带您快速了解JavaScript 与 TypeScript的区别。 一、TypeScript 和 JavaScript 之间的区别 JavaScript 和 TypeScript 看起来非常相似&#xff0c;但有一个重要的区…

CASAIM自动化平面度检测设备3D扫描零部件形位公差尺寸测量

平面度是表面形状的度量&#xff0c;指示沿该表面的所有点是否在同一平面中&#xff0c;当两个表面需要连接在一起形成紧密连接时&#xff0c;平面度检测至关重要。 CASAIM自动化平面度检测设备通过搭载领先的激光三维测头和智能检测软件自动获取零部件高质量测量数据&#xf…

Vue 入门(一)

一、注意 Vue 不支持 IE8 及以下的版本&#xff0c;因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性&#xff0c;但它支持所有兼容 ECMAScript 5 的浏览器。 二、创建 Hello Vue Vue.js 的核心是实现了 MVVM 模式&#xff0c;它扮演的角色就是 ViewModel 层&#xff0c;那么…

STM32使用HAL库中外设初始化MSP回调机制及中断回调机制详解

STM32使用HAL库之Msp回调函数 1.问题提出 在STM32的HAL库使用中&#xff0c;会发现库函数大都被设计成了一对&#xff1a; HAL_PPP/PPPP_Init HAL_PPP/PPPP_MspInit 而且HAL_PPP/PPPP_MspInit函数的defination前面还会有__weak关键字 上面的PPP/PPPP代表常见外设的名称为…

Vue配置代理(解决跨域请求)

app.vue <template><div><button click"getStudents">获取学生信息</button><button click"getCars">获取汽车信息</button></div></template><script> import axios from axios export default {…

2.4g遥控卧室床头氛围灯开发方案

台灯作为我们日常生活的照明工具&#xff0c;不但给我们的生活提供了很大的便利&#xff0c;而且也丰富了我们的世界。随着电子产品的快速发展&#xff0c;家用电器逐渐趋向智能化&#xff0c;台灯也更加智能。智能台灯以2.4g合封芯片为主控芯片&#xff0c;实现遥控器控制台灯…

依托大数据信息技术构建智慧水务系统,推动城市供水快速发展

随着城市化进程的步伐大大变快&#xff0c;城市建设与科学信息技术的融合程度也在不断提升&#xff0c;尤其是大数据信息技术的迅猛发展&#xff0c;为民生工程由信息化向智能化转型提供了条件。以城市的水务系统为例&#xff0c;依托大数据信息技术构建智慧水务系统是智慧城市…