普通二叉搜索树的模拟实现【C++】

news2024/9/28 14:10:04

二叉搜素树简单介绍

二叉搜索树又称二叉排序树,是具有以下性质的二叉树:

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

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

  3. 它的左右子树也分别为二叉搜索树

注意:空树也是二叉搜索树


二叉搜素树的模型

  1. K模型:

K模型即只有key作为关键字,节点中只需要存储Key即可,关键字即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

  1. KV模型:

每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
该种方式在现实生活中非常常见:
比如
英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。

K模式和KV模式实现上基本一样,就是节点中存储的是key还是<key,val>的区别


二叉搜素树的性能

二叉搜索树的性能取决于树的高度,因为一次查找最多查找高度次,而删除和插入也是在查找的基础上增加了一些O(1)的操作
在最理想的情况下,树是完全平衡的,平均查找、插入和删除的时间复杂度O(log N)。
但在最坏的情况下,树可能退化成一个链表,此时这些操作的时间复杂度将增加到O(N)。

例:
在这里插入图片描述


全部的实现代码放在了文章末尾

准备工作

创建两个文件,一个头文件BSTree.hpp,一个源文件test.cpp

【因为模板的声明和定义不能分处于不同的文件中,所以把成员函数的声明和定义放在了同一个文件BSTree.hpp中】

  1. RBTree.hpp:存放包含的头文件,命名空间的定义,成员函数和命名空间中的函数的定义

  2. test.cpp:存放main函数,以及测试代码


包含头文件

iostream:用于输入输出


类的成员变量

在这里插入图片描述


构造函数和拷贝构造

构造函数没什么好说的,默认构造就行了

BSTree() 
	:_root(nullptr)
{}

拷贝构造:
因为节点都是从堆区new出来的,所以要深拷贝

使用递归实现深拷贝:
因为拷贝构造不能有多余的参数,但是递归函数又必须使用参数记录信息
所以再封装一个成员函数,专门用来递归拷贝:
在这里插入图片描述
然后在拷贝构造里面调用一下这个函数就行了

拷贝构造
BSTree(const BSTree& obj)
{
	_root = Copy(obj._root);
}

swap和赋值运算符重载

交换两颗二叉搜索树的本质就是交换两颗数的资源(数据),而它们的资源都是从堆区申请来的,然后用指针指向这些资源
在这里插入图片描述

并不是把资源存储在了二叉搜索树对象中

所以资源交换很简单,直接交换_root指针的指向即可

void Swap(BSTree& obj)
{
	std::swap(_root, obj._root);
}

赋值运算符重载

BSTree& operator=(BSTree obj)
{
	this->Swap(obj);
	return *this;
}

为什么上面的两句代码就可以完成深拷贝呢?
这是因为:

使用了传值传参,会在传参之前调用拷贝构造,再把拷贝构造出的临时对象作为参数传递进去

赋值运算符的左操作数,*this再与传入的临时对象obj交换,就直接完成了拷贝

在函数结束之后,存储在栈区的obj再函数结束之后,obj生命周期结束

obj调用析构函数,把指向的从*this那里交换来的不需要的空间销毁


析构函数

使用递归遍历,把所有从堆区申请的节点都释放掉:
因为析构函数不能有多余的参数,但是递归函数又必须使用参数记录信息
所以再封装一个成员函数,专门用来递归释放:
在这里插入图片描述
然后在拷贝构造里面调用一下这个函数就行了

析构函数
~BSTree()
{
	Destroy(_root);
	_root = nullptr;
}

find

具体流程:
从根节点开始,将目标值(传入的key)与当前节点的key进行比较。
如果目标值小于当前节点值,则在左子树中继续查找;
如果目标值大于当前节点值,则在右子树中继续查找。

这个过程一直进行,直到找到与目标值或者到达空节点为止。

把上述过程转成代码:
在这里插入图片描述


insert

插入的具体过程如下:

  1. 树为空,则直接新增节点,赋值给二叉搜索树的成员变量_root指针

  2. 树不空,则按照查找(find)的逻辑找到新节点应该插入的位置

  3. 树不空,如果树中已经有了一个节点的key值与要插入的节点的key相同,就插入失败

这个过程一直进行,直到找到与传入的key相等的节点或者到达空节点为止。

把上述过程转成代码:
在这里插入图片描述


erase

删除操作较为复杂,需要先在数中找到要删除的节点,再根据要删除节点的子节点数量进行不同的处理:

  1. 如果要删除节点没有子节点,则直接删除该节点。

  2. 如果要删除节点有一个子节点(子树),则用其子节点(子树)替换该节点。

  3. 如果要删除节点有两个子节点(子树)
    在右子树中找到最小值的节点(或左子树中找到最大值的节点)来替换待删除节点,然后删除那个最小值(或最大值)的节点

情况1可以和情况2合并一下

把上述过程转成代码:

bool Erase(const K& key)
{
	Node* cur = _root;从根节点开始
	Node* parent = nullptr;
     
    先找到要删除的节点(cur)
	while (cur)如果到了空节点就结束循环
	{
		if (cur->_key < key) 目标值`大于`当前节点值,则在`右子树`中继续查找
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key) 目标值'小于'当前节点值,则在'左子树'中继续查找
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			break;找到要删除的节点了,结束循环
		}
	}

	如果找到空节点了,还没找到要删除的节点
	就说明树里面本来就没有这个key,不需要删除
	if (cur == nullptr)
	{
		return false;  删除失败,返回false
	}
	else
	{
		如果 左 子树为空, 右 子树不为空(或者左右都为空)
		即只有右子节点(右子树)或者没有子节点
		if (cur->_left == nullptr)
		{
			如果父亲节点为空,就表示cur为根节点
			if (parent == nullptr)
			{
				使用右子节点,代替根节点
				_root = cur->_right;
			}
			else  根据cur与它的父亲节点的链接关系
			{
				if (cur == parent->_left)
				{
					使用右子节点,代替cur
					parent->_left = cur->_right;
				}
				else
				{
					使用右子节点,代替cur
					parent->_right = cur->_right;
				}
			}
			delete cur;  删除cur节点,即要删除的节点
		}

		如果 右 子树为空, 左 子树不为空(或者左右都为空)
		即只有左子节点(左子树)或者没有子节点
		else if (cur->_right == nullptr)
		{
			如果父亲节点为空,就表示cur为根节点
			if (parent == nullptr)
			{
				使用左子节点,代替根节点
				_root = cur->_left;
			}
			else  根据cur与它的父亲节点的链接关系
			{
				if (cur == parent->_left)
				{
					使用左子节点,代替cur
					parent->_left = cur->_left;
				}
				else
				{
					使用左子节点,代替cur
					parent->_right = cur->_left;
				}
			}
			delete cur;  删除cur节点,即要删除的节点
		}

		else  如果左右子树都不为nullptr
		{
			去cur(要删除的节点)的右子树中找key最小的节点
			Node* tmp = cur->_right;
			Node* prev = cur;

			二叉搜索树的最小节点,一定在这颗树的最左边
			while (tmp->_left)  所以一直往左走,直到左子树为nullptr
			{
				prev = tmp;
				tmp = tmp->_left;  往左走
			}

			用右子树中key最小的节点的数据,替换cur中的数据
			也就相当于把cur(要删除的节点)删除了
			cur->_key = tmp->_key;
			cur->_val = tmp->_val;

			如果prev == cur,就说明tmp就是key最小的节点了
			此时tmp在cur(prev)的右边
			if (prev == cur)
			{cur(prev)的  右边  连上tmp的右子树
				因为tmp虽然是最左节点,但是它有可能还有右孩子
				cur->_right = tmp->_right;
				delete tmp;
			}
			else
			{
				把prev的  左边  连上tmp的右子树
				因为tmp虽然是最左节点,但是它有可能还有右孩子
				prev->_left = tmp->_right;
				delete tmp;
			}
		}
	}
	return true;  删除成功,返回true
}

empty

bool Empty()
{
    如果_root为空,那么树就是空的
	return _root == nullptr;
}

size

使用递归实现二叉搜索树的节点个数统计:
因为我们经常使用的stl的容器的size都是没有参数的,但是递归函数又必须使用参数记录信息
所以再封装一个成员函数,专门用来递归:
在这里插入图片描述
然后再size里面调用一下就行了

size_t Size()
{
	return _Size(_root);
}

中序遍历

中序遍历的递归函数:

在这里插入图片描述
然后再调用递归函数

void InOrder()
{
	_InOrder(_root);
}

全部代码



#include<iostream>
using namespace std;



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

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

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

	BSTree(const BSTree& obj)
	{
		_root = Copy(obj._root);
	}
	BSTree& operator=(BSTree obj)
	{
		this->Swap(obj);
		return *this;
	}

	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}
	void Swap(BSTree& obj)
	{
		std::swap(_root, obj._root);
	}

	bool Insert(const K& key, const V& val)
	{
		if (_root == nullptr)//树为空,则直接新增节点
		{
			//赋值给二叉搜索树的成员变量`_root`指针
			_root = new Node(key, val);

			return true;//返回true,代表插入成功
		}

		Node* cur = _root;//从根节点开始

		//定义parent来保存cur的父亲节点
		//假设根节点的父亲节点为nullptr
		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;
			}
		}

		Node* newnode = new Node(key, val);
		if (parent->_key > key)
		{
			parent->_left = newnode;
		}
		else
		{
			parent->_right = newnode;
		}
		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;//找不到就返回nullptr
	}

	bool Erase(const K& key)
	{
		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
			{
				break;
			}
		}
		if (cur == nullptr)
			return false;
		else
		{
			if (cur->_left == nullptr)
			{
				if (parent == nullptr)
				{
					_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 (parent == nullptr)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
						parent->_left = cur->_left;
					else
						parent->_right = cur->_left;
				}
				delete cur;
			}
			else
			{
				Node* tmp = cur->_right;
				Node* prev = cur;
				while (tmp->_left)
				{
					prev = tmp;
					tmp = tmp->_left;
				}
				cur->_key = tmp->_key;
				cur->_val = tmp->_val;

				if (prev == cur)
				{
					cur->_right = tmp->_right;
					delete tmp;
				}
				else
				{
					prev->_left = tmp->_right;
					delete tmp;
				}
			}
		}
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
	}
	bool Empty()
	{
		return _root == nullptr;
	}
	size_t Size()
	{
		return _Size(_root);
	}
	size_t Height()
	{
		return _Height(_root);
	}
private:
	Node* _root = nullptr;
	size_t _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int left = _Height(root->_left);
		int right = _Height(root->_right);

		return left > right ? left + 1 : right + 1;
	}
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* newnode = new Node(root->_key, root->_val);
		newnode->_left = Copy(root->_left);
		newnode->_right = Copy(root->_right);

		return newnode;
	}

	//使用  后序遍历  释放
	void Destroy(Node* root)
	{
		//空节点不需要释放,直接返回
		if (root == nullptr)
			return;

		Destroy(root->_left);//递归释放左子树

		Destroy(root->_right);//递归释放右子树

		delete root;//释放根节点
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);//遍历左子树

		//打印信息
		cout << root->_key << ":" << root->_val << endl;

		_InOrder(root->_right);//遍历右子树
	}

	//直接遍历二叉树进行节点统计
	size_t _Size(Node* root)
	{
		if (root == nullptr)
			return 0;

		//统计左子树节点个数
		int left = _Size(root->_left);
		//统计右子树节点个数
		int right = _Size(root->_right);

		return left + right + 1;//1是当前节点
	}
};


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

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

相关文章

C++深入学习string类成员函数(4):字符串的操作

引言 在c中&#xff0c;std::string提供了许多字符串操作符函数&#xff0c;让我们能够秦松驾驭文本数据&#xff0c;而与此同时&#xff0c;非成员函数的重载更是为string类增添了别样的魅力&#xff0c;输入输出流的重载让我们像处理基本类型的数据一样方便地读取和输出字符…

51单片机系列-串口(UART)通信技术

&#x1f308;个人主页&#xff1a; 羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 并行通信和串行通信 并行方式 并行方式&#xff1a;数据的各位用多条数据线同时发送或者同时接收 并行通信特点&#xff1a;传送速度快&#xff0c;但因需要多根传输线&#xf…

20.指针相关知识点1

指针相关知识点1 1.定义一个指针变量指向数组2.指针偏移遍历数组3.指针偏移的补充4.指针和数组名的见怪不怪5.函数、指针、数组的结合 1.定义一个指针变量指向数组 指向数组首元素的地址 指向数组起始位置&#xff1a;等于数组名 #include <stdio.h>int main(){int ar…

LeetCode 2266. 统计打字方案数

Alice 在给 Bob 用手机打字。数字到字母的 对应 如下图所示。 为了 打出 一个字母&#xff0c;Alice 需要 按 对应字母 i 次&#xff0c;i 是该字母在这个按键上所处的位置。 比方说&#xff0c;为了按出字母 s &#xff0c;Alice 需要按 7 四次。类似的&#xff0c; Alice 需…

Qt --- Qt窗口

一、前言 前面学习的所有代码&#xff0c;都是基于QWidget控件。QWidget更多的是作为别的窗口的一个部分。 Qt中的QMainWindow就是窗口的完全体 Menu Bar菜单栏 Tool Bar Area 工具栏&#xff0c;类似于菜单栏&#xff0c;工具栏本质上就是把菜单中的一些比较常用的选项&…

活动展览棚:灵活多变的展览解决方案—轻空间

在快速变化的市场环境中&#xff0c;活动展览棚作为一种创新的展示空间&#xff0c;正受到越来越多企业和组织的青睐。无论是展览、活动、还是市场推广&#xff0c;活动展览棚都能提供高效、灵活的解决方案&#xff0c;为品牌传播和产品展示带来全新体验。 便捷的搭建与拆卸 活…

C. Cards Partition 【Codeforces Round 975 (Div. 2)】

C. Cards Partition 思路&#xff1a; 可以O(n)直接判断&#xff0c;牌组从大到小依次遍历即可。 不要用二分答案&#xff0c;因为答案不一定是单调的 代码: #include <bits/stdc.h> #define endl \n #define int long long #define pb push_back #define pii pair<…

Angular与Vue的全方位对比分析

一、框架概述 Angular Angular是由Google开发和维护的一款开源JavaScript框架。它采用TypeScript编写&#xff0c;具有一套完整的开发工具和规范。Angular遵循MVC&#xff08;Model - View - Controller&#xff09;或更确切地说是MVVM&#xff08;Model - View - ViewModel&a…

【Python】数据可视化之分布图

分布图主要用来展示某些现象或数据在地理空间、时间或其他维度上的分布情况。它可以清晰地反映出数据的空间位置、数量、密度等特征&#xff0c;帮助人们更好地理解数据的内在规律和相互关系。 目录 单变量分布 变量关系组图 双变量关系 核密度估计 山脊分布图 单变量分布…

超全面的线程编程实战指南

第一部分&#xff1a;线程基本概念 一、线程简介 线程是操作系统能够进行运算调度的最小单位&#xff0c;它是一个进程内的独立控制流。线程之间共享同一进程的资源&#xff0c;如内存空间和其他系统资源。 二、线程的优势 效率高&#xff1a;由于线程共享相同的地址空间&a…

用Python+flask+mysql等开发的Excel数据资产落地工具

话不多说 1)Excel文件上传,列表预览 2)选中要导入结构及数据的Excel文件 约束说明: 2.1)Excel文件的第一行约定为表头名称 2.2)系统自动识别字段列名及数据类型,目前不支持合并表头 3)Excel建表导入数据成功后,可在表源列表中预览查看 4)对数据表源可进行透视图设计管理,可对…

Vue-Bag-Admin 采用漂亮的 Naive UI 构建的开源中后台系统,基于 Vue3 / Vite / TypeScript 等最新的前端技术栈

这是一款完成度很高、实用性很强的 admin 前端框架&#xff0c;颜值不错&#xff0c;推荐给大家。 Vue-Bag-Admin 在官网上也直接称为 Bag-Admin&#xff0c;这是一款专门为企业项目搭建中后台管理平台的前端框架&#xff0c;基于目前最新的前端技术栈 Vue3、Vite、TypeScript…

双十一儿童耳勺哪款好?双十一儿童专用掏耳神器推荐!

近期收到很多后台私信问儿童应该选择哪款耳勺&#xff0c;现在市面上掏耳神器众多&#xff0c;但要选择一个能适合儿童专用的产品要仔细斟酌。 如果挑选到不符合或者劣质的儿童掏耳工具&#xff0c;不仅清洁不干净不说&#xff0c;还会有损害儿童肌肤的风险&#xff01;那么专为…

Llama 3.2 90B刚开源就被Molmo-72B全面击败!

Meta此次发布的Llama 3.2一个新特性是视觉模型&#xff0c;包括11B和90B&#xff0c;作为首批支持视觉任务的Llama模型&#xff0c;但是allenai开源的多模态Molmo-72B&#xff0c;在视觉评测上全面击败Llama 3.2 90B。 两个新发布的开源LLM之间的基准测试比较&#xff1a;Molm…

leetcode163.缺失的区间,模拟

leetcode163.缺失的区间 给定一个排序的整数数组 nums &#xff0c;其中元素的范围在 闭区间 [lower, upper] 当中&#xff0c;返回不包含在数组中的缺失区间。 示例&#xff1a; 输入: nums [0, 1, 3, 50, 75], lower 0 和 upper 99, 输出: [“2”, “4->49”, “51-&…

OpenSource - 开源WAF_SamWaf

文章目录 PreSafeLine VS SamWaf开发初衷软件介绍架构界面主要功能 使用说明下载最新版本快速启动WindowsLinuxDocker 启动访问升级指南自动升级手动升级 在线文档 代码相关代码托管介绍和编译已测试支持的平台测试效果 安全策略问题反馈许可证书贡献代码 Pre Nginx - 集成Mod…

关系模型与关系代数——数据库原理 总结2

2.1 关系模型 关系数据结构 关系模型的数据结构是二维表&#xff0c;亦称为关系。关系数据库是表的集合&#xff0c;即关系的集合。表是一个实体集&#xff0c;一行就是一个实体&#xff0c;它由有关联的若干属性的值所构成。 关系模型的相关概念 列就是数据项 或 字段 或 属…

C++那些你不得不知道的(2)

C那些你不得不知道的&#xff08;2&#xff09; 1、缺省参数在使用的遍历 &#xff08;1&#xff09;以下是实现顺序表的初始化和检查容量空间的方式&#xff1a; void Init(list* ps) {ps->arr NULL;ps->Capacity ps->size 0; }void CheckCapacity(list* ps) {…

量化系统QTYX使用攻略|“自动交易”篇——ETF量化框架,集成“策略回测仓位风控下单”(更新v2.9.2)...

QTYX系统简介 股票量化交易系统QTYX是一个即可以用于学习&#xff0c;也可以用于实战炒股分析的系统。 分享QTYX系统目的是提供给大家一个搭建量化系统的模版&#xff0c;最终帮助大家搭建属于自己的系统。因此我们提供源码&#xff0c;可以根据自己的风格二次开发。 关于QTYX的…

ABAP版本管理

在开发中ABAP管理有查看&#xff0c;生成&#xff0c;比对&#xff0c;远程比对&#xff0c;回滚&#xff0c;删除等等操作。日常中往往会遇到需要回滚到上一版本的代码&#xff0c;但是ABAP不像git代码管理那么专业&#xff0c;但是也是可以回滚代码的。在此记录一下操作过程。…