C++进阶 | [3] 搜索二叉树

news2024/10/5 3:27:47

摘要:什么是搜索二叉树,实现搜索二叉树(及递归版本)


什么是搜索二叉树

搜索二叉树/二叉排序树/二叉查找树·BST(Binary Search Tree):特征——左小右大(不允许重复值)。即取搜索二叉树中任一结点为根往下看,总满足左子树所有结点的值都小于根的值,右子树所有结点的值都大于根结点的值。

搜索二叉树图例如下:

搜索二叉树-图例

实现搜索二叉树

(不需要 namespace 封装,因为这不是模拟实现,不会和库里产生冲突)

1. 创建结点_Node

class K 的这个 K 是搜索二叉树的一个命名惯例(Convention),即 key(关键词)。

如下这个图例,首先我们一定要明确一个结点的内容包括哪些部分,将这些看作整体,而不是分离的各部分。示例代码如下。

搜索二叉树结点-图例
template<class K>
struct BSTreeNode
{
	BSTreeNode(const K key = K())
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}


	K _key;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
};

(补充:上面我们解决了结点的创建,对于二叉树的创建,我们只需要用一个根结点的指针 Node* 来实现即可,具体见下面代码BSTree的成员变量。) 

2. 插入数据_Insert

思路:比较大小,按搜索二叉树的排列规则插入。

注意:①结点与新结点之间的链接问题;②注意树为空的情况。

(这部分很简单不多赘述,直接看代码。另外,注意细节,并测试这段代码的准确性后再接着往下写)

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:
	bool Insert(const K& key)
	{
		if (_rootp == nullptr)
		{
			_rootp = new Node(key);
			return true;
		}

		Node* curp = _rootp;
		Node* parent_p = curp;
		while (curp)
		{
			if (curp->_key > key)
			{
				parent_p = curp;
				curp = curp->_left;
			}
			else if (curp->_key < key)
			{
				parent_p = curp;
				curp = curp->_right;
			}
			else
				return false;
		}

		Node* newp = new Node(key);//注意结点之间的链接问题
		if (parent_p->_key > key)
		{
			parent_p->_left = newp;
		}
		else
		{
			parent_p->_right = newp;
		}
		return true;
	}
private:
	Node* _rootp = nullptr;
}

3. 中序遍历_InOrder

复习一下中序遍历:即 左子树(递归往下拆) 根 右子树(递归往下拆) 的顺序。

注意:这个函数肯定需要传递参数,即某个结点的指针。然后该函数将从这个传递来的参数的结点指针为根结点向下遍历,一般我们需要从整个树的根开始遍历,但是该搜索二叉树的根结点 (_rootp) 私有成员无法访问。这个问题可以通过多种方式解决,我们这里采用“套一层”的方式。具体看代码实现。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	void _InOrder(Node* rootp)
	{
		if (rootp == nullptr)
			return;

		_InOrder(rootp->_left);
		std::cout << rootp->_key << " ";
		_InOrder(rootp->_right);
	}

	void InOrder()
	{
		return _InOrder(_rootp);//"套一层"👉类内可以随意访问成员
	}

private:
	Node* _rootp = nullptr;
};

4. 查找结点_Find

根据搜索二叉树的排列特性去找,要找的这个值比当前结点值小就去左子树找,比当前结点大就去右子树找。这个函数实现起来很简单,不多赘述,具体实现可参考下面代码。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	bool Find(const K& key)
	{
		if (_rootp == nullptr)
			return false;

		Node* curp = _rootp;
		while (curp)
		{
			if (curp->_key > key)
			{
				curp = curp->_left;
			}
			else if (curp->_key < key)
			{
				curp = curp->_right;
			}
			else
				return true;
		}
		return false;
	}

private:
	Node* _rootp = nullptr;
};

5. 删除结点_Erase(难点⭐)

先找到结点,再删除。下面分析删除的思路。ps.以下对于 要被删除的结点 简称为 del.

思路:1.对于没有叶子结点的结点我们可以直接删除;
           2.没有右子树的情况:我们可以让 del 的 parent 结点来接管 del 的 leftchild。同时,我们还需要判断 del 为 parent 的 rightchild 还是 leftchild。图解如下,下图中“断开链接”只是形象的说法,实现的时候只需要将 parent 的 child结点的指针直接覆盖即可。(注意 del 为 整个树的根结点 的情况→我们需要选择一个结点为新的根节点)

           3.没有左子树的情况:同理(同没有右子树的分析)。(注意 要被删除的结点 是 整个树的根结点 的情况)

           4.左右子树都有的情况找左子树的最大结点或者右子树的最小节点与要删除的结点交换。左子树的最大结点即该子树的最右叶子结点,右子树的最小结点即该子树的最左叶子结点。 
如下图,选择 13 或者 9 与 10 交换都可以。
       9 作为左子树中最大的结点,满足①大于左子树所有结点;②小于右子树所有节点。
       13 作为右子树中最小的结点,满足①大于左子树所有结点;②小于右子树所有节点。

如上图所示,图中 the one 即为我们找到的子树中可以与 要被删除的结点 交换的结点(左子树的最大结点或者右子树的最小节点)

明确整体上的思路之后,我们来看具体怎么实现👇 (如有疑惑请参看注释)(使用swap函数记得声明头文件)

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:
	
	bool Erase(const K& key)
	{
		if (_rootp == nullptr)
			return false;

		Node* curp = _rootp;
		Node* parent_p = nullptr;
		while (curp)
		{
			if (curp->_key > key)
			{
				parent_p = curp;
				curp = curp->_left;
			}
			else if (curp->_key < key)
			{
				parent_p = curp;
				curp = curp->_right;
			}
			else
			{
				Node* del_nodep = curp;//找到了

				//进行删除
                //左右子树都没有的情况不需要单独判断,这个情况可以归属于没有 左子树/右子树 的情况
				if (del_nodep->_left == nullptr)//没有左子树的情况
				{
					if (parent_p == nullptr)
						_rootp = del_nodep->_right;

					else if (parent_p->_left == del_nodep)
						parent_p->_left = del_nodep->_right;
					else
						parent_p->_right = del_nodep->_right;

					delete del_nodep;
				}
				else if (del_nodep->_right == nullptr)
				{
					if (parent_p == nullptr)
						_rootp = del_nodep->_left;

					else if (parent_p->_left == del_nodep)
						parent_p->_left = del_nodep->_left;
					else
						parent_p->_right = del_nodep->_left;

					delete del_nodep;
				}
				else//左右子树都存在的情况
				{
					//找到适合替换要被删除的结点的结点。
                    //这里选择del_nodep的右树的最小结点,即右树的最左结点
					Node* subLeft_p = del_nodep->_right;
					Node* Left_parent_p = del_nodep;

					while (subLeft_p->_left)
					{
						Left_parent_p = subLeft_p;
						subLeft_p = subLeft_p->_left;
					}

					std::swap(subLeft_p->_key, del_nodep->_key);
					del_nodep = subLeft_p;

					if (Left_parent_p->_right == del_nodep)
						Left_parent_p->_right = del_nodep->_right;
					else
						Left_parent_p->_left = del_nodep->_left;

					delete del_nodep;

				}
				return true;//删除成功
			}
		}
		return false;
	}
private:
	Node* _rootp = nullptr;
};

说明:增删查改的“改”

搜索二叉树由于本身的特性是不可以在结点原本的数值上进行修改的。否则可能会破坏搜索二叉树。因此没有修改的相关函数。

6. 递归版_recursion

下面我们用递归的方式来实现“增删查”。(函数名后带‘R’,用于区分递归版本与非递归版本)

递归实际上是通过参数的改变来达到“向下递归”的效果的,实现递归版本肯定要传递结点的指针,这里我们统一通过“套一层”的方式解决。如下代码。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	bool FindR(const K& key)
	{
		return _FindR(_rootp, key);
	}

	bool InsertR(const K& key)
	{
		return _InsertR(_rootp, key);
	}

	bool EraseR(const K& key)
	{
		return _EraseR(_rootp, key);
	}
private:
	bool _FindR(Node* rootp, const K& key)
	{
		//
	}

	bool _InsertR(Node*& rootp, const K& key)
	{
	    //
	}

	bool _EraseR(Node*& rootp, const K& key)
	{
		//
	}
private:
	Node* _rootp = nullptr;
};

1)_FindR

思路:根据搜索二叉树的特性。首先判断当前结点是不是要找的结点,如果不是,若比这个要找的结点比这个结点小就递归去左树找;若比这个要找的结点大就递归去右树找。找到空结点即为没找到。

bool _FindR(Node* rootp, const K& key)
{
	if (rootp == nullptr)
		return false;
	if (rootp->_key == key)
		return true;

	if (rootp->_key > key)
		return _FindR(rootp->_left, key);
	else if (rootp->_key < key)
		return _FindR(rootp->_right, key);
}

2)_InsertR

思路:遵循搜索二叉树的特性。先找到合适位置再插入,要插入的结点比当前结点小就递归到左子树插入,比当前结点大就递归到右子树插入。一直到找到空位置(nullptr)即可插入。(ps.不允许插入重复值)

注意:关于链接的问题,我们可以通过引用传参巧妙地解决。为了方便理解,这里可以把引用传参看作传递了参数本身,而不是参数的一份拷贝。

bool _InsertR(Node*& rootp, const K& key)
{
	if (rootp == nullptr)
	{
		rootp = new Node(key);
		return true;
	}
	if (rootp->_key == key)
		return false;


	if (rootp->_key > key)
	{
		return _InsertR(rootp->_left, key);
	}
	else if (rootp->_key < key)
	{
		return _InsertR(rootp->_right, key);
	}
}

再次说明:Node* rootp 传值传参 是 将要传过来的指针变量的内容拷贝一份给 rootp,它们分别是两个指针变量;Node*& rootp 传引用传参 是 将要穿过的指针变量本身(也可以理解为这个指针变量的别名)传递过来,rootp就是这个指针变量,它们就是同一个指针变量。

插入时的链接问题-图解(例)

3)_EraseR

基本思路:先找到要删除的结点,没找到就直接返回 false,找到了就进行删除。

递归思路:要删除的结点值 key 比当前结点小就递归到左树删除;比当前结点大就递归到右树删除;等于当前结点就对这个结点进行删除。

bool _EraseR(Node*& rootp, const K& key)
{
	if (rootp == nullptr)
		return false;

	if (rootp->_key > key)
	{
		return _EraseR(rootp->_left, key);
	}
	else if (rootp->_key < key)
	{
		return _EraseR(rootp->_right, key);
	}
	else
	{
		Node* del_nodep = rootp;
		if (del_nodep->_left == nullptr)//左子树为空或左右子树为空
		{
			rootp = del_nodep->_right;
			delete del_nodep;
		}
		else if (del_nodep->_right == nullptr)//右子树为空
		{
			rootp = del_nodep->_left;
			delete del_nodep;
		}
		else//左右子树都不为空
		{
			Node* subLeft = del_nodep->_right;
			while (subLeft->_left)
			{
				subLeft = subLeft->_left;
			}

			std::swap(subLeft->_key, del_nodep->_key);

			return _EraseR(rootp->_right, key);
		}
	}
}

对于左右子树为空/左子树为空/右子树为空的情况:(以下图情况为例分析)

图例

对于左右子树都不为空的情况:(以下图情况为例分析)

图例


END

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

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

相关文章

【项目实战】使用Github pages、Hexo如何10分钟内快速生成个人博客网站

文章目录 一.准备工作1.安装git2.安装node安装 cnpm 3.使用 GitHub 创建仓库&#xff0c;并配置 GitHub Pages0.Github Pages是什么1. 在 GitHub 上创建一个新仓库2. 创建您的静态网站3. 启用 GitHub Pages4. 等待构建完成5. 访问您的网站 二. Hexo1.什么是Hexo2.安装Hexo1. 安…

RAG查询改写方法概述

在RAG系统中&#xff0c;用户的查询是丰富多样的&#xff0c;可能存在措辞不准确和缺乏语义信息的问题。这导致使用原始的查询可能无法有效检索到目标文档。 因此&#xff0c;将用户查询的语义空间与文档的语义空间对齐至关重要&#xff0c;目前主要有查询改写和嵌入转换两种方…

QT创造一个新的类(柱状图的类),并关联属性和方法

1.以在UI上添加柱状图的类为例&#xff08;Histogram&#xff09; #ifndef STUDY_HISTOGRAM_H #define STUDY_HISTOGRAM_H#include <QVector> #include <QWidget>// 前向声明 QT_BEGIN_NAMESPACE class QColor; class QRect; class QString; class QPaintDevice; …

适用于 macOS 的最佳独立 HBO Max 客户端

适用于 macOS 的最佳独立 HBO Max 应用程序。不再在浏览器选项卡之间切换。只需直接从 Dock 启动 Clicker for HBO Max 即可开始狂欢。 HBO Max 客户端 Clicker for HBO Max 下载 Clicker for HBO Max mac版安装教程 软件下载完成后&#xff0c;双击pkg根据提示进行安装 Clic…

27、Qt自定义标题栏

一、说明 QtWidget及其子类有默认的标题栏&#xff0c;但是这个标题栏不能美化&#xff0c;有时候满足不了我们的使用需求&#xff0c;所以进行自定义标题栏 二、下载图标 在下面的链接中下载两种颜色的最大化、向下还原、最大化和关闭八个图片&#xff0c;并找一张当做图标…

c++opencv Project3 - License Plate Detector

俄罗斯车牌识别案例&#xff1a;实时识别车牌&#xff0c;并且读取到指定文件夹中。 惯例先展示结果图&#xff1a; 对于摄像头读取图片进行车牌匹配&#xff0c;原理和人脸识别其实是一致的。 利用训练好的模型进行匹配即可。可参考&#xff1a; 对视频实现人脸识别-CSDN博…

MySQL索引(聚簇索引、非聚簇索引)

了解MySQL索引详细&#xff0c;本文只做整理归纳&#xff1a;https://blog.csdn.net/wangfeijiu/article/details/113409719 概念 索引是对数据库表中一列或多列的值进行排序的一种结构&#xff0c;使用索引可快速访问数据库表中的特定信息。 索引分类 主键索引&#xff1a…

drawio 网页版二次开发(1):源码下载和环境搭建

目录 一 说明 二 源码地址以及下载 三 开发环境搭建 1. 前端工程地址 2. 配置开发环境 &#xff08;1&#xff09;安装 node.js &#xff08;2&#xff09;安装 serve 服务器 3. 运行 四 最后 一 说明 应公司项目要求&#xff0c;需要对drawio进行二次开发&…

Redis学习1——redis简介、基础

介绍 redis简介 Redis(Remote Dictonary Server) 是由Salvatore Sanfilippo开发的key-value缓存数据库&#xff0c;基于C语言开发。目前市面上&#xff0c;Redis和MongoDB是当前使用最广泛的NoSQL&#xff0c;而就Redis技术而言&#xff0c;它的性能十分优越&#xff0c;可以…

HackMyVM-Animetronic

目录 信息收集 arp nmap nikto whatweb WEB web信息收集 feroxbuster steghide exiftool hydra ssh连接 提权 系统信息收集 socat提权 信息收集 arp ┌──(root㉿0x00)-[~/HackMyVM] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 08:00:27:9d:6d:7…

jenkins部署想定报错

报错&#xff1a; 解决办法&#xff1a; 登录被编译的设备&#xff0c;清楚旧代码&#xff0c;在重新执行

burp靶场xss漏洞(初级篇)

靶场地址 http://portswigger.net/web-security/all-labs#cross-site-scripting 第一关&#xff1a;反射型 1.发现搜索框直接注入payload <script>alert(111)</script> ​ 2.出现弹窗即说明攻击成功 ​ 第二关&#xff1a;存储型 1.需要在评论里插入payload …

object

object.clone() 在 Java 中&#xff0c;Object.clone() 方法执行的是浅拷贝&#xff08;shallow copy&#xff09;&#xff0c;而不是深拷贝&#xff08;deep copy&#xff09;。 浅拷贝&#xff08;Shallow Copy&#xff09;&#xff1a; 浅拷贝是指在拷贝对象时&#xff0…

Al Agent:开启智能化未来的关键角色,让机器更智能的为我们服务

文章目录 &#x1f680;Al Agent是什么&#x1f4d5;Al Agent的工作原理与技术&#x1f4aa;Al Agent应用领域&#x1f680;智能家居应用&#x1f308;医疗健康领域⭐金融服务行业&#x1f302;交通运输管理&#x1f3ac;教育培训应用 &#x1f512;Al Agent优势与挑战✊Al Age…

中国地形可调节高度-UE5-UE4

2000坐标系&#xff0c;可进行高度调整。 支持版本4.21-5.4版本 下载位置&#xff1a;https://mbd.pub/o/bread/ZpWZm5Zs

初探 JUC 并发编程:读写锁 ReentrantReadWriteLock 原理(8000 字源码详解)

本文中会涉及到一些前面 ReentrantLock 中学到的内容&#xff0c;先去阅读一下我关于独占锁 ReentrantLock 的源码解析阅读起来会更加清晰。 初探 JUC 并发编程&#xff1a;独占锁 ReentrantLock 底层源码解析 6.4&#xff09;读写锁 ReentrantReadWriteLock 原理 前面提到的 R…

LeetCode 209 长度最小的子数组(滑动窗口and暴力)

、 法一&#xff1a;滑动窗口 //使用滑动窗口来解决问题 //滑动窗口的核心点有&#xff1a; /*1.窗口内是什么&#xff1f;2.如何移动窗口的起始位置&#xff1f;3.如何移动窗口的结束位置&#xff1f;4.两个指针&#xff0c;怎么判断哪个指针是终止指针&#xff0c;哪个指针…

【核武器】2024 年美国核武器-20240507

2024年5月7日,《原子科学家公报》发布了最新版的2024美国核武器手册 Hans M. Kristensen, Matt Korda, Eliana Johns, and Mackenzie Knight, United States nuclear weapons, 2024, Bulletin of the Atomic Scientists, 80:3, 182-208, DOI: https://doi.org/10.1080/00963…

【JavaScript】内置对象 - 数组对象 ① ( 数组简介 | 数组创建 | 数组类型检测 )

文章目录 一、数组对象1、数组简介2、数组创建3、数组检测 - Array.isArray() 方法4、数组检测 - instanceof 运算符 Array 数组对象参考文档 : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array 一、数组对象 1、数组简介 在 JavaScr…

嫁接打印的技术要点

所谓嫁接打印&#xff0c;是一种增减材混合制造的方式。它将已成形的模具零件当作基座&#xff0c;在此基础上“生长”出打印的零件。其中基座通常采用传统加工方式制造&#xff0c;而打印部分则使用专用的金属粉末&#xff0c;通过 3D 打印技术成型。 嫁接打印之所以备受欢迎&…