【C++笔记】二叉搜索树的模拟实现

【C++笔记】二叉搜索树的模拟实现

  • 一、二叉搜索树的概念
  • 二、二叉搜索树的模拟实现
    • 2.0、定义二叉树节点
    • 2.1、非递归接口实现
      • 2.1.1、插入
      • 2.1.2、查找
      • 2.1.3、删除
    • 2.2、递归接口实现
      • 2.2.1、插入
      • 2.2.2、查找
      • 2.2.3、删除
  • 三、升级为K-V模型

一、二叉搜索树的概念

二叉搜索树的概念:

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

就比如下面这一棵树就是一颗二叉搜索树:
在这里插入图片描述
学习二叉搜索树其实也是为了后面学习AVL树和红黑树打下基础,因为AVL树和红黑树都是有二叉搜索树延伸出来的。

基于二叉搜索树的的特性,如果要在二叉搜索树中查找某一个数,最多只需要查找到最底层(如果比根大就去右子树去查找,如果比根小就去左子树中查找),很容易得出查找的时间复杂度为logn。所以在应用中二叉搜索树也常常用于查找。

但是二叉搜索树查找的时间复杂度也并非一直稳定在logn,如果是一些比较特殊的树,时间复杂度可能会升到n,就比如下面这棵树:
在这里插入图片描述
所以AVL树和红黑树就是为了解决二叉搜索树的这个缺陷而设计出来的。

而且我们后面要学到的set和map就是用红黑树封装出来的,所以学好二叉搜索树也就变成了基础中的基础。

二、二叉搜索树的模拟实现

2.0、定义二叉树节点

搜索二叉树的节点其实也和传统的二叉树节点一样,只是我们处理这些节点的规则变了而已:

// 搜索二叉树节点
template <class K>
struct BSTreeNode {
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _val;
	BSTreeNode(const K& val)
		:_left(nullptr)
		, _right(nullptr)
		, _val(val)
	{}
};

然后我们在搜索二叉树的类中就只需要封装一个根节点的指针即可:

template <class K>
class BSTree {
public :
	typedef BSTreeNode<K> Node;
	// ……
private :
	Node* _root = nullptr;
};

2.1、非递归接口实现

因为搜索二叉树可以说是为了应用而产生的,所以它主要的接口不多,只需要实现三个:插入、查找、删除即可。其他的一些接口和普通二叉树是一样的。

2.1.1、插入

根据二叉搜索输的规则,我们很容易就能想出插入该怎么写:
我们只需要从根节点出发,一直遍历,如果要插入的节点比当前节点小那我们就往左子树走,如果如果比当前节点大就往右子树走,知道当前节点cur走到空就确定了要插入的位置:
在这里插入图片描述
但是这样子,只是cur走到了空,我们并不是道cur是最有一个节点的做孩子还是右孩子,所以我们还需要一个parent指针一路记录cur的父亲,这样到cur走到空的时候就能判断是插入在左边还是右边了:
在这里插入图片描述

注意: 搜索二叉树是不能插入相同节点的。

代码实现:

// 非递归插入
	bool insert(const K& val) {
		if (nullptr == _root) {
			_root = new Node(val);
			return true;
		}
		Node* cur = _root;
		Node* parent = _root;
		while (cur) {
			parent = cur;
			if (val < cur->_val) {
				cur = cur->_left;
			}
			else if (val > cur->_val) {
				cur = cur->_right;
			}
			else { // 不能插入相同节点,如果相同直接返回false
				return false;
			}
		}
		// 判断插入在左还是在右
		if (val < parent->_val) {
			parent->_left = new Node(val);
		}
		else {
			parent->_right = new Node(val);
		}
		return true;
	}

2.1.2、查找

查找我想应该是最简单的了,我们按规矩走一遍就行了,如果找到就返回true,走到空就表示找不到,返回false即可:

// 非递归查找
bool Find(const K& val) {
	Node* cur = _root;
	while (cur) {
		if (val < cur->_val) {
			cur = cur->_left;
		}
		else if (val > cur->_val) {
			cur = cur->_right;
		}
		else {
			return true;
		}
	}
	return false;
}

2.1.3、删除

但删除就没有这么简单了,因为这得涉及到对这棵树局部的重新调整。
可能大家也能想到,删除叶子结点一定是最简单的,删除左右都不为空的节点一定是最难的。
但是叶子结点是左右都为空,我们其实可以将它分类到左为空或者右为空中,因为它两个条件都满足嘛。

左为空:
比如在下面这棵树中我们要删除10这个节点:
在这里插入图片描述
根据搜索二叉树的规则,我们会发现对于一个根节点,其左子树的所有节点都是小于它的,右子树的所有节点都是大于它的。
所以对于当前的情况,我们只需要将当前cur节点删除,然后让其父亲的左指针或右指针指向cur的右孩子即可:
在这里插入图片描述
右为空:
同理,右为空的处理和左为空类似:
在这里插入图片描述

左右都不为空:
左右都不为空的情况就有点儿复杂了,因为这种情况cur牵扯到的节点一定是最多的,有可能cur的做右子树都是很大的树,可想要做的调整应该是很大的!?

但其实我们并需要做多大的调整,调整这种情况我们只需要把注意力放在搜索二叉树的“规则”上。

对于每一棵子树,它的左孩子一定比它小,有孩子一定比它大。
而从这个规律中我们也能得到另个一规律,就是对于一个根而言,它的左子树的所有节点都一定比它小,它的右子树的所有节点都一定比它大。

我们从第二个规律入手就可以很好的解决了,那怎样做到调整之后保持这个规律呢?

先给出结论:

找出左/右字数中最大/小的节点,然后与cur交换,然后那个最小的节点(值已经交换,所以也就达到了删除cur的目的)。

以找到右子树的最小节点为例,我们来解释一下,这样做为什么行,比如在下面这棵树中,我们要上出节点3:
在这里插入图片描述
我们这里用subLeft来表示右子树的最小节点,cur表示当前要删除的节点。

由搜索二叉树的规律我们可知,一个树的最小节点一定是这棵树的最左节点,所以它一定是个叶子结点,所以删除subLeft就可以转化成和上面左/右为空的情况。
而一棵树的最小节点一定比这棵树中的所有节点都要小,又由上面的第二个结论,**我们可知subLeft一定是大于cur左子树中任意的节点的。**所以交换cur和subleft后的整棵子树依旧符合搜索二叉树的定义。

同理,选择右子树的最大节点也是同样的证明。

代码实现:

// 非递归删除
bool Erase(const K& val) {
	Node* cur = _root;
	Node* parent = _root;
	// 先找到要删除的节点
	while (cur) {
		if (val < cur->_val) {
			parent = cur;
			cur = cur->_left;
		}
		else if (val > cur->_val) {
			parent = cur;
			cur = cur->_right;
		}
		else {
			// 开始删除
			if (nullptr == cur->_left) { // 左为空
				if (cur == _root) { // 如果cur等于_root
					_root = _root->_right;
				}
				else {
					if (cur == parent->_left) {
						parent->_left = cur->_right;
					}
					else {
						parent->_right = cur->_right;
					}
				}
				delete cur;
			}
			else if (nullptr == cur->_right) { // 右为空
				if (cur == _root) {
					_root = _root->_left;
				}
				else {
					if (cur == parent->_left) {
						parent->_left = cur->_left;
					}
					else {
						parent->_right = cur->_left;
					}
				}
				delete cur;
			}
			else { // 左右都不为空
				Node* parent = cur;
				Node* subLeft = cur->_right; // 记录右子树中最小的节点
				// 找到右子树最小的节点(右子树中最左边的节点)
				while (subLeft->_left) {
					parent = subLeft;
					subLeft = subLeft->_left;
				}
				// 交换
				swap(subLeft->_val, cur->_val);
				// 转化成去删除subLeft
				if (subLeft == parent->_left) {
					parent->_left = subLeft->_right;
				}
				else {
					parent->_right = subLeft->_right;
				}
				delete subLeft;
			}
			return true;
		}
	}
	return false;
}

2.2、递归接口实现

我们以前的二叉树都是用递归实现的,二搜索二叉树也是二叉树,当然也可以使用递归实现。

2.2.1、插入

插入的递归实现其实很好写,如果要插入的节点比当前的节点小就转化成在左子树插入,如果要插入的节点把当前的节点大就转化成在右子树插入,如果相等就返回false即可。
用递归实现还有一个好处就是,我们没必要记录遍历到的每个节点的parent了,因为递归用传参的方式就很好的帮我们解决了。

先看代码实现:

// 递归版insert
bool insertR(const K& val) {
	return _insertR(_root, val); // 因为_root在类里面是私有的,在类外部不好传参,所以在这里设计了一个子函数
}
bool _insertR(Node *& root, const K & val) {
	if (nullptr == root) {
		root = new Node(val);
	}
	if (val < root->_val) {
		return _insertR(root->_left, val);
	}
	else if (val > root->_val) {
		return _insertR(root->_right, val);
	}
	return false;
}

不知道大家有没有注意到,我的_insertR子函数里的root指针给的是一个引用,为什么要给引用呢?如果不给引用的话会有什么问题呢?

先给出回答:这里的root必须给引用,如果不给引用的话新插入的节点和之前的节点就连接不上了。

为什么:
虽说现在已经是C++了,但是有时候解决一些问题还是需要用到C语言的知识的。
这里其实涉及到了C语言中变量的“左值”和“右值”的知识。

如下图:
在这里插入图片描述
如果我们想要将新的节点连接上parent,要执行parent->_right = cur,这其实是将cur放到parent->_right指向的 “空间”,也就是要用到parent->_right的 “左值”(“左值”表示空间,“右值”表示数据)。

而如果我们不适用引用传参,则我们传过来的值是一个局部变量,是是实参的拷贝:
在这里插入图片描述
也就是说只传指针的话,我们只是将新节点放到了局部变量的空间中,所以就没有连接上。

如果想要连接成功,也可以像C语言一样使用二级指针,但是现在有C++的引用可以用,谁还用二级指针啊,二级指针多恶心啊!

所以我们这里就需要使用引用传参,引用是对象的别名,所以用的还是一样的左值,一样的空间。

2.2.2、查找

查找也是很简单的,几乎和循环一样。
如果要查找的节点比当前节点小,就递归到左子树查找,如果要查找的节点比当前节点大,就递归到右子树查找,如果递归到空节点,则说明找不到,返回false即可:

// 递归版查找
bool FindR(const K& val) {
	return _FindR(_root, val);
}
bool _FindR(Node* root, const K& val) {
	if (nullptr == root) {
		return false;
	}
	if (val < root->_val) {
		return _FindR(root->_left, val);
	}
	else if (val > root->_val) {
		return _FindR(root->_right, val);
	}
	return true;
}

2.2.3、删除

递归删除也是先要找到节点才行,这个不多说。
递归删除虽说是递归,但是我们只是借助递归帮我们查找节点而已,删除操作其实是和迭代的方式一样的,唯一不同的是,当删除到左右都不为空的节点时,我们交换后可以利用递归转化成在左子树或右子树中删除同样值:

// 递归版删除
bool EraseR(const K& val) {
	return _EraseR(_root, val);
}
bool _EraseR(Node*& root, const K& val) {
	if (root == nullptr) {
		return false;
	}
	if (val < root->_val) {
		return _EraseR(root->_left, val);
	}
	else if (val > root->_val) {
		return _EraseR(root->_right, val);
	}
	else {
		if (nullptr == root->_left) {
			// 开始删除(和迭代的方式一样)
			Node* del = root;
			root = root->_right;
			delete del;
			return true;
		}
		else if (nullptr == root->_right) {
			Node* del = root;
			root = root->_left;
			delete del;
			return true;
		}
		else {
			Node* subLeft = root->_right;
			// 找到右子树最小的节点(右子树中最左边的节点)
			while (subLeft->_left) {
				subLeft = subLeft->_left;
			}
			// 交换
			swap(subLeft->_val, root->_val);
		}
		// 转化成在右子树删除同样的值
		return _EraseR(root->_right, val);
	}
}

三、升级为K-V模型

我们上面所实现的其实可以称作是K模型的二叉搜索树,但二叉搜索树最多的应用其实是K-V模型。
K-V模型其实就是一个键对应一个值,比如英汉字典就是典型的K-V模型,中文是键,英文是值,在查找的时候只需要找到中文就可以得到对应的英文了。

搜索二叉树的K-V模型也是一样,在查找的时候我们只需要给出对应的key,就能找到对应的value。

想要将我们上面写的二叉搜索树改成K-V模型其实很简单,只需要多加一个模板参数,然后将对应的接口修改一下即可:

修改节点:

// 搜索二叉树节点
template <class K, class V>
struct BSTreeNode {
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
	V _val;
	BSTreeNode(const K& keyl, const V& val)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
		,_val(val)
	{}
};

修改插入(非递归):

// 非递归插入
bool insert(const K& key, const V& val) {
	if (nullptr == _root) {
		_root = new Node(key, val);
		return true;
	}
	Node* cur = _root;
	Node* parent = _root;
	while (cur) {
		parent = cur;
		if (keyl < cur->_key) {
			cur = cur->_left;
		}
		else if (key > cur->_key) {
			cur = cur->_right;
		}
		else {
			return false;
		}
	}
	if (key < parent->_key) {
		parent->_left = new Node(key, val);
	}
	else {
		parent->_right = new Node(key, val);
	}
	return true;
}

因为K-V模型只是通过key找到val,所以比较的时候置于key有关。

因为我们的插入和删除都是依照key来判断的,而删除并不涉及到修改,所以也就与val无关,所以删除的逻辑是一样的,不用修改。

修改查找:
因为我们这里改成了K-V模型,查找的话最好是能查找出一个键值对这样的效果更好,所以我们查找成功的话就直接返回节点的指针即可:

// 非递归查找
Node* Find(const K& keyl) {
	Node* cur = _root;
	while (cur) {
		if (keyl < cur->_key) {
			cur = cur->_left;
		}
		else if (keyl > cur->_key) {
			cur = cur->_right;
		}
		else {
			return cur;
		}
	}
	return nullptr;
}

修改完后我们可以浅浅的来测试一下,实现一个简易的单词查找功能:

在这里插入图片描述

还有一个简易的统计频数的功能:
在这里插入图片描述

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

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

相关文章

systemd-timesyncd

介绍 systemd-timesyncd 是一个用于跨网络同步系统时钟的守护服务。它实现了一个 SNTP 客户端。与NTP的复杂实现相比&#xff0c;这个服务简单的多&#xff0c;它只专注于从远程服务器查询然后同步到本地时钟。除非你打算为客户端提供 NTP 服务器或者连接本地硬件时钟&#xff…

【C++】C++的介绍及其发展史

初识C 一、什么是C&#xff0c;为什么会出现C二、C的发展史三、C的重要性3.1 语言的使用广泛度3.2 在工作领域 四、C的学习路径 及 书籍推荐 一、什么是C&#xff0c;为什么会出现C C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。 对于复杂的问题&#xff…

【python】sys-psth和模块搜索路径

我们在导入一个模块的时候&#xff0c;比如说&#xff1a; import math它必然是有搜索路径的&#xff0c;那到底是在哪个目录下面找呢&#xff1f;Python解释器去哪里找这个文件呢&#xff1f;只有找到这个文件才能读取、装载运行该模块文件。 它一般按照如下路径寻找模块文件…

混合A Star算法

混合A Star算法就是将A*与Lattice Graph相关联。 使用栅格地图&#xff0c;保持在栅格地图的这个节点方格里都只记录一个机器人的State&#xff0c;例如图上的弯弯的线&#xff0c;进行剪枝&#xff0c;看一个节点的Cost&#xff08;时间、消耗的能量、路径的长度、终端状态惩…

ElasticSearch学习和使用 (使用head软件可视化es数据)

使用步骤 直接使用 Elasticsearch的安装和使用 下载Elasticsearch6.2.2的zip包&#xff0c;并解压到指定目录&#xff0c;下载地址&#xff1a;https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-2-2运行bin目录下的elasticsearch.bat启动Elasticsearch安…

五个做原型的好处和意义

绘制原型不仅是产品开发的重要基础&#xff0c;也是UI设计师在设计过程中应该采用的主要路径标识。绘制原型的重要性不亚于建筑师手中的设计图纸&#xff01;虽然原型设计可能会给产品开发带来一些误解&#xff0c;但毫不夸张地说&#xff0c;任何开发人员都可以将优秀的开发产…

SMART PLC MODBUSTCP速度测试

SMART PLC MODBUSTCP通信详细介绍请参看下面文章链接: S7-200SMART PLC ModbusTCP通信(多服务器多从站轮询)_matlab sumilink 多个modbustcp读写_RXXW_Dor的博客-CSDN博客文章浏览阅读6.4k次,点赞5次,收藏10次。MBUS_CLIENT作为MODBUS TCP客户端通过S7-200 SMART CPU上的…

《015.SpringBoot+vue之音乐网》【前后端分离】

《015.SpringBootvue之音乐网》【前后端分离】 项目简介 [1]本系统涉及到的技术主要如下&#xff1a; 推荐环境配置&#xff1a;DEA jdk1.8 Maven MySQL 前后端分离; 后台&#xff1a;SpringBootMybatisMySQL; 前台&#xff1a;Vue3.0 TypeScript Vue-Router Vuex Axios …

C++ 运算符重载(Complex复数类)

Complex复数类 Complex.h #ifndef COMPLEX_H #define COMPLEX_H#include <iostream> using namespace std;class Complex { private:double real 0; //复数的实部double imag 0; //复数的虚部 public:Complex() default; /*无参构造*/Complex(double r); …

今起不再“没完没了的接龙斗嘴”

今天本“人民体验官”推广人民日报官方微博&#xff08;转央视网&#xff09;的文化产品《数字减负不能比减脂还难》。 截图&#xff1a;来源“人民体验官”推广平台 在时下的一些网络自媒体平台之上&#xff0c;的确存在“越拉越多的群&#xff0c;没完没了的接龙&#xff0c…

微信支付平台C#SDK_微信支付.net SDK

一、微信支付平台C# SDK V3 https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat/blob/main/docs/WechatTenpayV3 接口对应整理&#xff1a; https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat/blob/main/docs/WechatTenpayV3/Basic_Mod…

计算机网络技术(一)

深入浅出计算机网络 微课视频_哔哩哔哩_bilibili 第一章概述 1.1 信息时代的计算机网络 1. 计算机网络各类应用 2. 计算机网络带来的负面问题 3. 我国互联网发展情况 1.2 因特网概述 1. 网络、互连网&#xff08;互联网&#xff09;与因特网的区别与关系 如图所示&#xff0…

Pinia 状态管理器 菠萝:Setup Store风格

Pinia介绍&#xff1a; Pinia 是 Vue 的专属状态管理库&#xff0c;它允许你跨组件或页面共享状态。 Pinia 大小只有 1kb 左右&#xff0c;超轻量级&#xff0c;你甚至可能忘记它的存在&#xff01; 相比 Vuex,Pinia 的优点&#xff1a; 更贴合 Vue 3 的 Composition API 风…

Android修行手册 - 可变参数中星号什么作用(冷知识)

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

Halcon WPF 开发学习笔记(2):Halcon导出c#脚本和WPF初步开发

文章目录 前言HalconC#教学简单说明如何二开机器视觉如何二次开发Halcon导出Halcon脚本新建WPF项目&#xff0c;导入Halcon脚本和Halcon命名空间 前言 我目前搜了一下我了解的机器视觉软件&#xff0c;有如下特点 优点缺点兼容性教学视频(B站前三播放量)OpenCV开源&#xff0…

TCSVT(IEEE Transactions on Circuits and Systems for Video Technology)期刊投稿指南

目录 TCSVT 期刊简介 TCSVT 期刊影响因子和分区​ 期刊官方网站 期刊投稿网址 投稿指南网址 稿件格式要求 稿件提交指南 A. 提交稿件 B. 手稿格式 C. 摘要指南 D. 提交图形的指南 E. 页面和彩色图形费用 TCSVT 期刊简介 IEEE Transactions on Circuits and Systems…

产品化的GPT,能否为“百模大战”照亮未来?

这两天&#xff0c;AI圈都处在一种莫名的震撼感当中。 北京时间 11月7日&#xff0c;OpenAI 举办了首次DevDay开发者日活动。活动现场发布了非常多内容&#xff0c;其中有一些按部就班的&#xff0c;比如技术上更新了最新版本的GPT-4 Turbo。也有一些让从业者目瞪口呆&#xff…

C语言数据结构-----双向链表增删查改的代码实现

文章目录 1.初始化双链表2.创建链表节点3.打印链表4.尾插5.尾删6.头插7.头删8.在pos之前插入8.1 在pos之前插入(改造头插)8.2 在pos之前插入(改造尾插) 9.删除pos位置9.1 删除pos位置(改造尾删)9.1 删除pos位置(改造头删) 10.查找11.毁灭 链接: 顺序表(动态顺序表增删查改的代码…

openGauss学习笔记-121 openGauss 数据库管理-设置密态等值查询-使用JDBC操作密态数据库

文章目录 openGauss学习笔记-121 openGauss 数据库管理-设置密态等值查询-使用JDBC操作密态数据库121.1 连接密态数据库121.2 调用isValid方法刷新缓存示例121.3 执行密态等值查询相关的创建密钥语句121.4 执行密态等值查询相关的创建加密表的语句121.5 执行加密表的预编译SQL语…

系列八、Mybatis一对多查询,只查询出了一条记录

一、Mybatis一对多查询&#xff0c;只查询出了一条记录 1.1、问题说明 典型的权限管理框架的数据库表中&#xff0c;一般会存在这样3种角色的表&#xff0c;即用户表、角色表、用户角色关联表&#xff0c;表设计好之后&#xff0c;往这三张表中初始化了一些测试数据&#xff0…