【C++、数据结构】二叉搜索树 模拟实现

news2024/11/20 2:41:17

文章目录

  • 📖 前言
  • 1. 二叉搜索树
  • 2. 二叉搜索树的模拟实现
    • 2.1 结点的声明
    • 2.2 基本的几个成员函数
      • 非递归版本
        • (1)查找:
        • (2)插入:
        • (4)删除:(重点)
      • 递归版本
        • (1)查找:
        • (2)插入:(重点)
        • (3)删除:

📖 前言

  • 从本章起,我们开始深入学习二叉树,学习其更高端的应用,然后将学习STL中比较重要的两个容器set和map。
  • 学习二叉搜索树也是为以后学习和实现set和map做铺垫。

1. 二叉搜索树

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

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

在这里插入图片描述

也就是说一棵二叉搜索树的任一个根节点,它的左子树所有节点的值都是小于根节点的值的,它的右子树所有结点的值都是大于根节点的值的。

二叉搜索树查找的时间复杂度:

  • 根据二叉搜索树的性质
  • 我们大多数人认为其搜索的一个值的速度是为树的高度次
  • 树的高度次的话,很多人就会认为是log2N次
  • 但是事实并不是,正确得查找时间复杂度是〇(N)

只有当是满二叉树或者是完全二叉树时间复杂度才是〇(logN)!!

解释:

当出现单边树的情况时,就是〇(N)的情况。
在这里插入图片描述

此时树的高速就是结点的个数,同时如果数据量过大,而且是递归查找的话,很有可能会有爆栈的风险!!
在以后我们会学习平衡二叉树,就是为了解决上述情况的问题。


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 基本的几个成员函数

template<class K>

class BSTree
{                
	typedef BSTreeNode<K> Node;
private:
	//没有参数是不能递归的
	void DestroyTree(Node* root)
	{
		if (root == nullptr)
			return;

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

	Node* CopyTree(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copyNode = new Node(root->_key);
		copyNode->_left = CopyTree(root->_left);
		copyNode->_right = CopyTree(root->_right);
		
		return copyNode;
	}
public:
	//强制编译器自己生成构造函数 -- C++11
	BSTree() = default;

	/*BSTree()
		:_root(nullptr)
	{}*/

	//前序遍历递归拷贝
	BSTree(const BSTree<K>& t)
	{
		_root = CopyTree(t._root);
	}

	//t1 = t2; -- 任何赋值重载都可以用现代写法
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

	~BSTree()
	{
		DestroyTree(_root);
		_root = nullptr;
	}

构造函数:

  • 这里我们可以采用传统的方法
  • 直接初始化成员变量
  • 也可以用C++11的语法default
  • 强制编译器自己生成构造函数

拷贝构造:

  • 这里我们用了递归的方式进行拷贝
  • 采用根 - 左 - 右 的前序遍历的递归方式对整个二叉树拷贝
  • 最后将跟结点返回

析构函数:

  • 析构函数我们这里也是采用递归的方式进行一个一个结点析构
  • 同样的我们再嵌套一个子函数
  • 也是采用类似前序遍历的方法将整个二叉树释放掉

采用递归方式的缺点就是如果数的结点个数足够多的时候,就会有爆栈的风险!!


非递归版本

(1)查找:

在二叉搜索树中找某个值:

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

根据二叉搜索树的性质,查找规则很简单:

  • 从根节点开始找起
  • 要找的值如果比根节点的值大,则在根节点的右子树中找
  • 要找的值如果比根节点的值小,则在根节点的左子树中找
  • 再在子树中重复上述操作,最终找到要找的值

所以再没有平衡二叉搜索树的情况下,查找的时间复杂度为〇(N)

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

		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

根据搜索二叉树的特性,插入规则如下:

  • 上述过程也是一个查找的过程
  • 根据要插入值的大小,定位其在树中合适的位置
  • 找到合适位置之后,直接插入即可

(4)删除:(重点)

二叉搜索树的删除,是一件非常麻烦的事情

  • 要删除结点,就要理清楚父子节点的链接关系(一不留神就把关系理乱了)
  • 要求删过之后的二叉树还是一棵搜索二叉树(相当困难,普通直接删除做不到)

分析问题:

  • (1)当没有孩子或者只有一个孩子时
  • 可以直接删除,孩子托管给父亲(托孤)

在这里插入图片描述
以删除14这个结点为例:

  • 该结点比10这个结点(父结点)大,在其右子树
  • 那么该右子树的所有的值都比10这个结点大
  • 所以要链接在10这个结点的右边

以删除7这个结点为例:

  • 该结点比6这个结点(父结点)大,在其右子树
  • 因为7这个结点没有孩子
  • 直接删除,将父节点(6结点)的右指向空
  • (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
		{
			//找到了就分三种情况

			//该结点有一个孩子 -- 左为空 or 右为空(托孤)
			//该结点有两个孩子 -- 替换法
			
			//第一种情况:该结点有一个孩子且该结点的左为空
			if (cur->_left == nullptr)
			{
				//当删除的是根节点的时候
				//if(parent == nullptr)
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}

				delete cur;
			}
			//第二种情况:该结点有一个孩子且该结点的右为空
			else if (cur->_right == nullptr)
			{
				if (cur == parent)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}

				delete cur; 
			}
			//两个孩子都不为空(替换法删除)
			else 
			{
				//我们这里统一找右树最左结点(最小)
				//右子树的最小结点替代

				//minParent一开始不能给空,因为右子树的跟一开始就可能是minRight
				//Node* minParent = nullptr; -- 循环直接不能进去

				Node* minParent = cur;

				//从右子树的根开始
				Node* minRight = cur->_right;
				
				//找最左结点(最小)
				while (minRight->_left)
				{
					minParent = minRight;   
					minRight = minRight->_left;
				}

				//交换
				swap(minRight->_key, cur->_key);
				//**return Erase(key); -- 这是错的,因为这里已经不符合搜索树的规则了
				//递归过程中找不到想要想要删除的数(交换到后头的数)
				
				//直接赋值
				//cur->_key = minRight->_key;     
				                                                 
				//删除

				//找到最小结点,此结点一定是该结点父亲结点的左孩子
				//此结点一定没有左孩子(一定是左为空),有可能有右孩子,也可能没有右孩子
				//此时只需要将父亲的左指向该结点的右孩子即可
				//删除完成

				if (minParent->_left == minRight)
				{
					minParent->_left = minRight->_right;
				}
				else if (minParent->_right == minRight)
				{
					minParent->_right = minRight->_right;
				}

				delete  minRight;
			}

			return true;
		}
	}

	return false;
}

代码解释,如下图:

  • 第一种情况:该结点有一个孩子且该结点的左为空

在这里插入图片描述

  • 第二种情况:该结点有一个孩子且该结点的右为空

在这里插入图片描述

  • 第三种情况:两个孩子都不为空(替换法删除)

在这里插入图片描述
左子树的最大值节点,或者右子树的最小值节点

  • 根据二叉搜索树的特性
  • 任何一个结点的左子树所有结点的值都比根小
  • 任何一个结点的右子树所有结点的值都比根大

找要删除结点的左子树的最大值节点:

  • 那么找左子树的最右边结点
  • 那么该结点一定比根结点的右子树中所有的值都小
  • 但是该结点在根结点的左子树中是最大的
  • 让其和根结点的值交换
  • 将被交换的结点删除后,整棵树仍保持是一棵二叉搜索树

同理,找右子树的最小值节点也是一样的道理


递归版本

递归版本理解起来就相对与非递归版本更好理解了,直接看代码

(1)查找:

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)
	{
		return _FindR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _FindR(root->_left, key);
	}
	else
	{
		return true;
	}
}

逐层递归查找即可…

(2)插入:(重点)

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

bool _InsertR(Node*& root, const K& key)
{
	//没有父指针,胜似父指针
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}

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

该如何链接上树呢?

  • 可以在递归的参数中多一个父亲结点,每次递归都更新一下Parent,然后再带到下一层递归
  • 显然这样在学过C++之后就麻烦了

用了一个指针的引用就解决了问题

  • 因为root的值此时是空,但是root同时是这个结点里的_left这个指针的别名
  • 相当于当前结点的父节点的左指针的别名
  • 意味着此时再去给root赋值就是去给该结点父亲结点的_left赋值
  • 那么此时就链接起来了

(3)删除:

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)
	{
		return _EraseR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _EraseR(root->_left, key);
	}
	else
	{
		Node* del = root;
		
		//root是要删除结点的左结点/右结点的别名

		if (root->_left == nullptr)
		{
			root = root->_right;
		}
		else if (root->_right == nullptr)
		{
			root = root->_left;
		}
		else
		{
			Node* minRight = root->_right;
			while (minRight->_left)
			{
				minRight = minRight->_left;
			}

			swap(root->_key, minRight->_key);

			return _EraseR(root->_right, key);
			//转换成在root->_right(右子树)中去删除key
			//这里删除这个key一定会走左为空的场景(找最小)
		}

		delete del;
		return true;
	}
}

相等时就开始删除了(递归只是用来查找要删除的数的位置)

  • root是要删除结点的左结点 / 右结点的别名

分三种情况删除:

  1. 要删除的结点左为空
  2. 要删除的结点右为空
  3. 要删除的结点左右都为空(替换法)

总的来说递归版本比非递归版本更容易理解,删除过程参考非递归删除过程……(有异曲同工之妙)

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

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

相关文章

你阳了吗?可以接种最新加强针疫苗了!

今天不聊技术&#xff0c;唠唠嗑。刚才得知室友全部阳了&#xff0c;不知道我还能撑到多会&#x1f926;‍♂️当前时间&#xff0c;距离全国抗疫政策转向已有一个月时间&#xff0c;大家都不可避免的直面新冠病毒。奥密克戎的传播能力果然很强&#xff0c;短短半个月时间从周围…

Docker- 7.2、跨主机网络-overlay

Docker提供了overlay driver&#xff0c;使用户可以创建基于VxLan的overlay网络。VxLAN可将二层数据封装到UDP进行传输&#xff0c;VxLAN提供与VLAN相同的以太网二层服务&#xff0c;但拥有更强的扩展性和灵活性。Docker overlay网络需要一个key-value数据库用于保存网络状态信…

嵌入式C语言设计模式 --- 工厂方法模式

1 - 什么是工厂方法模式? 前一篇文章讲述了在使用简单工厂模式的时候,有一个明显的缺陷,就是我们添加一款新的LCD控制器的时候,需要修改工厂类的构造函数(因为只有一家工厂),在switch-case里面新增一个条件项,违背了面向对象设计的“开闭原则”。 为了解决这个问题,可…

让自己成为一个创作者

写博客三年多&#xff0c;创作不易&#xff0c;以下是自己的一点感悟&#xff0c;大家共勉&#xff01; “千万不要把“创作”当成是一件很大的事情&#xff0c;那只是思维方式和行为的转变而已&#xff01;” 01 — 我有个爱好--打游戏&#xff0c; 特别喜欢宫崎英高的魂系…

【JavaSE成神之路】数组结构的概念与应用技术

哈喽&#xff0c;我是兔哥呀&#xff0c;今天就让我们继续这个JavaSE成神之路&#xff01; 这一节啊&#xff0c;咱们要学习的内容是数组的概念与应用技术。 乍一听还挺唬人的有没有&#xff0c;数组这个东西呢并不是什么新的知识啦&#xff0c;之前的章节中我们已经学习过数组…

Linux虚拟机安装Hive(mysql安装)

Hive安装第01节 Hive安装部署1. 安装前准备2. 安装MySQL3. Hive安装配置4. metastore服务第02节. Hive客户端的使用1. 客户端介绍2. HiveServer2服务3. Hive CLI的使用4. beeline客户端5. DataGrip可视化客户端第02节 Hive简单使用1. 基本操作2. 查看YARN及HDFS3. 总结第01节 H…

Java开发 - 双链表其实不可怕

前言 说起链表&#xff0c;那还是当初上学的时候学习的&#xff0c;印象里就觉得像锁链一样一环扣一环&#xff0c;后来工作后就几乎没实际接触过链表&#xff0c;每当遇到链表&#xff0c;总是不知道该怎么讲&#xff0c;因为对链表的本质一无所知。也是在学习了Java后&#…

即将2023年了,我好想念那些2022年离职的兄弟

☆ 2022年终于即将要过去了&#xff0c;从年初开始&#xff0c;就有小伙伴不断的离开&#xff0c;10% 15% 20%&#xff0c;陆陆续续的。被裁的同学还拿着赔偿走了&#xff0c;不管未来怎么样吧&#xff0c;当下总算一身轻了。 ☆ 留下的其实一点也不轻松&#xff0c;以下这些经…

Anchor Based和Anchor Free的相爱相杀与ATSS的诞生

前言 我们都知道按照是否出现RPN可将目标检测算法分为two-stage和one-stage&#xff0c;其中one-stage的一个主要代表便是YOLO系列&#xff0c;而根据是否存在先验锚框的定义我们也可以将其分为Anchor based和Anchor free两类&#xff0c;关于这两种也是各有优劣&#xff0c;但…

7-7 静静的推荐

天梯赛结束后&#xff0c;某企业的人力资源部希望组委会能推荐一批优秀的学生&#xff0c;这个整理推荐名单的任务就由静静姐负责。企业接受推荐的流程是这样的&#xff1a; 只考虑得分不低于 175 分的学生&#xff1b;一共接受 K 批次的推荐名单&#xff1b;同一批推荐名单上…

入职软件测试一年多,薪资正常应该有多少?

软件测试这个行业我见过工作5年依然是初级水平的功能测试&#xff0c;拿着不到1W的薪资&#xff0c;也见过1年时间达到高级自动化水平的大牛学霸&#xff01;拿着25K的薪资。 影响你薪资的有两点&#xff01; 一、你的技术水平高低直接决定你薪资的多少&#xff0c;工作时间长…

谷粒学院——Day14【首页课程和名师功能】

❤ 作者主页&#xff1a;Java技术一点通的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是Java技术一点通&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得关注、点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习&#xff0c;共同进步&#xff01;&am…

数字硬件建模SystemVerilog-组合逻辑建模(4)组合逻辑决策优先级

数字门级电路可分为两大类&#xff1a;组合逻辑和时序逻辑。锁存器是组合逻辑和时序逻辑的一个交叉点&#xff0c;在后面会作为单独的主题处理。组合逻辑描述了门级电路&#xff0c;其中逻辑块的输出直接反映到该块的输入值的组合&#xff0c;例如&#xff0c;双输入AND门的输出…

四点流程做好商机管理

企业想做好商机管理&#xff0c;仅凭员工是做不到的&#xff0c;借助CRM销售管理系统是比较明智的选择。接下来小编从客户信息管理、业务进程跟踪、设置提醒、销售漏斗等方面讲讲企业如何做好商机管理。 只有提高商机的转化率&#xff0c;企业的利润才会增长。想做好商机管理&…

MySQL 索引事务 · 讨论适合索引的数据结构 · N叉搜索树 · B+树 · 聚簇索引与非聚簇索引 · 事务的四个核心特性

一、索引1.1 概念1.2 作用1.3 讨论-如何提高查询的速度合适的数据结构&#xff1a;二叉搜索树AVL 树红黑树哈希表以上数据结构的问题&#xff1a;N 叉搜索树B 树B 树1.4 索引的使用场景1.5 索引的使用1.6 聚簇索引与非聚簇索引聚簇索引非聚簇索引1.7 索引 - 小结二、事务2.1 为…

小程序-会议OA项目-首页

目录 一&#xff0c;flex弹性布局 什么是flex布局&#xff1f; flex属性 flex-direction属性 学习地址&#xff1a; OA项目搭建以及flex布局演示 二&#xff0c;轮播图--组件的使用 1.先去官方文档中查看轮播图组件如何使 2.在开发工具中查看演示及修改代码 3. 使用mock…

异步通信技术AJAX | 基于JSON、XML的数据交换

目录 一&#xff1a;快速搞定AJAX&#xff08;第二篇&#xff09; 1、JS中如何创建和访问JSON对象 2、基于JSON的数据交换&#xff08;重点&#xff09; 3、基于XML的数据交换&#xff08;了解&#xff09; 一&#xff1a;快速搞定AJAX&#xff08;第二篇&#xff09; 1、…

-source1.5中不支持diamond运算符解决办法

写了几年代码了&#xff0c; 回到最初了遇到了bug了&#xff0c;没有仔细思考&#xff0c;以为很容易&#xff0c;起始走到了误区&#xff0c;有种打了一辈子鹰&#xff0c;最后被麻雀啄了眼 de 感觉 首先来看一下我们的错误信息&#xff0c;如下&#xff1a; [ERROR] Failed…

相机标定笔记(2) -- 标定实践

标定板 为什么需要标定板? 相机标定的第一篇笔记中提到了相机标定所使用的模型&#xff0c;标定算法中我们需要一些可靠的样本点&#xff0c;这些样本点由世界坐标系中的3D点和其在图像上的2D像点组成。用这些2D和3D点对来求解标定参数。为了构建更高精度的3D和2D点&#xff0…

对于系统架构来说,要么进化,要么死亡

在亚马逊云科技年度re:Invent&#xff0c;亚马逊首席技术官Dr. Werner Vogels的主题演讲历来涵盖大量科学和技术领域&#xff0c;今年的演讲也不例外&#xff0c;座无虚席。现在&#xff0c;亚马逊云科技 2022 re:Invent 中国区 recap 正式也开始了&#xff0c;欢迎大家积极参与…