【二叉树进阶】二叉搜索树

news2024/11/15 10:14:40

目录

1. 二叉搜索树概念

2. 二叉搜索树的实现

2.1 创建二叉搜索树节点

2.2 创建实现二叉搜索树

2.3 二叉搜索树的查找

2.4 二叉搜索树的插入

2.5 二叉搜索树的删除

2.6 中序遍历

2.7 完整代码加测试

3. 二叉搜索树的应用

3.1 K模型:

3.2 KV模型:

4. 二叉搜索树的性能分析


1. 二叉搜索树概念

二叉搜索树(BST,Binary Search Tree):可以是一颗空树,满足以下性质:

  • 根节点的值大于左子树上所有节点的值,小于右子树上所有节点的值。
  • 它的左子树和右子树也分别为二叉搜索树。

它的中序遍历是有序的:0 1 2 3 4 5 6 7 8 9 ,所以也称为二叉排序树或二叉查找树。

2. 二叉搜索树的实现

int a[ ] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };

2.1 创建二叉搜索树节点

template<class K>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;
	//初始化节点
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

2.2 创建实现二叉搜索树

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
    //操作实现
    //查找
    bool Find(const K& key) { }
    //插入
    void Insert(const K& key) { }
    //删除
    bool Erase() { }
    //中序遍历
    void InOrder() { }
private:
    Node* _root = nullptr;
}

2.3 二叉搜索树的查找

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

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

bool Find(const K& key)
{
	if (_root == nullptr)
	{
		return false;
	}
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_left)
		{
			cur = cur->_left;
		}
		else if (key > cur->_right)
		{
			cur = cur->_right;
		}
		else
		{
			return true;
		}
	}
	return false;
}

2.4 二叉搜索树的插入

a、如果树为空,新增节点,赋值给_root指针。

b、按搜索二叉树性质查找插入位置,插入节点。

c、要插入的位置一定为为空的位置,所以要插入,还要找到要插入位置的父节点,让父节点指向新插入的节点。

bool Insert(const K& key)
{
	//如果树为空,新增节点
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	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 = new Node(key);
	if (key < parent->_key)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	return true;
}

2.5 二叉搜索树的删除

a、先判断要删除的元素是否存在,不存在,返回false。

b、若存在,则分为以下情况:

  1. 要删除的节点(叶子节点)左右子树为空
  2. 要删除的节点左子树为空或右子树为空
  3. 要删除的节点左子树右子树都不为空

实际操作起来,情况1和情况2能合并到一起实现,只有两种情况:

  • 要删除的节点的左子树或右子树为空:直接删除-----让父亲指向要删除节点右子树(左子树),再直接删除该节点。

如果要删除的该节点为根,则直接让它的右子树(左子树)赋值给_root让它成为新根即可。

  • 要删除的节点的左右子树都不为空:替换法删除----在它的右子树中找到最小值(最左节点)或在左子树中找到最大值(最右节点),将它的值与要删除节点的值替换,这时就转换为该节点的删除问题。

右子树最左节点的左子树一定为空,左子树最右节点的右子树一定为空,这就转换为第一种情况的删除了。

	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//查找
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			//找到了 删除
			else
			{
				//1.要删除节点的左子树为空或者右子树为空
				if (cur->_left == 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 == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
				}
				//要删除的左右子树都不为空
				else
				{
					//这里找到是右子树的最左值
					Node* rightMinParent = cur;
					Node* rightMin = cur->_right;//要替换的节点
					while (rightMin->_left)
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}
					//替换法交换
					cur->_key = rightMin->_key;
					//转换成删除rightMin
					if (rightMin == rightMinParent->_right)
					{
						//rightMin的左子树一定为空
						rightMinParent->_right = rightMin->_right;
					}
					else
					{
						rightMinParent->_left = rightMin->_right;
					}
					delete rightMin;
				}
				//这里我们去找左子树的最大值
				//else
				//{
				//	Node* leftMaxParent = cur;
				//	Node* leftMax = cur->_left;
				//	while (leftMax->_right)
				//	{
				//		leftMaxParent = leftMax;
				//		leftMax = leftMax->_right;
				//	}
				//	//替换法删除
				//	cur->_key = leftMax->_key;
				//	//转换为删除leftMax
				//	if (leftMax == leftMaxParent->_left)
				//	{
				//		leftMaxParent->_left = leftMax->_left;
				//	}
				//	else
				//	{
				//		leftMaxParent->_right = leftMax->_left;
				//	}
				//	delete leftMax;
				//}
				return true;
			}
		}
		return false;
	}

2.6 中序遍历

中序遍历:按照左子树 根 右子树的方式迭代遍历二叉树。

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

总之,我们在实现二叉搜索树的时候一定要考虑多种情况,每种情况也要多找几个节点进行分析。

2.7 完整代码加测试

BSTree.hpp:

//创建二叉树节点
template<class K>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;
	//初始化节点
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};
//创建实现二叉树
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		//如果树为空,新增节点
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		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 = new Node(key);
		if (key < parent->_key)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		return true;
	}
	//中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//查找
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			//找到了 删除
			else
			{
				//1.要删除节点的左子树为空或者右子树为空
				if (cur->_left == 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 == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
				}
				//要删除的左右子树都不为空
				else
				{
					//这里找到是右子树的最左值
					Node* rightMinParent = cur;
					Node* rightMin = cur->_right;//要替换的节点
					while (rightMin->_left)
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}
					//替换法交换
					cur->_key = rightMin->_key;
					//转换成删除rightMin
					if (rightMin == rightMinParent->_right)
					{
						//rightMin的左子树一定为空
						rightMinParent->_right = rightMin->_right;
					}
					else
					{
						rightMinParent->_left = rightMin->_right;
					}
					delete rightMin;
				}
				//这里我们去找左子树的最大值
				//else
				//{
				//	Node* leftMaxParent = cur;
				//	Node* leftMax = cur->_left;
				//	while (leftMax->_right)
				//	{
				//		leftMaxParent = leftMax;
				//		leftMax = leftMax->_right;
				//	}
				//	//替换法删除
				//	cur->_key = leftMax->_key;
				//	//转换为删除leftMax
				//	if (leftMax == leftMaxParent->_left)
				//	{
				//		leftMaxParent->_left = leftMax->_left;
				//	}
				//	else
				//	{
				//		leftMaxParent->_right = leftMax->_left;
				//	}
				//	delete leftMax;
				//}
				return true;
			}
		}
		return false;
	}
	bool Find(const K& key)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_left)
			{
				cur = cur->_left;
			}
			else if (key > cur->_right)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}
private:
	Node* _root = nullptr;
};
void TestBSTree()
{
	BSTree<int> t;
	int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	for (auto e : a)
	{
		t.Erase(e);
		t.InOrder();
	}
}

Test.cpp:

#include"BSTree.hpp"
int main()
{
	TestBSTree();
	return 0;
}

运行结果:

3. 二叉搜索树的应用

3.1 K模型:

K模型只有key作为关键码,结构中只需要存储key即可,关键码就是要搜索的值。

比如:给一个单词,判断该单词是否正确:

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

其实就是上面我们实现的二叉搜索树的查找。这里不在重复实现了。

3.2 KV模型:

每一个关键码Key,都有与子对应的Value,即<Key,Value>的键值对。

  • 比如:英汉词典就是英文与中文的对应关系,通过英文可以快速找到对应中文,英文单词与其中文构成<world,chinese>就构成一种键值对;
  • 比如:统计单词次数,统计成功后,给定单词可以快速找到其出现次数,单词与其出现次数<world,count>就构成一种键值对。

也很简单,其实就是让二叉搜索树中的节点多存一个值Value,查找、插入、删除等操作依旧是按照key去实现的。

改造二叉搜索树为KV结构模型:

template<class K,class V>
struct BSTreeNode
{
	BSTreeNode<K,V>* _left;
	BSTreeNode<K,V>* _right;
	K _key;
	V _value;//多存一个值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)
	{ }
	void _InOrder(Node* root)
	{ }
	void InOrder()
	{ }
	bool Erase(const K& key)
	{ }
	Node* Find(const K& key)//返回节点的指针,这里不再实现
	{ }
private:
	Node* _root = nullptr;
};

应用测试1:输入英文查找对应的中文

void TestBSTree()
{
	BSTree<string, string> dict;
	dict.Insert("string", "字符串");
	dict.Insert("tree", "树");
	dict.Insert("int", "整型");
	dict.Insert("search", "查找");
	dict.Insert("insert", "插入");
	string str;
	while (cin >> str)
	{
		BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret == nullptr)
		{
			cout << "词库中无此单词" << endl;
		}
		cout << ret->_value << endl;
	}
}

结果:

应用测试2:查找水果出现的次数

void TestBSTree()
{
	string arr[] = { "苹果","香蕉","西瓜","西瓜","香梨","西瓜" ,"苹果" ,"西瓜" ,"西瓜" ,"香蕉" ,"苹果" };
	BSTree<string, int> t;
	int i = 0;
	for (auto e : arr)
	{
		BSTreeNode<string, int>* ret = t.Find(e);
		//ret为空说明要查找的是第一次出现
		if (ret == nullptr)
		{
			t.Insert(e, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	t.InOrder();
}

结果:

4. 二叉搜索树的性能分析

插入、删除操作都必须先查找,所有查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同(无序、接近有序),可能得到不同结构的二叉搜索树:

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log2N(以2为底N的对数)。
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N。
如果为单支树,二叉搜索树的性能就没有了,那么这种情况就要用到AVL树和红黑树了,后续再讲解。

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

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

相关文章

【C++】入门基础(下)

Hi&#xff01;很高兴见到你~ 目录 7、引用 7.3 引用的使用&#xff08;实例&#xff09; 7.4 const引用 【第一分点】 【第二分点1】 【第二分点2】 7.5 指针和引用的关系&#xff08;面试点&#xff09; 8、inline 9、nullptr Relaxing Time&#xff01; ———…

asp.net core调用wps实现word转pdf的方法

1&#xff0c;首先安装wps&#xff0c;从官网下载安装包 2&#xff0c;创建.net core控制项目 添加com引用&#xff0c;搜索wps 准备一个word文档&#xff0c;名字叫001.docx&#xff0c;随便编写一些文字内容 3&#xff0c;word转pdf 编写代码 namespace WPSStu01 {inter…

MYSQL数据库基础篇——DDL

DDL&#xff1a;DDL是数据定义语言&#xff0c;用来定义数据库对象。 一.DDL操作数据库 1.查询 ①查询所有数据库 输入&#xff1b; 得到结果&#xff1a; ②查询当前数据库 输入&#xff1b; 例如执行下面语句&#xff1a; 2.创建 输入 然后展示数据库即可得到结果&…

Linux学习之路 - 线程概念补充理解

前面介绍了线程的基本概念&#xff0c;下面介绍一下线程在内核中的存在方式。 在OS中&#xff0c;存在着大量的线程&#xff0c;为了管理这些线程&#xff0c;我们必然需要用结构体对其进行描述&#xff0c;然后再组织起来管理。但是在linux中&#xff0c;实际上是不存在线程这…

QT 事件 Event 应用

文章目录 一、事件处理过程1. QT 事件应用介绍2. 事件分发过程 二、重写事件案例1. 鼠标事件2. 自定义按钮事件 一、事件处理过程 1. QT 事件应用介绍 众所周知Qt是一个基于C的框架&#xff0c;主要用来开发带窗口的应用程序&#xff08;不带窗口的也行&#xff0c;但不是主流…

【C++】 —— string的使用

前言 string类虽然不在STL的容器中&#xff0c;但string类十分重要&#xff0c;string类是对字符串的存储和相关操作。 basic_string std::basic_string类是C的一个模版类&#xff0c;它支持多种字符类型。 char &#xff1a;用于表示一个字节的字符&#xff0c;使用ASCII编码。…

揭开谜底:用 C 语言打造你的扫雷游戏!

目录 1. 功能概述 用户界面 2. 游戏分析与设计 2.1 数据结构分析 地雷存储&#xff1a; 玩家视图&#xff1a; 2.2 文件结构设计 3. 代码实现 game.h game.c test.c 亮点功能与创新 智慧的较量&#xff1a;核心游戏循环 进阶功能&#xff1a;让游戏更加与众不同 还…

MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例

目录 深入理解 JavaScript 中的 MutationObserver&#xff1a;原理与实战案例 一、MutationObserver 简介 二、MutationObserver 的工作原理 1、基本用法 2、observe 方法的配置项 三、实战案例 案例 1&#xff1a;监控动态内容加载 案例 2&#xff1a;监控属性变化 案…

如何处理模型API速率限制

引言 当我们访问大模型相关的API服务时&#xff0c;通常会遇到速率限制(即限流)&#xff0c;它用于防止用户向某个API发送大量请求&#xff0c;防止请求过载&#xff0c;确保每个人都能公平地访问API。 速率限制的方式 速率限制通常有以下几种形式&#xff1a; RPM(request…

连续时间,离散频率 傅里叶

时域周期——不是把一个信号周期化&#xff0c;而是周期信号取一个周期是x(t),对其周期化不会发生时域的重叠。故当接收到信号&#xff0c;在DFT时&#xff0c;以整个接收到的时间信号为周期进行延拓 推导公式时思路&#xff1a;时域卷积周期冲击&#xff0c;用傅里叶变换推导出…

一键智能改写文章,快速提升内容的吸引力

在这个信息如潮水般涌来的时代&#xff0c;优质的内容创作成为了吸引眼球、传递价值的关键。而有时候&#xff0c;我们可能会面临着已有文章需要优化、旧内容需要焕发新活力的情况。此时&#xff0c;一键智能改写文章的工具就如同一位神奇的魔法师&#xff0c;它能帮助我们将文…

基于深度学习的图像分类或识别系统(含全套项目+PyQt5界面)

目录 一、项目界面 二、代码实现 1、网络代码 2、训练代码 3、评估代码 4、结果显示 三、项目代码 一、项目界面 二、代码实现 1、网络代码 该网络基于残差模型修改 import torch import torch.nn as nn import torchvision.models as modelsclass resnet18(nn.Modul…

【C语言】(指针系列2)指针运算+指针与数组的关系+二级指针+指针数组+《剑指offer面试题》

前言&#xff1a;开始之前先感谢一位大佬&#xff0c;清风~徐~来-CSDN博客&#xff0c;由于是时间久远&#xff0c;博主指针的系列忘的差不多了&#xff0c;所以有些部分借鉴了该播主的&#xff0c;有些地方如果解释的不到位&#xff0c;请翻看这位大佬的&#xff0c;感谢大家&…

C++ char*和char[] 可能指向的内存区域详解(附实验)

C char* 指向的内存区域详解 写在前面c内存结构简介指针常量和常量指针简介情况一&#xff1a;char* 指向栈区内容情况二&#xff1a;char* 指向堆区内容情况三&#xff1a;char* 指向常量区内容情况四&#xff1a;char* 指向静态区内容情况五&#xff1a;char* 指向全局区内容…

Scratch游戏-史诗忍者7免费下载

小虎鲸Scratch资源站-免费少儿编程Scratch作品源码,素材,教程分享网站! 作品描述&#xff1a; 在Scratch版本的《史诗忍者7》中&#xff0c;你需要穿越关卡&#xff0c;击败敌人并收集33个水果。通过灵活的操作和精准的攻击&#xff0c;逐步闯过重重难关。游戏中提供了丰富的技…

【GESP】C++一级练习BCQM3005,基本输出语句printf

一道基础练习题&#xff0c;练习基本输出语句printf。 BCQM3005 题目要求 描述 输出表达式1234∗5678的结果。 输入 无 输出 1234∗56787006652 输入样例 无 输出样例 1234 * 5678 7006652 全文详见个人独立博客&#xff1a;https://www.coderli.com/gesp-1-bcqm3005/ 【…

使用 SuperCraft AI 设计书橱模型的指南

在现代家居设计中&#xff0c;书橱不仅是存放书籍的地方&#xff0c;更是展示个人品味和风格的重要家具。借助 SuperCraft AI&#xff0c;你可以轻松设计出独一无二的书橱。以下是详细的步骤指南&#xff0c;帮助你从零开始设计一个理想的书橱。 1. 创建项目 首先&#xff0c…

【探索数据结构与算法】插入排序:原理、实现与分析(图文详解)

目录 一、插入排序 算法思想 二、插入排序 算法步骤 四、复杂度分析 时间复杂度&#xff1a;O(n^2) 空间复杂度&#xff1a;O(1) 稳定性&#xff1a;稳定算法 五、应用场景 &#x1f493; 博客主页&#xff1a;C-SDN花园GGbond ⏩ 文章专栏&#xff1a;探索数据结构…

node卸载流程

步骤&#xff1a; 1.开始中搜素”命令提示符“&#xff0c;并将其以”管理员身份运行“ 在弹出的框中输入cmd&#xff0c;并确认进入”命令提示符“ 2.在里面通过npm config list查看node相关文件路径&#xff0c; 并找到config from与prefix &#xff0c;后面对应的路径&…

element-plus的菜单组件el-menu

菜单是几乎是每个管理系统的软件系统中不可或缺的&#xff0c;element-plus提供的菜单组件可以快速完成大部分的菜单的需求开发&#xff0c; 该组件内置和vue-router的集成&#xff0c;使用起来很方便。 主要组件如下 el-menu 顶级菜单组件 主要属性 mode:决定菜单的展示模式…