二叉搜索树K和KV结构模拟

news2024/12/25 9:59:01

一 什么是二叉搜索树

  这个的结构特性非常重要,是后面函数实现的结构基础,二叉搜索树的特性是每个根节点都比自己的左树任一节点大,比自己的右树任一节点小。

例如这个图,

 41是根节点,要比左树大,比右树小,满足但还不够,还要去看看41的左子树的根和右子树的根是否满足,更要判断这棵树上所有的根节点是不是都满足。而这棵树最厉害的地方之一我们用中序遍历(顺序左根右)便可以知道,遍历结果为13,15,17,22,28,33,37,41,42,50,53,58,61,66,78,排序不就排好了吗,复杂度可媲美快排和归并。二叉搜索树另一个功能那当然就是搜索了,例如我们要找66,66比根节点大,就不用去左子树找了,一下子少遍历一半,然后就去右子树找,和根节点58比较,66比58大,再去右子树找,再比较就找到了,最多查找高度次,满二叉树下为log(n)。而二叉搜索树是不是完美无缺,我也以为已经完美了,不好意思,我太年轻了,直到我看到下面这颗树。

 这个查找一次的效率就退化为O(N)了,解决办法:转化为平衡二叉树,通俗点就是换个根节点重新构造二叉树,例如把5或者7换成根节点,大家可以试试练习一下构建二叉树,构建完后的高度肯定比上图低,查找效率不就高了吗。

   在说二叉搜索树的实现前,我们先说说什么是K结构,什么是KV结构,K结构就是只存一个数据,这个数据称为关键码,例如在英文词库里找一个英文单词,就是用关键码查找,需要的数据也是找到的关键码,但是KV结构就不同了,例如,通过拼音找汉字,这个时候拼音就是关键码,但是我们需要的数据不是拼音这个关键码,而是与之对应的汉字,这就是KV结构。

二 二叉搜索树K结构实现

1 树的节点类

template<class T>
	struct TreeNode
	{
		TreeNode(const T& val)
			:_val(val)//_val可能为自定义类型,在初始化列表初始化方便调用构造函数
		{
			;
		}
		TreeNode* left = nullptr;
		TreeNode* right = nullptr;
		T _val;
	};

2 BinaryTree树类

查找和排序都封装到了BinaryTree类中,和list一样,将节点类和树类分开封装。

 (1) 默认构造函数


	template<class T>
	class BinaryTree
	{
	public:
		typedef TreeNode<T> Node;
		BinaryTree(Node*node=nullptr)  该构造函数是用节点指针初始化,
                                    也可以再写个构造函数用个BinaryTree对象初始化
			:_root(node)
		{
			;
		}
     private:
		Node* _root = nullptr;树类只需根节点地址即可管理整棵树
	};
		

  (2)  拷贝构造函数

因为两个copy函数都是用递归遍历二叉树,所以只能再写一个子函数,毕竟外部无法传_root指针。

void copy1(Node*tree)//前序遍历加复用insert拷贝二叉树
		{
			if (tree == nullptr)
				return;
			insert(tree->_val); 先插入根节点的值,再去左子树和右子树获取节点的数值
                                 insert内部会开辟空间
			copy(tree->left);
			copy(tree->right);
		}


      方法二比较巧妙的是它的第一个参数,root是外部传参_root的引用
      所以root=new node(),可以直接修改根节点
      而要拷贝左子树就传root->left的引用,这样new出来的节点可以直接连接到根的_left指针上。
      右子树同理。

		void copy2(Node*&root,Node*tree)
		{
			if (tree== nullptr)
				return;
			root = new Node(tree->_val);
			copy2(root->left,tree->left);
			copy2(root->right,tree->right);
		}
		BinaryTree(const BinaryTree<T>&tree)
		{
			//copy(tree._root);
			copy2(_root, tree._root);
		}

copy2函数传指针引用我是受下面一个成员函数实现的启发,这个传引用一定要好好体会,方便理解后面的函数,非常巧妙。

(3)  赋值

   用的是现代写法,比如t1,t2是两个BinaryTree对象,t1=t2就会调用下面的赋值函数,可是我的参数不是引用,那按规定自定义类型传值传参要用拷贝构造(我在类的成员函数博客曾提及),t2传参给tree就要调用拷贝构造,那tree就是一个新拷贝的对象,我们就可以用swap直接交换tree和t1的根节点指针,并且tree就是一个局部对象,函数调用完后会自动调用析构函数,省去了我们写析构t1树和创建新树的功夫,都给编译器做了。(string模拟的赋值也用到了现代写法)

​

		void swap(BinaryTree<T>& tree)
		{
			std::swap(_root, tree._root);
		}
		void operator=(BinaryTree<T> tree)
		{
			swap(tree);
		}

​

(4) find函数

搜索树怎么能缺少搜索功能呢


这个是find函数的子函数,子函数原因和上面同理,都是一开始传参外部无法获取_root,
         因为递归遍历代码量少,所以我实现的是递归版本
		bool _findR(Node*root,const T& val)
		{
			if (root == nullptr)
				return false;

			if (val > root->_val)//val比当前_val大,去右树找
			{
			    return _findR(root->right, val);
			}
			else if (val < root->_val)//val比当前_val小,去左树找
			{
				return _findR(root->left, val);
			}
			else  
			{
				return true;//val和当前_val相等,返回true
			}
		}

        //下面这个是外部调用的find函数,只需要传要查找的值即可  
		bool find(const T& val)
		{
			return _findR(_root, val);
		}

我之前在写find函数时,我还想着返回false是不是应该当左树和右树都没找到才返回false,好一会才醒悟,我们之所以去左树找,就是因为要找的val比根节点的值小,那右树更不会有了,所以左树找到nullptr就应该返回false,同理右树找到nullptr也返回false。

(5) insert函数

因为要递归去找合适的位置插入,所以同样要写一个子函数  
   void _insertR(Node*& root, const T&val)
		 {
			if (root == nullptr)当找到空节点,就可以插入了,此时才是引用起作用的时候
			{
				root = new Node(val);  直接就可以修改了,因为root是上一个节点的left或者 
                                        right指针的引用。
 
				return;
			}
			Node* cur = root;
			if (val > cur->_val)//val大于当前根,插入到右树去
			{
				_insertR(cur->right,val);
			}
			else if (val < cur->_val)//val小于当前根,插入到左树去
			{
				_insertR(cur->left, val);
			}
			else
			{
				return;
			}
		}
		void insert(const T& val)
		{
			_insertR(_root, val);
		}

(6) 中序遍历

		
		void _Inorder(Node* root)//中序递归遍历
		{
			if (root == nullptr)
				return;
			_Inorder(root->left);  一直往左子树递归,直到左子树为空,算访问完,可以访问根。
			cout << root->_val << " "; 
			_Inorder(root->right); 然后去右子树访问,同样分为左子树,根,右子树
		}
		void Inorder()
		{
			_Inorder(_root);//调用子函数,外部无法获取私有成员_root
		}
		

(7) erase函数

     bool _Rerase(Node*&root,const T&val)
		{
			if (root == nullptr)
				return false;
			Node* cur = root;
			if (val > root->_val)//用_val找节点
			{
				return _Rerase(root->right, val);
			}
			else if (val < root->_val)
			{
				return _Rerase(root->left, val);
			}
			else//找到了
			{
				//该节点只有一个或者无子节点
				if (root->left == nullptr)  由于root是上一节点左指针或者右指针的别名,
                                            所以可以直接拿root->right来赋值给root,否则还要 
                                            判断root->right是链接在上一节点的left指针还是 
                                             right指针。
				{
					root = root->right;
				}
				else if (root->right == nullptr)
				{
					root = root->left;
				}
				else
				{
					删除有两个子节点的节点-替换法
					找该节点左子树中最大的,或者右子树中最小的来替换删除节点
					Node* leftMax = root->left;
					while (leftMax->right)
					{
						leftMax = leftMax->right;
					}
					std::swap(leftMax->_val, root->_val);
					return _Rerase(root->left, val);
      调用_Rerase去删除leftMax节点,
         这里必须要传root->left,去左子树删除值为val的节点,不能传root
    例如我们交换leftMax的7和root的8值,如果传root,8的值比7大,就会去右树删,
    就找不到leftMax节点了,但是root的左子树仍然满足二叉搜索的特性,就可以找到leftMax节点并删除。  


				}
				delete cur;  该处统一释放删除节点,并返回true
				return true;
			}
		}
		bool erase(const T& val)//删除某个节点
		{
			return _Rerase(_root, val);
		}
	

三 kv结构实现

   本来我以为kv结构是要将K结构的树大改,当我实现后才发现,赋值可以直接照搬,,find,insert,erase中大量的if判断都是用关键码判断,根本不需要改动,中序遍历也就多打印一个数据,还有insert和拷贝构造函数要在new一个节点的时候多传一个参数。

接下来就看看一些比较重要的改动,在这里_key存关键码,而我上面二叉树K结构中是_val存的关键码,不要搞混了。

1 树的节点类

     template<class T,class K>
	struct TreeNode
	{
		TreeNode(const T& val,const K&key)
			:_val(val)
			,_key(key)
		{
			;
		}             类内可不加模板参数,也就是说TreeNode等价于TreeNode<T,k>

		TreeNode* left = nullptr; 
		TreeNode* right = nullptr;
		T _val;//存与关键码对应的数据
		K _key;//_key存关键码
	};

2 BinaryTree类

有了先前K结构树的基础,这里构造和析构函数我们就很好理解。

(1)构造和析构函数


	
	template<class T, class K>
	class BinaryTree
	{
	public:
		typedef TreeNode<T,K> Node;
		BinaryTree(Node* node = nullptr)  默认构造无改变
			:_root(node)
		{
			;
		}
		void _DestroyR(Node*&root)  递归释放节点,采用后序遍历的方式delete
		{
			if (root == nullptr)
				return;
			_DestroyR(root->left);
			_DestroyR(root->right);
			delete root;
			root = nullptr;
		}
		~BinaryTree()
		{
			_DestroyR(_root);
		}
	private:
		Node* _root = nullptr;   成员变量是不变的,毕竟kv结构的树用根节点同样可以管理
	};
}
		

(3)erase函数

  bool _Rerase(Node*& root, const T& key)
		{
			if (root == nullptr)
				return false;
            Node* cur = root; //记录节点,方便后面delete
			if (key > root->_key)
			{
				return _Rerase(root->right,key);
			}
			else if (key <root->_key)
			{
				return _Rerase(root->left, key);
			}
			else//找到了
			{
                
				//该节点只有一个或者无子节点
				if (root->left == nullptr)
				{
					root = root->right;
				}
				else if (root->right == nullptr)
				{
					root = root->left;
				}
				else
				{
					Node* leftMax = root->left;
					while (leftMax->right)
					{
						leftMax = leftMax->right;
					}
					std::swap(leftMax->_val, root->_val); 在交换时要多交换一个值
					std::swap(leftMax->_key, root->_key);
					return _Rerase(root->left, key); 并且还是用key值去找leftMax删
				}  删除完leftMax后就直接return了,就不会重复删除。
				delete cur;
				return true;
			}
		}
		bool erase(const T& val)//删除某个节点
		{
			return _Rerase(_root, val);
		}

二叉搜索树中最复杂的就是erase函数,大家在此处一定要画图理解。

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

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

相关文章

数据结构-队列的实现(C语言版)

前言 队列是一种特殊的线性表&#xff0c;它只允许在一端对数据进行插入操作&#xff0c;在另一端对数据进行删除操作的特殊线性表&#xff0c;队列具有先进先出的&#xff08;FIFO&#xff09;的 特性&#xff0c;进行插入操作的一端称为队尾&#xff0c;进行删除操作的一端称…

关于Android Studio Http Proxy设置

对敌人最大的蔑视就是沉默。--鹿丸 我们使用Android Studio 开始构建的时候会有卡顿的情况&#xff0c;甚至死机&#xff0c;也就是所谓的【android studio】构建卡住问题&#xff0c;如果依赖库类都是国内的&#xff0c;检查是否开启了代理 这个地方选择下面的自动代理 国内…

Redis_事务操作

13. redis事务操作 13.1事务简介 原子性(Atomicity) 一致性(Consistency) 隔离性(isolation) 持久性(durabiliby) ACID 13.2 Redis事务 提供了multi、exec命令来完成 第一步&#xff0c;客户端使用multi命令显式地开启事务第二步&#xff0c;客户端把事务中要执行的指令发…

WebRTC音视频通话-实现GPUImage视频美颜滤镜效果iOS

WebRTC音视频通话-实现GPUImage视频美颜滤镜效果 在WebRTC音视频通话的GPUImage美颜效果图如下 可以看下 之前搭建ossrs服务&#xff0c;可以查看&#xff1a;https://blog.csdn.net/gloryFlow/article/details/132257196 之前实现iOS端调用ossrs音视频通话&#xff0c;可以查…

KNN分类器、神经网络原理基础与代码实现

急切学习 两步&#xff1a;&#xff08;1&#xff09;归纳 &#xff08;2&#xff09;演绎 例如&#xff1a;贝叶斯分类器、决策树分类等等。 惰性学习 将训练数据建模过程推迟到需要对样本分类时&#xff08;直观理解&#xff1a;死记硬背&#xff0c;记住所有的训练数据&…

Springboot集成ip2region离线IP地名映射-修订版

title: Springboot集成ip2region离线IP地名映射 date: 2020-12-16 11:15:34 categories: springboot description: Springboot集成ip2region离线IP地名映射 1. 背景2. 集成 2.1. 步骤2.2. 样例2.3. 响应实例DataBlock2.4. 响应实例RegionAddress 3. 打开浏览器4. 源码地址&…

HTML表单标签大全并附有详细代码+案例

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生 &#x1f43b;‍❄️个人主页&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;落798. &#x1f54a;️系列专栏&#xff1a;零基础学java ----- 重识c语言 ---- 计算机网络—【Spring技术内幕】…

YOLOv8目标检测算法

YOLOv8目标检测算法相较于前几代YOLO系列算法具有如下的几点优势&#xff1a; 更友好的安装/运行方式速度更快、准确率更高新的backbone&#xff0c;将YOLOv5中的C3更换为C2FYOLO系列第一次尝试使用anchor-free新的损失函数 YOLOv8简介 YOLOv8 是 Ultralytics 公司继 YOLOv5…

Android学习之路(2) 设置视图

一、设置视图宽高 ​ 在Android开发中&#xff0c;可以使用LayoutParams类来设置视图&#xff08;View&#xff09;的宽度和高度。LayoutParams是一个用于布局的参数类&#xff0c;用于指定视图在父容器中的位置和大小。 ​ 下面是设置视图宽度和高度的示例代码&#xff1a; …

【算法——双指针】LeetCode 283 移动零

题目描述&#xff1a; 思路&#xff1a; (双指针) O(n)O(n)O(n) 给定一个数组 nums&#xff0c;要求我们将所有的 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 如图所示&#xff0c;数组nums [0,1,0,3,12]&#xff0c;移动完成后变成nums [1,3,12,0,0] &am…

【探索SpringCloud】服务发现-Nacos使用

前言 在聊服务注册中心时&#xff0c;便提到了Nacos。这次便来认识一下。当然&#xff0c;这自然没有官方介绍那般详尽&#xff0c;权当是学习了解Nacos原理的一个过程吧。 Nacos简介 Nacos&#xff0c;全名&#xff1a;dynamic Naming And Configuration Service. 而这个名…

静态网页和动态网页区别

1&#xff0c;静态网页和动态网页有何区别 1) 更新和维护 静态网页内容一经发布到网站服务器上&#xff0c;无论是否有用户访问&#xff0c;这些网页内容都是保存在网站服务器上的。如果要修改网页的内容&#xff0c;就必须修改其源文件&#xff0c;然后重新上传到服务器上。…

SpringBoot框架

一、SpringBoot概述 1. 简介 springboot是spring家族中的一个全新框架&#xff0c;用来简化spring程序的创建和开发过程。在以往我们通过SpringMVCSpringMybatis框架进行开发的时候&#xff0c;我们需要配置web.xml&#xff0c;spring配置&#xff0c;mybatis配置&#xff0c;…

【算法训练营】队列合集(2) 2073. 买票需要的时间 || 面试题 03.04. 化栈为队 ||

&#x1f4cd;前言 本篇将学习queue的OJ题&#xff0c;每一题的标题都是超链接哦&#xff0c;我会将queue的基础知识放到最后供你参考~ &#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 &#x1f618;欢迎关注&#xff1a;&am…

【LVS-NAT配置】

配置 node1&#xff1a;128&#xff08;客户端&#xff09; node2&#xff1a;135&#xff08;调度器&#xff09; RS&#xff1a; node3&#xff1a;130 node4&#xff1a;132 node2添加网络适配器&#xff08;仅主机模式&#xff09; [rootnode2 ~]# nmtui[rootnode2 ~]#…

棒球在国际上的流行·棒球1号位

棒球在国际上的流行 1. 棒球的起源与历史 棒球的起源源于美国。19世纪中叶&#xff0c;由于美国领土的扩张&#xff0c;当时的美国殖民地的印第安人将棒球类游戏&#xff0c;带到了当时的弗吉尼亚州的奥克兰。后来&#xff0c;棒球运动流传到了加利福尼亚州的圣迭戈。早期的棒…

Pyqt5使QTextEdit或QLabel等框框背景透明

设置&#xff1a;textEdit->setStyleSheet(“background-color: rgb(255, 255, 255, 60);”);

登录验证两种方案:token和cookie以及对比

cookie HTTP无状态&#xff0c;每次请求都要携带cookie&#xff0c;以帮助识别用户身份&#xff1b; 服务端也可以向客户端set-cookie&#xff0c;cookie大小限制为4kb&#xff1b; cookie默认有跨域限制&#xff0c;不跨域共享和传递&#xff0c;例如&#xff1a; 现代浏览…

7.4.tensorRT高级(2)-使用RAII接口模式对代码进行有效封装

目录 前言1. RAII接口模式2. 问答环节总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 高级-使用 RAII 接口模式对…

实战:ros机器人运行不稳定,也许是use_sim_time没有设置对

搞机环境&#xff0c;ubuntu 20.04 ros2 版本 foxy ros机器人搞了很久了&#xff0c;但是有一个初学者很容易忽略的参数&#xff1a;use_sim_time&#xff0c;设置不对&#xff0c;会让程序出跑起来有莫名其妙的问题。 use_sim_time &#xff1a;直白翻译&#xff1a; 用_仿…