C++进阶:二叉搜索树

news2024/11/13 9:49:10

文章目录

  • 1 二叉搜索树概念
  • 2 二叉搜索树的实现
    • 2.1 结点的定义
    • 2.2 二叉搜索树的插入
    • 2.2 二叉搜索树的查找
    • 2.3 二叉搜索树的删除
    • 2.4 二叉搜索树的默认成员函数
      • 2.4.1 拷贝构造
      • 2.4.2 析构函数
      • 2.4.3 赋值重载
  • 3 二叉搜索树的应用
    • 3.1 k模型
    • 3.2 kv模型
  • 4 二叉搜索树的性能分析

1 二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
①若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
②若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
③它的左右子树也分别为二叉搜索树

在这里插入图片描述

2 二叉搜索树的实现

2.1 结点的定义

template <class k>
	struct BSTreeNode
	{
		BSTreeNode<k>* _left;//指向左孩子
		BSTreeNode<k>* _right;//指向右孩子
		k _key;//数据域
		BSTreeNode(const k& key)
			: _left(nullptr)
			, _right(nullptr)
			, _key(key)
		{

		}

	};

2.2 二叉搜索树的插入

插入的具体过程如下:

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

在这里插入图片描述
非递归实现

bool Insert(const k& key)
		{
			if (_root == nullptr)//根为空,直接new一个新节点作为根
			{
				_root = new Node(key);
				return true;
			}
			//根不为空,从根开始依次往下找,直到找到插入结点的位置(即cur为空)
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < key)//key比当前结点的key要大,去右子树查找
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)//key比当前结点的key要小,去左子树查找
				{
					parent = cur;
					cur = cur->_left;
				}
				else//二叉搜索树中不允许存储key值相同的结点
				{
					return false;
				}
			}
			cur = new Node(key);
			if (cur->_key < parent->_key)//判断新new出的结点和parent的关系,将两者连接起来
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			return true;
		}

递归实现

bool InsertR(const k& key)
		{
			return _InsertR(_root, key);
		}
bool _InsertR(Node*& root, const k& key)
		{
			if (root == nullptr)//根为空,直接new出一个新节点当做根
			{
				root = new Node(key);
				return true;
			}
			if (root->_key < key)//key大于当前结点的key,去右子树查找
			{
				_InsertR(root->_right, key);
			}
			else if (root->_key > key)//key小于当前结点的key,去左子树查找
			{
				_InsertR(root->_left, key);
			}
			else if (root->_key == key)//key等于当前结点的key,返回false
			{
				return false;
			}
		}

1 因为_root为私有成员变量,在类外面不能使用,但是递归需要传递_root作为参数,因此这里做了两层嵌套,在类外面调用InsertR()函数,不需要传递任何参数,在类里面通过InsertR()函数调用_InsertR(Node*& root, const k& key) 函数,在类里面可以使用私有变量,从而实现插入操作

2 乍一看代码,只是有查找插入结点的位置,那么是如何插入该结点的呢?这里引用起了很重要的作用

在这里插入图片描述

假设插入的值为16,当走到14的时候,通过比较,16大于14,所以应去14的右子树查找,此时把把14->_right传递给了root,因为是引用,root就是14->_right的别名,因为此时root为空,所以直接new一个新结点,即root=new Node(16),也就是 14->_right=new Node(16),一步,new出了新结点也实现了连接

2.2 二叉搜索树的查找

①从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
②最多查找高度次,走到空,还没找到,这个值不存在。

非递归实现

bool find(const k& key)
		{
			if (_root == nullptr)//根为空,返回false
			{
				return false;
			}
			Node* cur = _root;//从根节点开始查找
			while (cur)
			{
				if (cur->_key < key)//key大于当前结点的key,去右子树查找
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)//key小于当前结点的key,去左子树查找
				{

					cur = cur->_left;
				}
				else//找到了
				{
					return true;
				}
			}
			//查找到空还没找到,说明没有这个结点
			return false;
		}

递归实现

	bool FindR(const k& key)
		{
			return _FindR(_root, key);
		}
	bool _FindR(Node* root, const k& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < key)
			{
				_FindR(root->right, key);
			}
			else if (root->_key > key)
			{
				_FindR(root->left, key);
			}
			else
			{
				return true;
			}
		}
		

2.3 二叉搜索树的删除

删除的具体过程:

首先查找元素是否在二叉搜索树中,如果不存在,则返回false, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来待删除节点有四种情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:使用替换法,找该删除结点的左子树中的最大结点右子树中的最小结点与该结点进行替换,然后删除该结点。
情况d中,使用替换法的原因是删除结点以后,该树还要保持二叉搜索树的结构,因此需要找一个等效结点,替换该结点的位置。并且在替换后该结点可以直接进行删除,那么进行替换的这个节点要么没有孩子,要么只有一个孩子。即左子树的最右结点或右子树的最左结点。

非递归实现

bool Erase(const k& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)//查找该结点
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else//找到了要删除的结点
				{
					if (cur->_left == nullptr)//左为空
					{
						if (cur == _root)//先要考虑cur为根的情况
						{
							_root = cur->_right;
						}
						else//判断要删除的结点和父结点的关系
						{
							if (cur == parent->_left)
							{
								parent->_left = cur->_right;
							}
							else if (cur == parent->_right)
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
						cur = nullptr;
					}
					else if (cur->_right == nullptr)//右为空
					{
						if (cur == _root)//先要考虑cur为根的情况
						{
							_root = cur->_left;
						}
						else//判断要删除的结点和父结点的关系
						{
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else if (cur == parent->_right)
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
						cur = nullptr;
					}
					else//左右都不为空
					{
						//找右子树最小的(即右子树的最左结点),该结点要么没有孩子结点,要么只有一个右孩子结点
						Node* min = cur->_right;
						Node* minParent = cur;
						while (min->_left)//可能不进循环,即cur->_right就是右子树最小的结点,所以minParent不能为空
						{
							minParent = min;
							min = min->_left;
						}
						swap(cur->_key, min->_key);//找到右子树最小结点后,交换二者的_key值
						if (minParent->_left == min)//判断min结点和minParent结点的相对位置
						{
							minParent->_left = min->_right;
						}
						else
						{
							minParent->_right = min->_right;
						}
						delete min;
						min = nullptr;
					}
					return true;

				}


			}
			//没有找到该结点
			return false;
		}

递归实现

bool EraseR(const k& key)
		{
			return _EraseR(_root, key);
		}
bool _EraseR(Node*& root, const k& key)
		{

			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < key)//先查找该结点的位置
			{
				_EraseR(root->_right, key);
			}
			else if (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)
					{
						min = min->_left;
					}
					swap(root->_key, min->_key);
					return _EraseR(root->_right, key);//交换完成以后,要删除该结点,递归调用删除函数,在删除结点的右子树中进行删除该结点
				}
				delete del;

				return true;
			}
		}

再来看看引用在递归删除时候的妙用
假设删除的结点为14
在这里插入图片描述

当找到14的时候,此时的root也就是10->_right的别名,因为14的右结点为空,所以root=root->_left也就是10->_right=14->_left ,之后再删除14这个结点,便可完成删除操作

在这里插入图片描述

2.4 二叉搜索树的默认成员函数

2.4.1 拷贝构造

	BSTree(const BSTree<k>& t)
		{
			_root = _copyNode(t._root);
		}
	Node* _copyNode(Node* root)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			Node* copy = new Node(root->_key);//先拷贝构造出根节点
			copy->_left = _copyNode(root->_left);//递归拷贝构造左子树
			copy->_right = _copyNode(root->_right);//递归拷贝构造右子树
			return copy;
		}

2.4.2 析构函数


		~BSTree()
		{
			_destroy(_root);
		}
		void _destroy(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_destroy(root->_left);//递归析构左子树
			_destroy(root->_right);//递归析构右子树
			delete root;//删除根节点
			root = nullptr;
		}
		

2.4.3 赋值重载

BSTree<k>& operator==(const BSTree<k> t)
		{
			swap(this->_root, t._root);//简化版的现代写法
			return *this;
		}

3 二叉搜索树的应用

3.1 k模型

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

3.2 kv模型

KV模型:每一个关键码key,都有与之对应的值value,即<Key, value>的键值对。该种方
式在现实生活中非常常见:
①英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<english, chinese>就构成一种键值对;
②再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。

改造二叉搜索树为kv结构,即给结点增加一个数据域value,但是在查找的时候还是根据key值去查找

	template <class k, class v>
			struct BSTreeNode
			{
				BSTreeNode<k, v>* _left;
				BSTreeNode<k, v>* _right;
				k _key;
				v _value;
				BSTreeNode(const k& key, const v& value)
					: _left(nullptr)
					, _right(nullptr)
					, _key(key)
					, _value(value)
				{

				}

			};
			template <class k, class v>
			class BSTree
			{
				typedef BSTreeNode<k, v> Node;
			public:
				bool Insert(const k& key, const v& value)
				{
					if (_root == nullptr)
					{
						_root = new Node(key, value);
						return true;
					}
					Node* cur = _root;
					Node* parent = nullptr;
					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, value);
					if (cur->_key < parent->_key)
					{
						parent->_left = cur;
					}
					else
					{
						parent->_right = cur;
					}
					return true;
				}
				Node* find(const k& key)
				{
					if (_root == nullptr)
					{
						return nullptr;
					}
					Node* cur = _root;
					while (cur)
					{
						if (cur->_key < key)
						{
							cur = cur->_right;
						}
						else if (cur->_key > key)
						{

							cur = cur->_left;
						}
						else
						{
							return cur;
						}
					}
					return nullptr;
				}
				bool Erase(const k& key)
				{
					
				}
				void InOrder()
				{
					_InOrder(_root);
					cout << endl;
				}


			private:


				void _InOrder(Node* root)
				{
					if (root == nullptr)
					{
						return;
					}
					_InOrder(root->_left);
					cout << root->_key << ":" << root->_value << " ";
					_InOrder(root->_right);
					//递归需要传递参数,但是_root为私有变量,在类外面不能访问,所以套了两层函数
				}


				Node* _root = nullptr;


			};

简单的中英互译

void test1()
			{
				BSTree<string, string>t;
				t.Insert("apple", "苹果");
				t.Insert("banana", "香蕉");
				t.Insert("left", "左边");
				t.Insert("right", "右边");
				string str;
				while (cin >> str)
				{
					BSTreeNode<string,string>* ret = t.find(str);
					if (ret)
					{
						cout << "对应的中文:" << ret->_value << " ";
						cout << endl;
					
					}
					else
					{
						cout << "对应的中文:" << "没有对应的中文" << " ";
						cout << endl;
					}

				}
			}

统计次数

void test2()
			{
				string arr[] = { "苹果","香蕉","苹果","橘子","香蕉","苹果","香蕉" };
				BSTree<string, int>countTree;
				for (auto& str : arr)
				{
					BSTreeNode<string, int>* ret = countTree.find(str);
					if (ret)
					{
						ret->_value++;
					}
					else
					{
						countTree.Insert(str, 1);
					}
				}
				countTree.InOrder();

			}

4 二叉搜索树的性能分析

插入和删除操作都必须先查找,所以查找效率代表了二叉搜索树中各个操作的性能
对于二叉搜索树的查找,走的就是从根节点到要查找的结点的路径,其比较次数为给定结点在二叉搜索树中的层数,最少可能为1次,即根结点就是要找的结点,最坏情况下为高度次。
所以二叉搜索树的查找性能取决于二叉搜索树的形状

理想形状:二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为O(log2n)
在这里插入图片描述
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为O(N)
在这里插入图片描述
当退化为单支的时候,二叉搜索树的性能就失去了,如何解决呢?且听下回分析

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

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

相关文章

【字典转模型 Objective-C语言】

一、点按钮,弹出的这个效果,这实际上是个Label, 这实际上是一个Label,点按钮弹出的这个效果, 设置一个Label的背景色、前景色、透明度、等等, 让它加进来,然后通过动画让它隐藏掉, 这就是,这个效果的实现思路, 咱们这个效果,先稍微往后放一放, 这个并不是重点…

匈牙利算法学习笔记

匈牙利算法学习笔记1. 前言1.1 二分图1.2 二分图匹配2. 匈牙利算法(Hungarian Algorithm)2.1 基础概念2.2 实现步骤参考链接&#xff1a;1. 14-4&#xff1a;匈牙利算法 Hungarian Algorithm1. 前言 1.1 二分图 二分图通常针对无向图问题。假设G(V,E)G(V,E)G(V,E)是一个无向图…

Linux搭建gitlab服务器

第一步&#xff1a;切换到root用户 sudo root或者 sudo -i第二步&#xff1a;执行以下命令 yum install curl openssh-server openssh-clients postfix cronie -y​systemctl start postfix.servicechkconfig postfix onlokkit -s http -s ssh第三步&#xff1a;添加Gitlab&…

如何理解 Python 的赋值逻辑

摘要&#xff1a; 如果你学过 C 语言&#xff0c;那么当你初见 Python 时可能会觉得 Python 的赋值方式略有诡异&#xff1a;好像差不多&#xff0c;但又好像哪里有点不太对劲。 本文比较并解释了这种赋值逻辑上的差异。回答了为什么需要这种赋值逻辑以及如何使用这种赋值逻辑…

Acer新蜂鸟Swift3电脑开机总是蓝屏错误怎么办?

Acer新蜂鸟Swift3电脑开机总是蓝屏错误怎么办&#xff1f;有用户使用的Acer新蜂鸟Swift3电脑一开机的时候&#xff0c;没过几秒电脑桌面就变成了蓝屏的了&#xff0c;通过强制重启之后依然会重复这个问题&#xff0c;那么这个问题要怎么去进行解决了&#xff0c;今天将你怎么重…

Android 反序列化漏洞攻防史话

Java 在历史上出现过许多反序列化的漏洞&#xff0c;但大部分出自 J2EE 的组件。即便是 FastJSON 这种漏洞&#xff0c;似乎也很少看到在 Android 中被实际的触发和利用。本文即为对历史上曾出现过的 Android Java 反序列化漏洞的分析和研究记录。 序列化和反序列化是指将内存数…

k8s-kubectl命令

文章目录一、kubectl 基本命令1、陈述式资源管理方法:2、声明式资源管理办法二、基本信息查看三、项目的生命周期创建kubectl run命令四、金丝雀发布(Canary Release)——陈述式管理方法五、声明式管理方法kubectl create 和 kubectl apply区别一、kubectl 基本命令 1、陈述式…

交叉验证 | 机器学习

1、交叉验证 1.1概念 交叉验证的核心思想&#xff1a;对数据集进行多次划分&#xff0c;对多次评估的结果取平均&#xff0c;从而消除单次划分时数据划分得不平衡而造成的不良影响。因为这种不良影响在小规模数据集上更容易出现&#xff0c;所以交叉验证方法在小规模数据集上更…

Profinet转ModbusTCP网关连接昆仑通态触摸屏配置案例

本案例是模拟将Modbus TCP 设备数据接入到西门子PROFINET 网络中。 使用设备为西门子 S7-1500 型 PLC, Profinet转ModbusTCP网关。MODBUS 从站昆仑通态触摸屏。 配置方法&#xff1a; 打开博图&#xff0c;新建项目并添加站点。 添加1513PLC。 设置好IP并处于联网状态 导入Pr…

【前端】小程序开发入门:安装开发工具、目录结构与项目配置

文章目录前期准备目录结构app.jsonpageswindow其他前期准备 开发小程序要先申请一个对应的AppID&#xff1a;微信小程序 (qq.com) 微信官方小程序开发文档&#xff1a;微信开放文档 (qq.com) 然后安装一个小程序开发工具&#xff1a; 选择稳定版&#xff1a; 安装后打开&…

组合由于继承

目录 前言&#xff1a; 1.什么是继承&#xff1f; 2.继承的劣势、问题&#xff1f; 3.组合相比继承有哪些优势&#xff1f; 4、如何判断该用组合还是继承&#xff1f; 参考资料 前言&#xff1a; 我们在平时日常开发设计的过程中&#xff0c;经常会有人提到一条经典的设…

音视频基础之封装格式与音视频同步

封装格式的概念 封装格式(也叫容器&#xff09;就是将已经编码压缩好的视频流、音频流及字幕按照一定的方案放到一个文件中&#xff0c;便于播放软件播放。 一般来说&#xff0c;视频文件的后缀名就是它的封装格式。 封装的格式不一样&#xff0c;后缀名也就不一样。 比如&a…

MMPBSA结合自由能计算原理

MMPBSA结合自由能计算原理 计算结合自由能的方法有很多&#xff0c;例如&#xff0c;热力学积分&#xff08;Thermodynamic Integration&#xff0c;TI&#xff09;、自由能微扰&#xff08;Free Energy Perturbation&#xff0c;FEP&#xff09;、MM/PB(GB)SA、线性相互作用能…

安科瑞消防应急照明和疏散指示系统在城市隧道的应用分析

【摘要】&#xff1a;随着城市的发展&#xff0c;交通量越来越大&#xff0c;交通状况越来越复杂&#xff0c;城市隧道的修建也随之变多。当隧道照明正常时&#xff0c;隧道内路面有足够的照度&#xff0c;隧道中快速行驶的汽车&#xff0c;大部分司机不用打开车灯或只需打开车…

Swift 周报 第二十三期

前言 本期是 Swift 编辑组自主整理周报的第十四期&#xff0c;每个模块已初步成型。各位读者如果有好的提议&#xff0c;欢迎在文末留言。 欢迎投稿或推荐内容。目前计划每两周周一发布&#xff0c;欢迎志同道合的朋友一起加入周报整理。 勇敢是即便知道好结局不会每每降临在…

项目中异常信息的统一处理以及JSR03校验

在项目中&#xff0c;我们经常会对前端传过来的数据判断是否有一些错误&#xff0c;比如&#xff1a;id是否为空&#xff0c;传过来的名称是否合格&#xff0c;如果不符合我们通常会抛出异常&#xff0c;那么小的项目可能每次抛出异常也不是很麻烦&#xff0c;但是对于一个大型…

详解HashMap

目录 1.hash code 2.数据结构 3.初始化 4.存取 4.1.put 4.2.get 5.迭代 6.扩容 7.JDK1.7版本存在的问题 7.1.性能跌落 7.2.循环链表 8.散列运算 9.扰动函数 1.hash code hash code是使用hash函数运算得到的一个值&#xff0c;是对象的身份证号码&#xff0c;用于…

将项目从 SVN 迁移到 GIT

场景 项目开发中&#xff0c;项目原本是用的SVN&#xff0c;已经用了一年了&#xff0c;现在公司要抛弃SVN用Git&#xff0c;要求把SVN的代码直接搬过去Git&#xff0c;并保留之前的历史提交记录。 操作步骤 找到已经被svn管理的项目的根目录 WinFarm&#xff0c;右键 Git Ba…

手把手教你将微信小程序放到git上

背景 首先&#xff0c;要创建一个自己的git仓库&#xff0c;这里默认大家都能够自己创建了git仓库了。如果不会创建仓库的话&#xff0c;百度一下&#xff0c;很容易就能够创建了&#xff01;&#xff08;后续&#xff0c;如有不知道在哪里&#xff0c;怎么创建仓库的话&#…

群晖-第4章-Docker安装redis

群晖-第4章-Docker安装redis 本章介绍群晖docker安装redis的方法。如果你需要外网访问&#xff0c;可以参考我的群晖第1章。 参考 群晖使用 docker部署 Redis - 编程之家 Redis设置密码_惜惜然的博客-CSDN博客 在本地新建一个文本文件&#xff0c;命名为redis.conf&#xff…