C++--二叉搜索树初阶

news2024/11/28 19:27:23

       前言:二叉搜索树是一种常用的数据结构,支持快速的查找、插入、删除操作,C++中map和set的特性也是以二叉搜索树作为铺垫来实现的,而二叉搜索树也是一种树形结构,所以,在学习map和set之前,我们先来学习这种新的树形结构--二叉搜索树。

目录

1.二叉搜索树

二叉搜索树的功能及其实现

二叉搜索树的插入和查找

二叉搜索树的删除

查找函数递归实现

插入函数递归实现

删除函数递归实现

拷贝构造和赋值运算符重载

搜索二叉树的相关性质及其应用

二叉搜索树的查找

key-value模型


1.二叉搜索树

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

     若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

     若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

     它的左右子树也分别为二叉搜索树,同样的,我们可以发现,二叉搜索树的中序遍历是升序排序的

二叉搜索树的功能及其实现

        我们先定义基础的节点的数据结构,明显是一个二叉树类型的节点,有左右节点,运用模板知识,我们可以描述出以下的节点结构体和搜索树的类的描述:

template<class T>
struct BSTreeNdoe {
	BSTreeNdoe(const T& data = T())
		: _left(nullptr), _right(nullptr), data(data)
	{}
	BSTreeNdoe<T>* _left;
	BSTreeNdoe<T>* _right;
	T data;

};
template<class T>
class BSTree {

	typedef BSTreeNdoe<T> Node;
public:
     //成员函数

private:
	Node* root = nullptr;

};

下面的功能函数将从上面给出的结构来进行扩充功能和优化改写:

二叉搜索树的插入和查找

插入的具体过程如下

a. 树为空,则直接新增节点,赋值给root指针

b. 树不空,按二叉搜索树性质查找插入位置,插入新节点,从树的根节点依次寻找:如果待插入的值比当前节点大,那直接插入当前节点的右边,反之,插入当前节点的左边,这里注意,一般的,在二叉搜索树中不会存在相同的值,需要注意的是,为了保证新节点能够和树连接起来,需要记录好父节点

//插入操作
	bool insert(const T& key)
	{
		if (root == nullptr)//如果当前树为空,那么直接建树
		{
			root = new Node(key);
			return true;
		}
		//如果不为空,直接寻找合适的位置插入即可
		Node* cur = root;
		Node* parent = nullptr;//记录父亲节点用来连接
		while (cur)
		{
			parent = cur;
			if (cur->data > key)//比当前节点小,往当前节点左边走
				cur = cur->_left;
			else if (cur->data < key)//比当前节点大,往当前节点右边走
				cur = cur->_right;
			//一般来说,搜索二叉树不会存在相同的两个值
			else
				return false;
		}
		cur = new Node(key);
		if (parent->data > key)
			parent->_left = cur;
		else if (parent->data < key)
			parent->_right = cur;
		return true;
	}

二叉搜索树的查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。

b、最多查找高度次,走到到空,还没找到,这个值不存在。

bool find(const T& key)
	{
		Node* cur = root;
		while (cur)
		{
			if (cur->data > key)
				cur = cur->_left;
			else if (cur->data < key)
				cur = cur->_right;
			else
				return true;
		}
		return false;
	}

二叉搜索树的删除

        首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

a. 要删除的结点无孩子结点

b. 要删除的结点只有左孩子结点

c. 要删除的结点只有右孩子结点

d. 要删除的结点有左、右孩子结点

       实际上,这四种情况中,a情况可以算作b或者c情况中,因为删除叶子结点,本质上可以看做要删除的节点只有为nullptr的右孩子或者左孩子节点,我们来分别对这三种情况进行分析,

       对于替换法,我们需要注意特殊的情况,从上面的图可以看出,左子树中的最右节点,本质上就是第一次往待删除节点的左孩子节点走,之后的每一步,都开始往其右孩子方向走,右子树的最左节点是第一次往待删除节点的右孩子节点走,之后一直往其左孩子方向走,此时,我们的替换法是建立在待删除节点的左右孩子都存在的前提下,所以,左树最右节点和右树最左节点,左树和右树一定都具备,但是不一定有最右和最左节点,下面我们就以左树最右节点为例,来进行讨论分析:

下面是一颗普通的二叉树:

        因为此时待删除节点需要满足存在左右节点,所以,此时符合要求的节点只有8,3,其中8是正常的情况,因为其左子树有右孩子,通过8到3之后可以直接找到7,所以8可以直接和7进行交换,因为此时已经遍历到了左子树的最右节点,也就是说,该节点不可能再存在右孩子,但是不能保证没有左孩子,比如说没有7这个节点,要删除8,8就只能和6交换,此时我们需要将4,也就是被交换节点的左子树放在3的右孩子上,这样就能达到删除8的目的;

     此时我们再来看3这个节点,这个节点特殊在其只有一个左孩子节点,并且左孩子节点没有了右子树,导致我们找左树最右节点的时候,只能找到第一步左树的位置而被迫停下来,此时,这个左树就只能当做左树的最右节点来看待,只是此时我们在将1和3交换之后,操作上要发生变化,因为此时没有了右子树,所以被交换后,3成了左子树,也就是说需要删除的是1的左子树而不是像上面的情况一样直接将被交换节点的父节点的右树直接被删除节点的左树。

这里我们给出代码和测试二叉树,方便我们下来模拟运行和理解:

//删除节点
	bool Erase(const T& key)
	{
		Node* parent = nullptr;
		Node* cur = root;
		while (cur)
		{
			if (cur->data < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->data > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				//找到了,开始删除
				//只有右孩子
				if (cur->_left == nullptr)
				{
					//此时父节点有三种情况

					//如果要删除的是根节点,需要特殊处理
					if (cur == root)
						root =cur->_right;
					else if (cur == parent->_left)
						parent->_left = cur->_right;
					else if (cur == parent->_right)
						parent->_right = cur->_right;
					delete cur;
				}
				//只有左孩子
				else if (cur->_right == nullptr)
				{
					//此时父节点有三种情况

					//如果要删除的是根节点
					if (cur == root)
						root = cur->_left;
					else if (cur == parent->_left)
						parent->_left = cur->_left;
					else if (cur == parent->_right)
						parent->_right = cur->_left;
					delete cur;
				}
				//有左右两个孩子节点
				else
				{
					//法1:假设默认选择左子树的最大节点
					//Node* pright = cur->_left;//先找到左子树,此时我们能确保待删除节点是有两个孩子的,所以p一定不为空
					//Node* pp = cur;//初始时父节点等于待删除节点
					//while (pright->_right)
					//{
					//	pp = pright;//更新父节点
					//	pright = pright->_right;
					//}
					//std::swap(cur->data, pright->data);

					//
					//if (pright == pp->_left)//左子树不存在右孩子,直接将p->_left赋值pp的左孩子即可
					//	pp->_left = pright->_left;
					//else  //左子树存在右孩子
					//	pp->_right = pright->_left; //直接将交换后的节点的左子树放到待删除节点的父节点的右子树上
					//delete pright;

					//法2:假设选择右子树的最小节点
					Node* pleft = cur->_right;//选择右子树,但是最终目的是寻找右子树的最左节点
					Node* pp = cur;
					while (pleft->_left)
					{
						pp = pleft;
						pleft = pleft->_left;
					}
					std::swap(cur->data, pleft->data);

					if (pleft == pp->_right)//如果待删除节点的右子树没有左孩子
						pp->_right = pleft->_right;
					else //如果是正常情况
						pp->_left = pleft->_right;//最左节点的右孩子需要保留在pp的左子树
					delete pleft;
				}
				return true;
			}
		}
		return false;
	}

查找函数递归实现

protected:
	bool findr(Node* root,const T&key)
	{
		if (root == nullptr)
			return false;
		else if (root->data > key)
			return finr(root->_left, key);
		else if (root->data < key)
			return finr(root->_right, key);
		else
			return true;
	}
public:
	bool FindR(const T&key)
	{
		return findr(root,key);
	}

插入函数递归实现

protected:
	bool insertr(Node* &root, const T& key)//由于要修改原树,所以建议传引用
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (root->data > key)
			return insertr(root->_left, key);
		else if (root->data < key)
			return insertr(root->_right, key);
		else
			return false;
	}
public:
	bool InsertR(const T& key)
	{
		return insertr(root, key);
	}

删除函数递归实现

//节点的删除递归实现
protected:
	bool eraser(Node* &root, const T& key)
	{
		if (root == nullptr)
			return false;

		if (root->data < key)
		{
			return eraser(root->_right, key);
		}
		else if (root->data > key)
		{
			return eraser(root->_left, key);
		}
		else
		{
			// 删除
			if (root->_left == nullptr)
			{
				Node* del = root;
				root = root->_right;
				delete del;

				return true;

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

				return true;
			}
			else
			{
				//以删除右子树的最小节点为例
				Node* pleft = root->_right;
				while (pleft->_left)
				{
					pleft = pleft->_left;
				}

				std::swap(root->data, pleft->data);

				// 转换成在子树递归删除
				return eraser(root->_right, key);
			}
		}
	}
public:
	bool EraseR(const T& key)
	{
		return eraser(root, key);
	}

拷贝构造和赋值运算符重载

	//拷贝构造函数
protected:
	Node* copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newroot = new Node(root->data);
		newroot->_left = copy(root->_left);
		newroot->_right = copy(root->_right);

		return newroot;
	}
public:
	BSTree(const BSTree<T>& b)
	{
		root = copy(b.root);
	}
	BSTree(){}//构造函数
	//赋值运算符重载
	BSTree<T>& operator=(BSTree<T>& b)
	{
		std::swap(root, b.root);
		return *this;
	}

搜索二叉树的相关性质及其应用

二叉搜索树的查找

       我们不难发现,搜索二叉树的中序遍历是对应的升序排列,当我们在二叉搜索树中查找某个值的时候,我们却不能简单的认为其查找效率是logn级别的,因为查找效率需要考虑最坏的情况,而我们的logn的效率实际上是理想化的情况,比某个数大和比某个数小的数各占一半,分别在这个数的左右两侧子树上,才有logn级别的效率,但是,若是在最坏的情况下,搜索树变成了类似于单链表式的链状结构,则查找效率就会变成 o(n) 级别的,因此,我们需要对二叉搜索树进行优化才能符合我们的要求,就叫做所谓的平衡搜索二叉树(AVL树,红黑树等),具体我们后面再做讲解。

key-value模型

        实际上,在C++中,key-value模型的例子有很多,比如map,pair等数据结构,它们通常是一个键值,一个值的方式组成,其中,键值作为功能函数的主要参数,相当于数组的下标,有了下标,我们就可以按下标访问任何一个存在的元素,相当于查找等操作,现在,我们想在二叉搜索树中实现key-value模型,此时,我们只需要将本来一个节点存储的一个数据,修改为一个节点可以存储两个数据即可,具体细节见下面的实现:

namespace 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)
			{
				parent = cur;

				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					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)
		{
			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 (cur == parent->_left)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
					}
					else if (cur->_right == nullptr)
					{//右为空
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
					}
					else
					{//左右都不为空

						// 右树的最小节点(最左节点)
						Node* parent = cur;
						Node* subLeft = cur->_right;
						while (subLeft->_left)
						{
							parent = subLeft;
							subLeft = subLeft->_left;
						}

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

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

					return true;
				}
			}

			return false;
		}
		void InOrder()
		{
			_InOrder(_root);
			std::cout << std::endl;
		}
	private:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;

			_InOrder(root->_left);
			std::cout << root->_key << ":" << root->_value << std::endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};
}

       以上就是二叉搜索树的初阶内容了,我们后续在学习map和set时还会使用二叉搜索树来实现,具体我们后面再来分析,下一篇,我们将讲解一些二叉树的相关例题,敬请期待~

       要做一个情绪稳定的成年人,。烦躁的时候千万不要讲话,也不要做任何决定,就安静的待一会。当你内心足够坚定的时候,谁都没有办法影响你的情绪。经历的越多,反而明白的更多,把期待降低,把依赖变少,努力把生活变成自己想要的样子。

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

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

相关文章

学习率设置

在我们刚刚接触深度学习时&#xff0c;对学习率只有一个很基础的认知&#xff0c;当学习率过大的时候会导致模型难以收敛&#xff0c;过小的时候会收敛速度过慢&#xff0c;其实学习率是一个十分重要的参数&#xff0c;合理的学习率才能让模型收敛到最小点而非局部最优点或鞍点…

学 Java 怎么进外企?

作者&#xff1a;**苍何&#xff0c;CSDN 2023 年 实力新星&#xff0c;前大厂高级 Java 工程师&#xff0c;阿里云专家博主&#xff0c;土木转码&#xff0c;现任部门技术 leader&#xff0c;专注于互联网技术分享&#xff0c;职场经验分享。 &#x1f525;热门文章推荐&#…

HNU程序设计 练习三-控制结构

1.台球游戏 【问题描述】 在本台球游戏中&#xff0c;包含多种颜色的球&#xff0c;其中&#xff1a;红球15只各1分、黄球1只2分、绿球1只3分、咖啡球1只4分、蓝球1只5分、粉球1只6分、黑球1只7分。 球的颜色表示为&#xff1a; r-红色球 y-黄色球 g-绿色球 c-咖啡色球 b-蓝色…

闭循环低温恒温器的使用注意事项

与液氮恒温器相比&#xff0c;闭循环低温恒温器显得稍微复杂一些&#xff01;这主要表现在组成部分、体积重量、使用操作、升降温时间等方面。闭循环低温恒温器主要由冷头、氦压缩机、两根氦气连管组成&#xff0c;配套设备还有控温仪、真空泵&#xff0c;可能还有循环水冷机。…

离散数学实践(2)-编程实现关系性质的判断

*本文为博主本人校内的离散数学专业课的实践作业。由于实验步骤已经比较详细&#xff0c;故不再对该实验额外提供详解&#xff0c;本文仅提供填写的实验报告内容与代码部分&#xff0c;以供有需要的同学学习、参考。 -------------------------------------- 编程语言&#xff…

VM虚拟机逆向 --- [NCTF 2018]wcyvm 复现

文章目录 前言题目分析 前言 第四题了&#xff0c;搞定&#xff0c;算是独立完成比较多的一题&#xff0c;虽然在还原汇编的时候还是很多问题。 题目分析 代码很简单&#xff0c;就是指令很多。 opcode在unk_6021C0处&#xff0c;解密的数据在dword_6020A0处 opcode [0x08, …

谈一谈SQLite、MySQL、PostgreSQL三大数据库

每一份付出&#xff0c;必将有一份收货&#xff0c;就像这个小小的果实&#xff0c;时间到了&#xff0c;也就会开花结果… 三大数据库概述 SQLite、MySQL 和 PostgreSQL 都是流行的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;但它们在功能、适用场景和性…

【UE】从UI拖拽生成物体 —— 更改位置与定点销毁

本篇在上一篇博客&#xff08;【UE】从UI中拖拽生成物体-CSDN博客&#xff09;基础上继续增加更改生成的Actor的位置与定点销毁Actor的功能。 目录 效果 步骤 一、修改生成好的Actor位置 解决问题一&#xff1a;从UI拖出多个actor后&#xff0c;只能对第一个拖出的actor的…

传智杯-21算法赛初赛B组题目详细解法解析-AB题(C/C++、Python、Java)

🚀 欢迎来到 ACM 算法题库专栏 🚀 在ACM算法题库专栏,热情推崇算法之美,精心整理了各类比赛题目的详细解法,包括但不限于ICPC、CCPC、蓝桥杯、LeetCode周赛、传智杯等等。无论您是刚刚踏入算法领域,还是经验丰富的竞赛选手,这里都是提升技能和知识的理想之地。 ✨ 经典…

UG\NX二次开发 先设置默认颜色再创建对象

文章作者:里海 来源网站:里海NX二次开发3000例专栏 感谢粉丝订阅 感谢 qq_42461973 订阅本专栏,非常感谢。 简介 有粉丝问,可不可以先设置默认颜色再创建对象?这个是可以的,下面是例子: 效果 代码 #include "me.hpp" using namespace std;

java/springboot服务第三方接口安全签名(Signature)实现方案

前言 有的时候&#xff0c;我们需要把我们系统里的接口开放给第三方应用或企业使用&#xff0c;那第三方的系统并不在我们自己的认证授权用户体系内&#xff0c;此时&#xff0c;要如何保证我们接口的数据安全和身份识别呢&#xff1f; 在为第三方系统提供接口的时候&#xf…

筑基新一代数据底座,中国科大让智慧科研更有数

著名科学哲学家库恩在《科学革命的结构》中认为&#xff0c;范式是科研的一种理论体系&#xff0c;范式的突破会带来一系列科学革命。 如今在科研领域&#xff0c; 人工智能不断打破科研边界&#xff0c;AI for Science被视为下一个科研新范式&#xff0c;不仅为科学研究带来了…

Cesium:为地图添加指北针、缩放按钮和比例尺

作者&#xff1a;CSDN _乐多_ 网上找的很多代码用不了。本文记录了Cesium中为地图添加指北针、缩放按钮和比例尺的可用代码。 文章目录 一、代码 一、代码 const viewer new Cesium.Viewer(cesiumContainer, {// ...navigationHelpButton: false,sceneModePicker: false,sc…

校验验证码是否过期(定时刷新验证码)

需求&#xff1a; 我们在登录的时候会遇到通过接口请求验证码的操作&#xff0c;这里的验证码会有过期的时间&#xff0c;当我们验证码过期了&#xff0c;我们要进行重新刷新验证码。 我们这里根据后端返回的当前时间和过期时间判断&#xff0c;过期的时间超过了当前时间的时候…

Java面向对象 下(六)

Java面向对象 ( 下) 观看b站尚硅谷视频做的笔记 文章目录 Java面向对象 ( 下)1、 关键字&#xff1a;static1.1、static 的使用1.1.1、static 修饰属性1.1.2、 static 修饰方法1.1.3、 static 修饰代码块1.1.4、 static 修饰内部类1.1.5、类变量 vs 实例变量内存解析 1.2、 自…

关于msvcp120.dll丢失的解决方法详解,快速解决dll丢失问题

在计算机使用过程中&#xff0c;经常会遇到“msvcp120.dll丢失”的错误提示。这个错误提示通常出现在运行某些程序或游戏时&#xff0c;造成相关应用程序可能无法正常启动或运行。那么&#xff0c;究竟是什么原因导致了msvcp120.dll文件的丢失呢&#xff1f;本文将详细解析msvc…

【QT】文件读写

新建项目 加入控件 整体做一个布局 功能&#xff1a;选择文件路径&#xff0c;打开文件&#xff08;两种文件格式&#xff1a;utf-8、GBK&#xff09; #include "widget.h" #include "ui_widget.h" #include <QPushButton> #include <QFileDial…

云产品ECS免费试用获取奖励步骤

文章目录 1、获取活动链接2、报名参加3、试用产品领取产品试用权限配置安全组访问应用提交作品 4、提交任务获取奖励 1、获取活动链接 活动时间2023.11.1&#xff5e;2023.11.30 名额有限&#xff0c;先到先得 进群群主获取活动链接 2、报名参加 直接点击链接进入小程序进行…

【带头学C++】----- 三、指针章* ---- 3.1指针变量的定义

指针在C语言是核心&#xff0c;在C中更是核心。所以本章节将详细讲解指针的使用方法以及指针的一些特殊用法&#xff0c;和引用的区别&#xff0c;以及指针涉及到一些算法基础。通过案例引导&#xff0c;使得能更清楚命明白。在C中的指针是一种数据类型&#xff0c;其使用方法和…

【Java 进阶篇】Java ServletContext功能详解:域对象的使用

Java ServletContext是Java Web应用程序中的一个关键组件&#xff0c;它提供了一种在不同Servlet之间共享数据的机制。这种共享通过域对象来实现&#xff0c;包括ServletContext域、Session域和Request域。在本篇博客中&#xff0c;我们将重点关注ServletContext域&#xff0c;…