【数据结构专栏】二叉搜索树(Binary Search Tree)的剖析?

news2024/11/25 2:56:54

在这里插入图片描述

文章目录

  • 🧨前言
    • 1、二叉搜索树的基本概念?
      • 2、二叉搜索树的节点结构组成?
        • 3、二叉搜索树的插入操作?
          • 4、二叉搜索树的删除操作?
            • 5、二叉搜索树的遍历?
          • 6、二叉搜索树的性能分析?
        • 🎉完整代码如下:

🧨前言

该章节主要就是明确二叉树是什么?二叉树的基本操作等。

1、二叉搜索树的基本概念?

二叉所搜索树:它是一棵有一定规则的二叉树。它的每个节点的都遵循这两个条件:
①左子树不为空的时候,左子树的所有节点的值都小于当前节点的值。
②右子树不为空的时候,右子树的所有节点的值都大于当前节点的值。
③左右子树也必须是二叉搜索树。

二叉搜索树的性质决定了节点之间的连接方式,也就是说它可能会连接长满二叉树,亦可能为完全二叉树,但是在插入的而数据是有序的时候,那连接形式就会成一棵链表,会导致该树的高度非常高,影响操作效率。但是有一种平衡二叉搜索树,比如AVL树或红黑树,可以解决这个问题,但这里先不考虑平衡,先假设是一棵普通的二叉搜索树进行讲述。

2、二叉搜索树的节点结构组成?

⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体看使⽤场景定义,map/set/multimap/multiset系列容器底层是⼆叉搜索树,其中map/set不⽀持插⼊相等
值,multimap/multiset⽀持插⼊相等值。
此文章的前提是使用一个普通二叉搜索树来分析,同时不包含重复值的节点
该树的节点组成有:
_key:用于存储数据值的变量。
_left:指向左子树的变量
_right:指向右子树的变量

代码结构如下:

template<class K>
class BSTNode
{
public:
	//构造函数,初始化列表给变量赋值
	BSTNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{}
public:
	K _key;
	BSTNode<K>* _left;
	BSTNode<K>* _right;
};
3、二叉搜索树的插入操作?

插入‌:如果树为空,新节点成为根节点;否则,按照二叉搜索树的性质找到插入位置并插入新节点‌。
从根节点开始,比较目标值与当前节点的值:
若目标值较小,则移动到左子树;
若目标值较大,则移动到右子树。
重复上述过程,直到找到一个空位置插入新节点。
代码加注释讲解:

//插入数据函数。
bool Insert(const K& key)
{
	//首先,为空的时候,直接插入到根节点。
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	//根节点不为空的时候,那就先从根节点开始。
	Node* cur = _root;
	Node* parent = nullptr;
	//直到为空就插入
	while (cur)
	{
		//若插入的数据值 小于 当前节点的数据值,往左走。
		if (key < cur->_key)
		{
			//临时记录当前节点的父节点,不然后面链接不上新节点。
			parent = cur;
			cur = cur->_left;
		}
		//插入的数据值 大于 当前节点的数据值,往右走。
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			//走到此处说明找到一个空位置,跳出循环进行链接。
			return false;
		}
	}
	//找到cur==nullptr,此时parent节点就是最后一个节点,在插入key时候,
	//还需要判断一次,看插入的值比父节点值大,那么就当作右孩子,反之就当作左孩子。
	//由于此时cur是为空的,没必要另开空间来存储新节点,因此就把key放入该节点中
	cur = new Node(key);
	if (key < parent->_key)
	{
		//插入到左边
		parent->_left = cur;
	}
	else
	{
		//插入到右边
		parent->_right = cur;
	}
	return true;
}
4、二叉搜索树的删除操作?

在实现二叉搜索树删除的接口的时候先是来说一下查找函数。
二叉搜索树的查找:思路和插入的一样,只是需要比较一下大小。

①通过需要查找的值key和二叉搜索树里面的每个节点的值cur->_key进行比较。查找值key 大于 当前节点值cur->_key,则就往当前节点cur的右子树走。
②查找值key 小于 当前节点值cur->_key,则就往当前节点cur的左子树走。
③找到后就返回当前节点cur

代码如下:

	//查找函数
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				//往左走
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;//在判断是否存在某个值的时候,直接返回true即可。
			}
		}
		return false;//说明没找到。
	}

删除操作:

  1. 要删除结点N左右孩⼦均为空。即叶子节点,可以直接删除。
  2. 要删除的结点N左孩⼦位空右孩⼦结点不为空
  3. 要删除的结点N右孩⼦位空左孩⼦结点不为空
  4. 要删除的结点N左右孩⼦结点均不为空

对应序号的解决策略:

  1. 把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)。
  2. 把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点。
  3. 把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点。
  4. ⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替换法删除。

第4种情况替换如下:
①先找到右子树的最小值节点(或左子树的最大值节点)。
②再用该最小值(或最大值)替换被删除节点的值。
③最后记得删除找到的最小值(或最大值)节点。

	//删除函数
	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、右位空,左不为空;4、左右都不为空
				// 左右为空的都可以划分到其中一个为空的情况,因为当左右都为空的时候,
				//下面检测到左节点为空,则就会进入左为空的程序
				
				//左为空
				if (cur->_left == nullptr)
				{
					//先判断是否为根节点
					if (cur == _root)
					{
						//直接把节点给到根节点
						_root = cur->_right;
					}
					else if (cur == parent->_left)//若是父节点的左孩子为空,则
					//把cur->_right给到父节点的左孩子
					{
						parent->_left = cur->_right;
					}
					else//反之就是把父节点的右孩子
					{
						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
					{
						parent->_right = cur->_left;
					}
					delete cur;
				}

				//左右都不为空
				else
				{
					//替代法
					Node* replaceParent = cur;
					//在删除左右都不为空的时候,需要找一个节点的值来替代,可以
					//是当前节点的右子树的最左节点,也可以是当前节点左子树的最右节点,
					//下面是用的右子树的最左节点。
					Node* replace = cur->_right;
					while (replace->_left)
					{
						replaceParent = replace;
						replace = replace->_left;
					}
					//此处replace就是右子树的最左孩子节点
					//把值给到要删除的节点,即替换
					cur->_key = replace->_key;
					//此时需要判断replace节点是replaceParent节点的左孩子还是右孩子
					if (replace == replaceParent->_left)
					{
						replaceParent->_left = replace->_right;
					}
					else
					{
						replaceParent->_right = replace->_right;
					}
					delete replace;//相当于替换后,就删除替换的那个节点
				}
				return true;
			}
		}
		//没找到就返回false
		return false;
	}
5、二叉搜索树的遍历?

中序遍历:对于二叉搜索树,中序遍历会按升序输出所有节点的值。
前序遍历、后序遍历:按照特定顺序访问节点,但不保证有序性。

下面把三种遍历方式都写出来:

//中序遍历搜索二叉树
void _InOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}

	//左 根 右
	_InOrder(root->_left);
	cout << root->_key<<" ";
	_InOrder(root->_right);
}

void _PrevOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	 cout << root->_key << " ";
	_InOrder(root->_left);
	_InOrder(root->_right);
}

void _PostOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	_InOrder(root->_left);
	_InOrder(root->_right);	
	cout << root->_key << " ";
}

运行结果如下:
在这里插入图片描述

6、二叉搜索树的性能分析?

最好和平均情况:树的高度为O(log n),插入、删除和查找操作的时间复杂度为O(log n)

最坏情况:树的高度为O(n),时间复杂度退化为O(n),通常发生在插入有序数据时,树退化为链表

为避免性能下降,通常使用平衡二叉搜索树,如AVL树或红黑树,以保证树的高度始终接近O(log n)。后续文章会讲述。

总结:二叉搜索树是一种高效的数据结构,适用于需要频繁进行插入、删除和查找操作的场景。理解其基本操作和实现细节是掌握更复杂数据结构和算法的基础。在实际应用中,通常需要结合平衡技术来保证性能的稳定性。

🎉完整代码如下:
#pragma once

#include<iostream>
using namespace std;

namespace N
{
	//先创建二叉树结构
	template<class K>
	class BSTNode
	{
	public:
		//构造函数
		BSTNode(const K& key)
			:_key(key)
			,_left(nullptr)
			,_right(nullptr)
		{}
	public:
		K _key;
		BSTNode<K>* _left;
		BSTNode<K>* _right;
	};

	template<class K>
	class BSTree
	{
		typedef BSTNode<K> Node;
	public:
		//插入数据函数
		bool Insert(const K& key)
		{
			//首先,为空的时候,直接插入到根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			//根节点不为空的时候,那就先从根节点开始
			Node* cur = _root;
			Node* parent = nullptr;
			//直到为空就插入
			while (cur)
			{
				//若插入的数据小于节点的数据值,往左走
				if (key < cur->_key)
				{
					//记录下一节点的父节点
					parent = cur;
					cur = cur->_left;
				}
				//大于节点的值
				else if (key > cur->_key)
				{
					parent = cur;
					//往右边走
					cur = cur->_right;
				}
				//相等就返回
				else
				{
					return false;
				}
			}
			//找到cur==nullptr,此时parent节点就是最后一个节点,在插入key时候,
			//还需要判断一次,比父节点值大,就插入到右孩子处,反之就左孩子
			//由于cur是为空的,因此把key放入该节点中
			cur = new Node(key);
			if (key < parent->_key)
			{
				//插入到左边
				parent->_left = cur;
			}
			else
			{
				//插入到右边
				parent->_right = cur;
			}
			return true;
		}

		//查找函数
		bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					//往左走
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				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、右位空,左不为空;4、左右都不为空
					// 左右为空的都可以划分到其中一个为空的情况,因为当左右都为空的时候,
					//下面检测到左节点为空,则就会进入左为空的程序
					
					//左为空
					if (cur->_left == nullptr)
					{
						//先判断是否为根节点
						if (cur == _root)
						{
							//直接把节点给到根节点
							_root = cur->_right;
						}
						else if (cur == parent->_left)//若是父节点的左孩子为空,
						//则把cur->_right给到父节点的左孩子
						{
							parent->_left = cur->_right;
						}
						else//反之就是把父节点的右孩子
						{
							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
						{
							parent->_right = cur->_left;
						}
						delete cur;
					}

					//左右都不为空
					else
					{
						//替代法
						Node* replaceParent = cur;
						//在删除左右都不为空的时候,需要找一个节点的值来替代,
						//可以是当前节点的右子树的最左节点
						Node* replace = cur->_right;
						while (replace->_left)
						{
							replaceParent = replace;
							replace = replace->_left;
						}
						//此处replace就是右子树的最左孩子节点
						//把值给到要删除的节点,即替换
						cur->_key = replace->_key;
						//此时需要判断replace节点是replaceParent节点的左孩子还是右孩子
						if (replace == replaceParent->_left)
						{
							replaceParent->_left = replace->_right;
						}
						else
						{
							replaceParent->_right = replace->_right;
						}
						delete replace;//相当于替换后,就删除替换的那个节点
					}
					return true;
				}
			}
			//没找到就返回false
			return false;
		}

		void InOrder()
		{
			_InOrder(_root);
		}

		void PrevOrder()
		{
			_PrevOrder(_root);
		}

		void PostOrder()
		{
			_PostOrder(_root);
		}
	private:
		//中序遍历搜索二叉树
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			//左 根 右
			_InOrder(root->_left);
			cout << root->_key<<" ";
			_InOrder(root->_right);
		}

		void _PrevOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			 cout << root->_key << " ";
			_InOrder(root->_left);
			_InOrder(root->_right);
		}

		void _PostOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_InOrder(root->_left);
			_InOrder(root->_right);	
			cout << root->_key << " ";
		}
	private:
		//BSTNode<K>* _root=nullptr;//定义根节点
		Node* _root = nullptr;
	};
}

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

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

相关文章

FastApi学习第三天:两表联查

两表联查 在 FastAPI 中&#xff0c;使用 Tortoise ORM 查询两表联查&#xff08;通常是通过外键关系进行联接&#xff09;是非常简单的。可以使用 select_related 或 prefetch_related 来执行联表查询&#xff0c;它们类似于 Django ORM 的 select_related 和 prefetch_relate…

Redis原理及应用

Redis简介 Redis是开源的&#xff08;BSD许可&#xff09;&#xff0c;数据结构存储于内存中&#xff0c;被用来作为数据库&#xff0c;缓存和消息代理。它支持多种数据结构&#xff0c;例如&#xff1a;字符串&#xff08;string&#xff09;&#xff0c;哈希&#xff08;hash…

Unity类银河战士恶魔城学习总结(P141 Finalising ToolTip优化UI显示)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ UI部分暂时完结&#xff01;&#xff01;&#xff01; 本章节优化了UI中物品描述的显示效果&#xff0c;技能描述的显示效果 并且可以批…

oracle的静态注册和动态注册

oracle的静态注册和动态注册 静态注册&#xff1a; 静态注册 : 指将实例的相关信息手动告知 listener 侦 听 器 &#xff0c; 可以使用netmgr,netca,oem 以及直接 vi listener.ora 文件来实现静态注册&#xff0c;在动态注册不稳定时使用&#xff0c;特点是&#xff1a;稳定&…

社交电商专业赋能高校教育与产业协同发展:定制开发AI智能名片及2+1链动商城小程序的创新驱动

摘要&#xff1a;本文围绕社交电商有望成为高校常态专业这一趋势展开深入探讨&#xff0c;剖析国家政策认可下其学科发展前景&#xff0c;着重阐述在专业建设进程中面临的师资短缺及实践教学难题。通过引入定制开发AI智能名片与21链动商城小程序&#xff0c;探究如何借助这些新…

数据指标与标签在数据分析中的关系与应用

导读&#xff1a;分享数据指标体系的文章很多&#xff0c;但讲数据标签的文章很少。实际上&#xff0c;标签和指标一样&#xff0c;是数据分析的左膀右臂&#xff0c;两者同样重要。实际上&#xff0c;很多人分析不深入&#xff0c;就是因为缺少对标签的应用。今天系统的讲解下…

使用Electron将vue2项目打包为桌面exe安装包

目录 一、下载electron模板项目 【electron-quick-start】​ 二、打开项目&#xff0c;安装所有依赖 三、在打exe包的时候报错是因为没有&#xff0c;需要检查并安装之后重新打包&#xff1b; 四、经过这么疯狂的一波操作之后&#xff0c;就可以打包出你想要的exe安装包&am…

MySQL基础大全(看这一篇足够!!!)

文章目录 前言一、初识MySQL1.1 数据库基础1.2 数据库技术构成1.2.1 数据库系统1.2.2 SQL语言1.2.3 数据库访问接口 1.3 什么是MySQL 二、数据库的基本操作2.1 数据库创建和删除2.2 数据库存储引擎2.2.1 MySQL存储引擎简介2.2.2 InnoDB存储引擎2.2.3 MyISAM存储引擎2.2.4 存储引…

Linux之NFS共享文件操作

一、注意点 以下操作使用root用户 代理端需要访问服务端的2049、111端口二、nfs下载 # 服务端和代理端都要安装 yum –y install rpcbind yum –y install nfs-utils三、配置共享目录-【服务端】 *修改/etc/exports文件&#xff0c;追加以下内容 /home/app_adm/test ip1(in…

C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用-腾讯云开发者社区-腾讯云

C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用-腾讯云开发者社区-腾讯云 C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用 发布于 2021-06-10 00:10:59 7.1K0 举报 文章被收录于专栏&#xff1a;c#学习笔记 一、介绍 DockPanelSuite是托管在…

杰发科技AC7840——EEP中RAM的配置

sample和手册中示例代码的sram区地址定义不一样 这个在RAM中使用没有限制&#xff0c;根据这个表格留下足够空间即可 比如需要4096字节的eep空间&#xff0c;可以把RAM的地址改成E000&#xff0c;即E000-EFFF&#xff0c;共4096bytes即可。

web-03

CSS回顾 选择器 标签选择器 标签{}ID选择器 标签中定义ID属性。 #ID值{}类选择器 标签中使用class属性 .类名{}关于DIV/span div任意的大小的长方形&#xff0c;大小css&#xff1a; width, height控制。—换行 span-- 一行内 CSS常用属性 width/height 宽度/高度 定义&…

CI配置项,IT服务的关键要素

随着现今数字经济的不断发展&#xff0c;逐渐成熟的IT 基础设施已不再是简单的竞争优势&#xff0c;而已成为企业生存和发展的基石。然而&#xff0c;仅仅拥有强大的基础设施是不够的。为了保障 IT 服务的平稳运行和持续交付&#xff0c;企业还需要重点关注 IT 服务的核心构建模…

ApiChain-编写迭代单测用例

项目地址&#xff1a;ApiChain 项目主页 写单测用例&#xff0c;就像画一幅有向不循环的图&#xff0c;图中的每个节点是这个单测用例的每一个步骤&#xff0c;连线代表着数据的流向&#xff0c;这幅图通常有一个或者多个起点&#xff0c;但通常只有一个终点。起点的数据来源于…

九、FOC原理详解

1、FOC简介 FOC&#xff08;field-oriented control&#xff09;为磁场定向控制&#xff0c;又称为矢量控制&#xff08;vectorcontrol&#xff09;&#xff0c;是目前无刷直流电机&#xff08;BLDC&#xff09;和永磁同步电机&#xff08;PMSM&#xff09;高效控制的最佳选择…

企业OA管理系统:Spring Boot技术实现与案例研究

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了企业OA管理系统的开发全过程。通过分析企业OA管理系统管理的不足&#xff0c;创建了一个计算机管理企业OA管理系统的方案。文章介绍了企业OA管理系统的系统分析部…

【tensorflow的安装步骤】

创建一个虚拟环境 conda create -n tensorflow python3.6激活虚拟环境 conda activate tensorflow使用镜像源下载 pip install tensorflow1.15.0 -i https://pypi.tuna.tsinghua.edu.cn/simple/特别特别重要的点&#xff01;&#xff01;&#xff01; 别用WiFi或者校园网下…

网络安全-web架构-nginx配置

1. nginx访问&#xff1a; 访问的是index.html&#xff0c; 访问ip访问的资源就是在/usr/share/nginx/html中&#xff1b; 当nginx不认识&#xff0c;浏览器认识的话&#xff0c;浏览器会自动渲染。 当nginx认识&#xff0c;浏览器不认识的话&#xff0c;浏览器会把它加载成…

ES6 模块化语法

目录 ES6 模块化语法 分别暴露 统一暴露 ​编辑 默认暴露 ES6 模块化引入方式 ES6 模块化语法 模块功能主要由两个命令构成&#xff1a;export 和 import。 ⚫ export 命令用于规定模块的对外接口&#xff08;哪些数据需要暴露&#xff0c;就在数据前面加上关键字即可…

基于Java Springboot高校洗浴管理系统

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…