C++ - 二叉搜索树的基本实现

目录

0. 引言

1. 二叉搜索树

1.1 定义

1.2 特点

2. 二叉搜索树的实现 

2.1 基本框架 

 2.2 查找

2.3 插入 

2.4 删除 

2.4.1 右子树为空

 2.4.2 左子树为空

2.4.3 左右都不为空 

2.4.4 代码

0. 引言

在C语言数据结构中,我们已经基本了解过二叉树,这篇博客分享的二叉搜索树,主要是为了方便学习后面的 map 以及 set 的学习。那么,现在让我们开始吧!

1. 二叉搜索树

1.1 定义

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

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

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

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

这里我们可以看到,将数据存入二叉搜索树中进行查找时,理想情况下只需要 logN 的时间复杂度。这就是 二叉搜索树 名字的由来,搜索(查找)速度很快。

1.2 特点

搜索二叉树的基本特点:左比根小,右比根大

  • 若某个节点的左节点不为空,则左节点的值一定比当前节点的值小,且其左子树的所有节点都比它小;
  • 若某个节点的右节点不为空,则右节点的值一定比当前节点的值大,且其右子树的所有节点都比它大;
  • 二叉搜索树的每一个节点的根,左,右 都满足基本特点。

除此之外,二叉搜索树还有一个特点: 中序遍历的结果为升序

例如,对下面这个搜索二叉树进行中序遍历: 

上述结果为:1  3  4  5  6  7  10  13  14 

由此可见搜素二叉树也具有排序价值,故也称为二叉排序树。 

2. 二叉搜索树的实现 

2.1 基本框架 

我们主要利用C++的类和对象,泛型编程等特点来建立节点的框架,并在此框架的基础上完善各种功能。具体代码如下:

#pragma once

#include <iostream>

//部分展开,避免冲突
using std::cout;	//遍历时需要用到
using std::endl;

//命名空间
namespace LHY
{
	//利用模板,泛型编程
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode(const K& key)
			:_left(nullptr)
			,_right(nullptr)
			,_key(key)
		{}

		//二叉树包含左节点指针、右节点指针、节点值信息
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;
	};

	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	private:
		Node* _root = nullptr;	//二叉搜索树的根
	};
}

这里需要注意的是,二叉搜索树的节点类需要写出构造函数,因为后面创建新节点时会用到;二叉搜索树的根可以给个缺省值 nullptr确保后续不会出错。

 2.2 查找

查找思路较为简单,当查找值比当前值大,往右子树走,当查找值比当前值小时,则往左走,若相等,即为找到。代码如下:

bool Empty() const
{
	return _root == nullptr;
}

bool Find(const K& key) const
{
	//如果为空,则查找失败
	if (Empty())
		return false;

	Node* cur = _root;
	while (cur)
	{
		//如果查找值比当前值大,则往右走
		if (cur->_key < key)
			cur = cur->_right;
		//如果查找值比当前值小,则往左走
		else if (cur->_key > key)
			cur = cur->_left;
		else
			return true;	//找到了
	}

	return false;	//没找到
}

例如,当我们查找 7 时,只需查找 4 次,即可得到结果。

 返回 bool 值是为了表示操作成功或者失败。

2.3 插入 

实现插入操作实际上与查找差不多,插入操作需要先查找合适的位置再进行插入操作。因此我们的思路如下:

  • 先找到合适的位置(满足基本特点)
  • 如果当前位置不为空(冗余),则插入失败
  • 为空则结束循环,进行插入:创建新节点、判断需要插在左边还是右边、链接新节点完成插入

具体代码如下:

bool Insert(const K& key)
{
	//如果为空,则就是第一次插入
	if (Empty())
	{
		_root = new Node(key);
		return true;
	}

	//需要记录父节点
	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
		{
			//出现冗余,插入失败
			return false;
		}
	}

	cur = new Node(key);

	//判断需要链接至左边还是右边
	if (parent->_key < key)
		parent->_right = cur;
	else
		parent->_left = cur;

	return true;
}

二叉搜索树的根为多少,取决于谁第一个插入,后序插入的节点都是基于根节点进行插入的

当找到合适位置时,需要根据当前 key 值与父节点的值进行判断,插入至合适的位置(满足基本特点)。

我们以插入 15 为例子,第一步我们先查找合适的位置

第二步,插入节点:

若查找不到合适的位置,则插入失败。 且代码当前实现的二叉搜索树不允许冗余,如果想要实现冗余的二叉搜索树,可以规定重复的值插在左边或右边,都是可行的。

在确认 新节点的链接位置时,可以通过 parentcur key 值判断,也可以通过原有链接关系判断 如果是通过原有链接判断:parent->_right == cur 需要先创建新节点 new_node(不能覆盖 cur 的值),利用 cur 进行链接判断后,再进行新节点链接 推荐直接使用 key 值判断,省时省力

总结: 

  • 在执行循环查找合适位置前,需要创建变量记录父节点的位置,方便后续进行新节点链接
  • 找到合适位置后,需要将新节点与父节点进行比较判断,确认链接在左边还是右边
  • 插入失败返回 false,插入成功返回 true。

2.4 删除 

删除的思路如下:先利用查找来判断目标值是否存在,如果存在,则进行删除,此时,待删除的节点可能会存在多种情况,需要具体问题具体分析,如果不存在,则删除失败。下面我们依次来看删除的各种可能性。

2.4.1 右子树为空

当右子树为空时,只 需要将其左子树与父节点进行判断链接即可,无论其左子树是否为空,都可以链接,链接完成后,删除目标节点。

 

 2.4.2 左子树为空

同理,左子树为空时,将其右子树与父节点进行判断链接,链接完成后删除目标节点。 

 

2.4.3 左右都不为空 

当左右都不为空时,就有点麻烦了,需要找到一个合适的值(即 > 左子树所有节点的值,又 < 右子树所有节点的值),确保符合二叉搜索树的基本特点。符合条件的值有:左子树的最右节点(左子树中最大的)、右子树的最左节点(右子树中最小的),将这两个值中的任意一个覆盖待删除节点的值,都能确保符合要求。

解释: 为什么找 左子树的最右节点或右子树的最左节点的值覆盖 可以符合要求?因为左子树的最右节点是左子树中最大的值,> 左子树所有节点(除了自己),< 右子树所有节点,右子树的最左节点也是如此,都能符合要求。

2.4.4 代码
bool Erase(const K& key)
{
	if (Empty())
		return false;

	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->_right == nullptr)
			{
				//右为空,考虑将左子树链接
				if (cur == _root)
					_root = cur->_left;
				else
				{
					if (parent->_right == cur)
						parent->_right = cur->_left;
					else
						parent->_left = cur->_left;
				}

				delete cur;
			}
			else if (cur->_left == nullptr)
			{
				//左为空,考虑将右子树链接
				if (cur == _root)
					_root = cur->_right;
				else
				{
					if (parent->_right == cur)
						parent->_right = cur->_right;
					else
						parent->_left = cur->_right;
				}

				delete cur;
			}
			else
			{
				//左右子树都不为空,找左子树的最右节点
				//可以更改为找右子树的最左节点
				parent = cur;
				Node* maxLeft = cur->_left;

				while (maxLeft->_right)
				{
					parent = maxLeft;
					maxLeft = maxLeft->_right;
				}

				//替换,伪删除
				cur->_key = maxLeft->_key;

				if (parent->_right == maxLeft)
					parent->_right = maxLeft->_left;
				else
					parent->_left = maxLeft->_left;

				delete maxLeft;
			}

			return true;
		}
	}

	return false;
}

总结: 

左右子树都为空时:直接删除;左子树、右子树其中一个为空时:托孤,将另一个子树(孩子)寄托给父节点,然后删除自己;左子树、右子树都不空:找一个能挑起担子的保姆,照顾左右两个子树(孩子),然后删除多余的保姆。

注意:涉及更改链接关系的操作,都需要保存父节点的信息;右子树为空、左子树为空时,包含了删除 根节点 的情况,此时 parent 为空,不必更改父节点链接关系,更新根节点信息后,删除目标节点即可,因此需要对这种情况特殊处理;右子树、左子树都为空的节点,包含于 右子树为空 的情况中,自然会处理到;左右子树都不为空的场景中,parent 要初始化为 cur,避免后面的野指针问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

04异常Lambda算法正则

异常 异常是什么&#xff1f; 异常是代码在编译或者执行的过程中可能出现的错误。避免异常的出现&#xff0c;同时处理可能出现的异常&#xff0c;让代码更稳健。 异常分为几类&#xff1f; 编译时异常、运行时异常。编译时异常&#xff1a;没有继承RuntimeExcpetion的异常…

GB/T 28181标准中的错误码,国标28181中可能出现的SIP协议相关的错误码及其含义

目录 一、GB/T 28181标准介绍 &#xff08;一&#xff09;概述 &#xff08;二&#xff09;关键内容和特点 1. 系统架构&#xff1a; 2. 设备接入&#xff1a; 3. 网络通信&#xff1a; 4. 业务功能&#xff1a; 5. 安全保护&#xff1a; 6. 平台管理&#xff1a; &a…

MATLAB 构建协方差矩阵,解算特征值和特征向量(63)

MATLAB 局部点云构建协方差矩阵,解算特征值和特征向量(63) 一、算法介绍二、算法实现1.代码2.结果一、算法介绍 对于某片有待分析的点云,我们希望构建协方差矩阵,计算特征值和特征向量,这是很多算法必要的分析方法,这里提供完整的计算代码(验证正确) !!! 特别需要注意…

03-JAVA设计模式-责任链模式

责任链模式 什么是责任链模式 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为设计模式&#xff0c;允许你将请求沿着处理者链进行传递。每个处理者均对请求进行某些处理&#xff0c;并可决定是否将请求沿着链传递下去。这种模式给予请求的处理…

使用ArrayList.removeAll(List list)导致的机器重启

背景 先说一下背景&#xff0c;博主所在的业务组有一个核心系统&#xff0c;需要同步两个不同数据源给过来的数据到redis中&#xff0c;但是每次同步之前需要过滤掉一部分数据&#xff0c;只存储剩下的数据。每次同步的数据与需要过滤掉的数据量级大概在0-100w的数据不等。 由…

Windows 关闭占用指定端口的进程

以下示例以443端口为例&#xff0c;具体哪个端口视自己情况而定 输入命令 # 输出的最后一列就是进程号pid netstat -ano | findstr "443" 找出占用443端口的进程号(pid)&#xff08;第二列是你本机的应用占用的端口&#xff0c;看第二列就行&#xff09;如下图&am…

面向电力行业定制安全云工作站解决方案,麒麟信安出席2024年电力企业信创替代技术研讨会

日前&#xff0c;由中国电子企业协会主办的“2024年电力企业信创替代技术研讨会”在江苏南京正式召开。会议以国家推进实现自主可控、加快建设“数字中国”为大背景&#xff0c;聚焦电力企业紧抓“信创替代”机遇&#xff0c;通过安全可靠的软硬件迭代升级&#xff0c;实现企业…

算法打卡day39|动态规划篇07| Leetcode 70. 爬楼梯(进阶版)、322. 零钱兑换、279.完全平方数

算法题 Leetcode 70. 爬楼梯&#xff08;进阶版&#xff09; 题目&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 < m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 注意&#xff1a;给定 n 是一个正整数。 输入描述…

初始Linux(上)

目录 Linux的发展史UNIX发展史Linux的发展史 Linux下的基本命令ls指令pwd命令cd指令touch指令mkdir指令rmdir指令和rm指令man指令cp指令mv指令cat指令 总结 Linux的发展史 UNIX发展史 1968年&#xff0c;一些来自通用电器公司、贝尔实验室和麻省理工学院的研究人员开发了一个名…

从0到1实现RPC | 12 限流

在服务提供者provider端添加限流逻辑 限流&#xff1a;指定时间内请求数超过指定阈值时就抛出异常。 在ProviderInvoker的调用过程中&#xff0c;添加限流逻辑&#xff1a; 使用滑动窗口SlidingTimeWindow统计30s的请求数&#xff1b;每个服务service对应一个滑动窗口&#…

【C语言】字符串函数和内存函数及其模拟实现

文章目录 前言 一、常见字符串库函数1.strlen函数2.长度不受限制的字符串函数2.1 strcpy2.2 strcat2.3 strcmp 3.长度受限制的字符串函数3.1 strncpy3.2 strncat3.3 strncmp 二、字符串查找函数strstrstrtok 三、strerror函数四、内存操作函数1.memcpy2.memmove3.memcmp 五、字…

天地人和•大道不孤——卢禹舜中国画作品展在重庆美术馆隆重开幕

2024年4月12日&#xff0c;由中国国家画院、重庆市文化和旅游发展委员会主办&#xff0c;重庆美术馆&#xff08;重庆画院、重庆国画院&#xff09;、北京八荒锦绣美术馆、中国国际文化交流基金会卢禹舜艺术基金承办的“天地人和•大道不孤——卢禹舜中国画作品展”开幕式在重庆…

照片jpeg怎么变成jpg格式?这2种方法超简单!

在上传或下载照片时&#xff0c;某些网络服务可能对jpeg格式的上传或下载速度较慢&#xff0c;或者可能对文件大小有限制。通过将照片转换为jpg格式&#xff0c;您可以减小文件大小&#xff0c;提高上传和下载速度&#xff0c;并适应网络服务对jpg格式的更好支持&#xff0c;接…

·13·1dawwd

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

21 标准错误

标准输出重定向关闭无数据 下面的代码&#xff1a; #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>int main() {close(1);i…

使用Postman发送跨域请求实验

使用Postman发送跨域请求 1 跨域是什么&#xff1f;2 何为同源呢?3 跨域请求是如何被检测到的&#xff1f;4 Postman跨域请求测试4.1 后端准备4.2 测试用例4.2.1 后端未配置跨域请求(1) 前端不跨域&#xff08;2&#xff09;前端跨域 4.2.2 后端配置跨域信息&#xff08;1&…

商标没有去注册有哪些不好的影响!

有些商家咨询普推知产老杨&#xff0c;商标没有去注册有哪些不好的影响&#xff0c;其实对企业来说还有许多实际不利的影响&#xff0c;有时代价比注册一个商标要大很多。 想的商标名称没去注册商标&#xff0c;如果别人抢注拿下商标注册证&#xff0c;那就会涉及侵权&#xf…

C++11 设计模式4. 抽象工厂(Abstract Factory)模式

问题的提出 从前面我们已经使用了工厂方法模式 解决了一些问题。 现在 策划又提出了新的需求&#xff1a;对于各个怪物&#xff0c;在不同的场景下&#xff0c;怪物的面板数值会发生变化&#xff0c; //怪物分类&#xff1a;亡灵类&#xff0c;元素类&#xff0c;机械类 …

Fence同步

在《Android图形显示系统》没有介绍到帧同步的相关概念&#xff0c;这里简单介绍补充一下。 在图形显示系统中&#xff0c;图形缓存GraphicBuffer可以被不同的硬件来访问&#xff0c;如CPU、GPU、HWC都可以对缓存进行读写&#xff0c;如果同时对图形缓存进行操作&#xff0c;有…

26、链表-环形链表II

思路&#xff1a; 这道题就是判断链表中是否有环&#xff0c;首先使用集合肯定可以快速地解决&#xff0c;比如通过一个set集合遍历&#xff0c;如果遍历过程中有节点在set中已经存在那么说明存在环。返回这个节点即可 第二种方式就是通过快慢指针方式寻找环。如何做呢&#xf…