深入浅出C++ ——二叉搜索树

news2024/11/28 4:43:39

文章目录

  • 一、二叉搜索树概念
  • 二、二叉搜索树操作
    • 1. 二叉搜索树的查找
    • 2. 二叉搜索树的插入
    • 3. 二叉搜索树的删除
  • 三、二叉搜索树的实现
  • 四、二叉搜索树的性能分析

一、二叉搜索树概念

  二叉搜索树又称二叉排序树/二次查找树,它是一棵空树或者是每颗子树都具有以下性质的二叉树

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树

在这里插入图片描述

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

二、二叉搜索树操作

1. 二叉搜索树的查找

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

2. 二叉搜索树的插入

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

3. 二叉搜索树的删除

  1. 左为空,父亲指向他的右。 也就是说左子节点为空,让它父节点指向该节点的右子节点,再直接删除该节点。
  2. 右为空,父亲指向他的左。 也就是说右子节点为空,让它父节点指向该节点的左子节点,再直接删除该节点。
  3. 左右子节点都不为空时,使用替换法删除

  在第一节的例子中,删除1、4、7、13、14、10节点属于前两种情况,删除3、8、10、6节点属于第三种情况。


替换法删除

  找到左子树的最右节点或者右子树的最左节点,替换该节点赋值给删除节点,直接删除替换节点,因为替换节点没子节点或者只有一个子节点,再归类到前两种情况。


修改

   K模型的搜索二叉树不支持修改,增删查的时间复杂度为O(h),h是树的高度,最坏的情况是h为N。


三、二叉搜索树的实现

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。 该种方式在现实生活中非常常见,例如:刷卡进楼。
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> //key
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* parent = nullptr;	//这里父节点指针给nullptr不会报错,因为考虑极端情况只有一个节点,那也会进入下面的循环中,执行parent = 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
			parent->_left = cur;
		return true;
	}

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

	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						//找到了
			{
				//开始删除
				//1、左为空 ,父亲指向它的右 ,把另一个孩子托管给父亲
				//2、右为空 ,父亲指向它的左
				//3、左右都不为空
				if (cur->_left == nullptr) // 左为空 ,父亲指向它的右
				{
					if (cur == _root)//考虑极端情况删除的是_root根节点的情况
					{
						_root = cur->_right; //左为空,更新根
					}
					else
					{
						if (cur == parent->_left) //判断该节点是父亲的左还是右
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //右为空 ,父亲指向它的左
				{
					if (cur == _root)//考虑极端情况删除的是_root根节点的情况
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
					delete cur;
					cur = nullptr;
				}
				else//左右都不为空
				{
					//	替换法删除,替换的节点可以是左树的最大节点或者是右树的最小节点
					//	这里采用 找到右树的最小节点进行替换
					Node* minParent = cur; //	考虑极端情况,要删除的是根节点,所以minparent不可以为空
					Node* min = cur->_right;
					while (min->_left)
					{
						minParent = min;
						min = min->_left;
					}
					swap(cur->_key, min->_key);
					if (minParent->_left == min)  //判断minParent和min的关系
					{
						minParent->_left = min->_right;
					}
					else
						minParent->_right = min->_right;

					delete min;
					min = nullptr;
				}
				return true;
			}
		}
		return false;
	}

	void InOrder()//中序遍历打印
	{
		_InOrder(_root);//套用了一层,第一次传过去了_root,后面递归就传他的子树
		cout << endl;
	}

	bool FindR(const K& key)//递归版本的查找
	{
		return _Find(_root, key);
	}

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

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
	
	BSTree() = default;//C++11 强制编译器生成默认的构造

	~BSTree()
	{
		_Destsory(_root);
	}

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	// 传值传参
	BSTree<K>& operator = (BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

private:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* copyRoot = new Node(root->_key);
		copyRoot->_left = _Copy(root->_left);
		copyRoot->_right = _Copy(root->_right);
		return copyRoot;
	}
	void _Destsory(Node*& root)
	{
		if (root == nullptr)
			return;
		_Destsory(root->_left);
		_Destsory(root->_right);
		delete root;
		root = nullptr;
	}
	//_InOrder(_root)   递归调用不能写这个,不然每次传过去都是根节点,只能第一次传递根节点,用一个形参cur来接收根节点,
	void _InOrder(Node* cur) //递归必须 显示 的传递子树,而在外面调用的时候要传根节点_root,但是拿不到私有的_root。可以写一个getroot函数去拿到_root,或者像这样
	{
		if (cur == nullptr)
			return;
		_InOrder(cur->_left);
		cout << cur->_key << " ";
		_InOrder(cur->_right);
	}

	bool _Find(Node* cur, const K& key)
	{
		if (cur == nullptr)
			return false;

		if (cur->_key < key)
			return _Find(cur->_right, key);

		else if (cur->_key > key)
			return _Find(cur->left, key);

		else
			return true; //return true 按顺序依次从开辟的栈帧返回到最外层
	}

	bool _InsertR(Node*& cur, const K& key)
	{
		if (cur == nullptr)
		{
			cur = new Node(key); //cur是一个局部变量,所以要用引用
			return true;
		}
		if (cur->_key < key)
			return _InsertR(cur->_right, key);

		else if (cur->_key > key)
			return	_InsertR(cur->_right, key);

		else
			return false; //天然去重

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

		if (cur->_key < key)
			return _EraseR(cur->_right, key);

		else if (cur->_key > key)
			return _EraseR(cur->_left, key);

		else  //删除
		{
			Node* del = cur;
			if (cur->_left == nullptr) //左为空
			{
				cur = cur->_right;     
			}
			else if (cur->_right == nullptr) //右为空
			{
				cur = cur->_left;
			}
			else              //左右都不为空
			{
				//找右树的最左节点
				Node* min = cur->_right;
				while (min->_left)
				{
					min = min->_left;
				}
				swap(cur->_key, min->_key);
				return _EraseR(cur->_right, key); //从这个点的右树开始删除key
			}
			delete del;
			del = nullptr;
			return true;
		}
	}
	Node* _root = nullptr;
};

应用:

int main()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto& e : a)
	{
		t.Insert(e);
	}
	//排序+去重
	t.InOrder(); 
	t.Erase(3);
	t.InOrder();
}

  1. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。 该种方式在现实生活中也非常常见,例如:统计某物品出现的次数
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* 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, value);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}

	Node* 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 cur;
			}
		}

		return nullptr;
	}

	bool Erase(const K& key)
	{
		//...
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << ":" << root->_value << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

应用:

int main()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		//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个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

  但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

  • 最优情况下,二叉搜索树为完全二叉树,或者接近完全二叉树,其平均比较次数为:log2 N
  • 最差情况下,二叉搜索树退化为单支树,或者类似单支,其平均比较次数为:N

  如果退化成单支树,二叉搜索树的性能就失去了。将二叉搜素树改进为AVL树和红黑树,不论按照什么次序插入关键码,性能都能达到最优。

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

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

相关文章

Spring Boot 框架 集成 Knife4j(内含源代码)

Spring Boot 框架 集成 Knife4j&#xff08;内含源代码&#xff09; 源代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87480176 目录Spring Boot 框架 集成 Knife4j&#xff08;内含源代码&#xff09;源代码下载链接地址&#xff1a;[htt…

Bmp图片格式介绍

Bmp图片格式介绍 介绍 BMP是英文Bitmap&#xff08;位图&#xff09;的简写&#xff0c;它是Windows操作系统中的标准图像文件格式&#xff0c;能够被多种Windows应用程序所支持。随着Windows操作系统的流行与丰富的Windows应用程序的开发&#xff0c;BMP位图格式理所当然地被…

JetPack板块—Android X解析

Android Jetpack简述 AndroidX 是Android团队用于在Jetpack中开发&#xff0c;测试&#xff0c;打包&#xff0c;发布和版本管理的开源项目。相比于原来的Android Support库,AndroidX 可以称得上是一次重大的升级改进。 和Support库一样&#xff0c;AndroidX与Android 操作系…

投出1000份简历,苦于软件测试没有项目经验,全部石沉大海,辞职5个月,我失业了......

想要找一份高薪的软件测试工作&#xff0c;简历项目必不可少&#xff08;即使是应届生&#xff0c;你也要写上实习项目&#xff09;。所以很多自学的朋友找工作时会碰到一个令人颇感绝望的拦路虎&#xff1a;个人并没有实际的项目工作经验怎么办&#xff1f; 怎么办&#xff1f…

6.1 反馈的基本概念及判断方法

一、反馈的基本概念 1、什么是反馈 反馈也称为 “回授”。在电子电路中&#xff0c;将输出量&#xff08;输出电压或输出电流&#xff09;的一部分或全部通过一定的电路形式作用到输入回路&#xff0c;用来影响其输入量&#xff08;放大电路的输入电压或输入电流&#xff09;…

【软件测试】接口自动化测试你真的会做吗?资深测试工程师的总结......

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

离线环境拷贝迁移 conda envs 环境(蛮力方法,3行命令)

前言 最近要使用 GPU 服务器做实验&#xff0c;可惜的是&#xff0c;有网络连接的服务器显卡旧&#xff0c;算力不够&#xff1b;显卡较新的机器没有联网。于是有需求将旧机器上配置好的 conda 环境迁移至新机器。网上给的默认方法生成 yaml 文件迁移等 需要联网&#xff0c;只…

孪生生产线:法兰工厂数据驱动的颠覆性创新

2018 年&#xff0c;世界经济论坛(WEF)携手麦肯锡公司共同倡议并正式启动了全球“灯塔工厂网络项目”(Lighthouse Network)&#xff0c;共同遴选率先应用工业革命 4.0 技术实现企业盈利和持续发展的创新者与示范者。这就使得工厂系统需要对各流水线及生产运行成本方面进行多角度…

Neuron Selectivity Transfer 原理与代码解析

paper&#xff1a;Like What You Like: Knowledge Distill via Neuron Selectivity Transfercode&#xff1a;https://github.com/megvii-research/mdistiller/blob/master/mdistiller/distillers/NST.py本文的创新点本文探索了一种新型的知识 - 神经元的选择性知识&#xff0c…

整型在内存中的存储(详细剖析大小端)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是整型在内存中的存储噢&#xff0c;现在&#xff0c;就让我们进入整型在内存中的存储的世界吧 数据类型详细介绍 整型在内存中的存储&#xff1a;原码、反码、补码 大小端字节序介绍及判断 数据类型介绍 前面我们已经学…

【扫盲】数字货币科普对于完全不了解啥叫比特币的小伙伴需要的聊天谈资

很多人并不清楚&#xff0c;我们时常听说的比特币&#xff0c;以太坊币&#xff0c;等等这些东西到底是一场骗局还是一场货币革命&#xff1f; 下面就围绕这数字货币的历史以及一些应用场景开始分析这个问题。 一、 开端 一切从2008年中本聪&#xff08;Satoshi Nakamoto&…

shiro反序列化漏洞与无依赖CB链分析

环境搭建 git clone https://github.com/apache/shiro cd shiro git checkout shiro-root-1.2.4将 shiro/samples/web/pom.xml 中的jstl依赖改为1.2: <dependency><groupId>javax.servlet</groupId><artifactId>jstl</artifactId><version&g…

【数据结构与算法】3.(单向、无向、带权)图,广度、深度优先搜索,贪心算法

文章目录1.图简介2.图的存储方式2.1.邻接矩阵存储方法2.2.邻接表存储方法3.有向、无向图和查询算法3.1.数据结构3.2.广度优先算法BFS3.3.深度优先算法DFS3.3.1.DFS查询单条路径3.3.2.DFS查询所有路径4.带权图和贪心算法4.1.贪心算法4.2.基于带权无向图使用贪心算法查询最优路径…

混合精度训练,FP16加速训练,降低内存消耗

计算机中的浮点数表示&#xff0c;按照IEEE754可以分为三种&#xff0c;分别是半精度浮点数、单精度浮点数和双精度浮点数。三种格式的浮点数因占用的存储位数不同&#xff0c;能够表示的数据精度也不同。 Signed bit用于控制浮点数的正负&#xff0c;0表示正数&#xff0c;1表…

MAC地址IP地址 端口

网络结构&#xff1a; 服务器-客户机&#xff08;C/S&#xff09;Client-Server结构&#xff0c;如QQ,LOL都拥有客户端 优点&#xff1a;响应速度快&#xff0c;形式多样&#xff0c;安全新较高缺点&#xff1a;安装软件和维护&#xff0c;不能跨平台LINUX/windows/MAC浏览器-…

C语言——柔性数组

目录0. 前言1. 思维导图2. 柔性数组的特点3. 柔性数组的使用4. 柔性数组的优势5. 结语0. 前言 柔性数组是在C99标准时引入&#xff1a; 结构中的最后一个元素允许是未知大小的数组&#xff0c;这就叫柔性数组成员。 代码示例&#xff1a; typedef struct flexible_arr {int a…

leetcode 1011. Capacity To Ship Packages Within D Days(D天内运送包裹的容量)

数组的每个元素代表每个货物的重量&#xff0c;注意这个货物是有先后顺序的&#xff0c;先来的要先运输&#xff0c;所以不能改变这些元素的顺序。 要days天内把这些货物全部运输出去&#xff0c;问所需船的最小载重量。 思路&#xff1a; 数组内数字顺序不能变&#xff0c;就…

【Storm】【一】简介

介绍 1.1 简介 Storm 是 Apache 旗下免费开源的分布式实时计算框架。Storm可以轻松、可靠地处理无限数据流&#xff0c;对实时分析、在线机器学习、连续计算、分布式RPC&#xff0c;ETL等提供高效、可靠的支持。 1.2 什么是分布式计算 分布式计算&#xff0c;将一个任务分解…

云解析专家解密《狂飙》剧中京海市人民政府域名访问真相

这段时间&#xff0c;最火的国产剧当属《狂飙》无疑。尽管不久前迎来了大结局&#xff0c;但这部剧的热度依然不减&#xff0c;成为大家茶余饭后热议的话题。 出于对这部剧的喜爱&#xff0c;小编开启了二刷模式&#xff0c;在重温剧情的同时&#xff0c;对于其中的一些细节也…

Windows 10注册表损坏怎么办?

注册表是一个复杂的数据库&#xff0c;如果不进行维护&#xff0c;它就会填充损坏的和孤立的注册表项。尤其是对Windows进行升级时&#xff0c;损坏或丢失的注册表项也会不断累积&#xff0c;从而影响您的系统性能。如果您的Windows 10系统正在经历注册表损坏的问题&#xff0c…