【C++学习】日积月累——二叉搜索树详解

news2024/9/21 13:42:08

一、二叉搜索树

1.1 二叉搜索树概念

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

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值;
  • 若它的右子树不为空,则右子树上所有节点的值大于根节点的值;
  • 它的左右子树也分别为二叉搜索树。

1.2 二叉搜索树的相关案例

图1 二叉树

  在图1所示的二叉树a和b,两棵树都不符合1.1节二叉搜索树的性质。a中,根结点6的左子树上所有结点都大于根结点的值;b中,根结点98的左子树上所有结点的值都小于根结点的值;因而图1的二叉树a和二叉树b都不是二叉搜索树。

图2 标准二叉搜索树

此外,图2的二叉树相关结点完全切合二叉搜索树的性质,因而图2是标准的二叉搜索树。

二、二叉搜索树的操作

  以图2的标准二叉搜索树为例,需要使用到数组arr,[] = {8,3,1,10,6,4,7,14,13}。

2.1 二叉搜索树的查找

查找的具体过程如下:

从根结点开始比较、查找,比根结点大则往右“边走边查找”,比根小则往左“边走边查找”;

最多查找高度次,走到空,若还没找到,则这个值不存在。

2.2 二叉搜索树的插入

插入的具体过程如下:

  • 树为空,则直接新增结点,赋值给root指针;
  • 树不为空,按二叉搜索树性质查找插入位置,插入新结点;

图3 二叉搜索树的插入

  在图3中,以在二叉树中插入数值1和16为例,依据插入的具体过程规则当树不为空时,根据二叉搜索树的性质找到结点,并插入新节点,本案例按照中序排序进行排序。调试结果如图4所示,具体代码详见第三节二叉搜索树的实现

图4 调试结果

2.3 二叉搜索树的删除

  首先,查找元素是否在二叉搜索树中,若不存在则返回;否则要删除的结点可能分以下四种情况:

  • a.要删除的结点无孩子;
  • b.要删除的结点只有左孩子;
  • c.要删除的结点只有右孩子;
  • d.要删除的结点有左、右孩子结点;

对于上述四种情况,实际情况a可以与情况b或c合并,归纳总结删除的过程如下:

  • 情况b:删除该结点且使被删除结点的双亲结点指向被删除结点的左孩子结点 – 直接删除;
  • 情况c:删除该结点且使被删除结点的双亲结点指向被删除结点的有孩子结点 – 直接删除;
  • 情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除结点中,再来处理该节点的删除问题 – 替换法删除

具体逻辑详见第三部分二叉搜索树的代码实现。

三、二叉搜索树的实现

二叉搜索树的实现包含两个文件:BSTree.h和Test.c。以下分别为头文件和测试文件:

#pragma once
//二叉索树的结点(结构)
template<class K>
struct BSTreeNode {
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};

//二叉搜索树
template<class K>
class BSTree {
	typedef BSTreeNode<K> Node;//重命名
public:
	//BSTree()
	//	:_root(nullptr)
	//{}

	BSTree() = default;//默认构造函数

	BSTree(const BSTree<K>& t) {
		_root = Copy(t._root);
	}//拷贝构造

	BSTree<K>& operator=(BSTree<K> t) {
		swap(_root, t._root);
		return *this;
	}//赋值函数

	~BSTree() {
		Destroy(_root);
	}//析构函数

	bool Insert(const K& key) {
		if (_root == nullptr) {
			_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;
	}

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

	void InOrder() {
		_InOrder(_root);
		cout << endl;
	}

	//删除比较复杂,需要考虑因素较多
    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 {
				//delete
				if (cur->_left == nullptr) {
                    //解决根节点删除
					if (cur == _root) {
						_root = cur->_right;
					}
					else {
						if (parent->_left == cur) {
							parent->_left = cur->_right;
						}
						else {
							parent->_right = cur->_right;
						}
					}
					delete cur;
				}
				else if (cur->_right == nullptr) {
                    //同样解决cur的右为空,其本质为根节点时的删除
					if (cur == _root) {
						_root = cur->_left;
					}
					else {
						if (parent->_left == cur) {
							parent->_left = cur->_left;
						}
						else {
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				else {
                    //需要删除的结点拥有2各子结点或叶子
					Node* pminRight = cur;
					Node* minRight = cur->_right;

					while (minRight->_left) {
						pminRight = minRight;
						minRight = minRight->_left;
					}
					cur->_key = minRight->_key;
					if (pminRight->_left == minRight) {
						pminRight->_left = minRight->_right;
					}
					else {
						pminRight->_right = minRight->_left;
					}

					delete minRight;
				}
				return true;
			}
		}
		return false;
	}

	bool FindR(const K& key) {
		return _FindR(_root, key);
	}

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

	bool EraseR(const K& key) {
		return _EraseR(_root, key);
	}

	

protected:
	Node* Copy(Node* root) {
		if (root == nullptr) {
			return nullptr;
		}

		Node* newRoot = new Node(root->_key);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);
		return newRoot;
	}

	void Destroy(Node*& root) {
		if (root == nullptr) {
			return;
		}

		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}


	void _InOrder(Node* root) {
		if (root == nullptr) {
			return;
		}

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	bool _FindR(Node* root, const K& key) {
		if (root == nullptr) {
			return false;
		}

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

	bool _InsertR(Node*& root, const K& key) {
		if (root == nullptr) {
			root = new Node(key);
			return true;
		}

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


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

		if (root->_key < key) {
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key) {
			return _EraseR(root->_left, key);
		}
		else {
			Node* del = root;
			if (root->_left == nullptr) {
				root = root->_right;
			}
			else if (root->_right == nullptr) {
				root = root->_left;
			}
			else {
				Node* maxleft = root->_left;
				while (maxleft->_right) {
					maxleft = maxleft->_right;
				}

				swap(root->_key, maxleft->_key);
				return _EraseR(root->_left, key);
			}
			delete del;

			return true;
		}
	}

private:
	Node* _root;
};

测试文件:

#include<iostream>
using namespace std;

#include"BSTree.h"
void test_binaryTree01() {
	BSTree<int> t1;
	int arr[] = { 8,3,1,10,6,4,7,14,13 };
	for (auto e : arr) {
		t1.Insert(e);
	}
	t1.InOrder();

	BSTree<int> t2;
	for (auto i : arr) {
		t2.InsertR(i);
	}
	t2.InOrder();

	t1.Erase(7);
	t1.InOrder();

	t1.Erase(14);
	t1.InOrder();

	t1.Erase(3);
	t1.InOrder();

	t1.Erase(8);
	t1.InOrder();
}

void test_binaryTree02()
{
	int arr[] = { 8,3,1,10,6,4,7,14,13 };
	BSTree<int> t1;
	for (auto e : arr) {
		t1.Insert(e);
	}
	t1.InOrder();

	BSTree<int> t2(t1);
	t2.InOrder();
}

测试文件的test_binaryTree01()函数的调试结果:

图5 二叉搜索树的增删调试结果

四、二叉搜索树的应用

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

例如:给一个单词world,判断该单词是否拼写正确,具体方式如下:

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

2、KV模型:每一个关键码key,都有与之对应的值value,即<key,value>的键值对。该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word,chinese>就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word,count>就构成一种键值对
namespace KeyValue {
	template<class K, class V>
	struct BSTreeNode {
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;

		BSTreeNode(const K& key = K(), const V& value = V())
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
	};

	template<class K, class V>
	class BSTree {
		typedef BSTreeNode<K, V> Node;
	public:
		BSTree() = default;

		~BSTree() {
			Destroy(_root);
		}

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

		bool Insert(const K& key, const V& value) {
			if (_root == nullptr) {
				_root = new Node(key, value);
				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,value);
			if (parent->_key < key) {
				parent->_right = cur;
			}
			else {
				parent->_left = cur;
			}
			return true;
		}

		bool Erase(const K& key) {

		}

		void InOrder() {
			_InOrder(_root);
			cout << endl;
		}

	protected:
		void Destroy(Node*& root) {
			if (root == nullptr) {
				return;
			}

			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
			root = nullptr;
		}

		void _InOrder(Node* root) {
			if (root == nullptr) {
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root;
	};

}

//测试文件
void test_binaryTree03() {
	KeyValue::BSTree<string, string> dict;
	dict.Insert("string", "字符串");
	dict.Insert("tree", "树");
	dict.Insert("left", "左边、剩余");
	dict.Insert("right", "右边");
	dict.Insert("sort", "排序");

	string str;
	while (cin >> str) {
		KeyValue::BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret == nullptr) {
			cout << "单词拼写错误,词库中没有这个单词:" << endl;
		}
		else {
			cout << str << "中文翻译:" << ret->_value << endl;
		}
	}
}

void test_binaryTree04() {
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	KeyValue::BSTree<string, int> countTree;
	for (auto str : arr) {
		auto ret = countTree.Find(str);
		if (ret == nullptr) {
			countTree.Insert(str, 1);
		}
		else {
			ret->_value++;
		}
	}
	countTree.InOrder();
}

前面KV模型的代码实现的调试结果具体如下图6所示:

图6 KV模型实例一

test_binaryTree04()为KV模型实例二,具体的调试结果详见图7:

图7 KV模型实例二

总结

二叉搜索树的性能分析

  插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对拥有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多;但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

图8 二叉搜索树的两种形态

最优情况下,二叉搜索树为完全二叉搜索树(或者接近完全二叉树),其平均比较次数为: l o g N log N logN

最差情况下,图8的右图所示:二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:$ \frac{N}{2} $

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

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

相关文章

程序员常用的代码比较工具,你更喜欢哪款?

目录 &#x1f4a1; Linux 命令行的对比工具 一. diff 二. vimdiff命令 &#x1f4a1; GUI 比对工具 三. WinMerge 四. Diffuse 五. Code Compare 六. Beyond Compare 七. UltraCompare 八. Altova DiffDog 九. Kompare 十. Meld 十一. XXdiff 十二. KDiff3 十…

拓扑排序模板及例题

概念 一个有向无环图必然存在一个拓扑序列与之对应。 流程&#xff1a; 先将所有入度为0的节点入队将队列中的节点出队&#xff0c;出队序列就是对应拓扑序。对于弹出的节点x&#xff0c;遍历x所有出度y&#xff0c;对y进行入读减一操作检查入度减一之后的节点y&#xff0c;…

慈航公益·高莞乡情行

2023年4月9日&#xff0c;慈航公益高莞乡情行---高村陈屋第二届“敬老睦宗情暖乡土”活动日暨“英华教育奖学基金”成立大会在高村陈屋如期举行。 高村陈屋位于河源市连平县境内&#xff0c;为了赶上10点开始的活动&#xff0c;清晨六点半&#xff0c;志愿者们便从慈航出发&am…

12电感的应用

目录 一、电源电路使用 1、设计实例 二、高频电路中使用 1、选择Q值高的电感器 2、选择自谐振频率高的电感器 3、选择电感偏差小的电感器 三、控制振荡频率 四、确保高频信号的隔离 五、共模轭流线圈 一、电源电路使用 如式&#xff08;5&#xff09;所示&#xff0c;…

数码照片管理系统Damselfly

什么是 Damselfly &#xff1f; Damselfly 是一个基于服务器的数码照片管理系统。Damselfly 旨在管理基于文件夹的大型照片集合&#xff0c;特别关注快速搜索和关键字标记工作流程。Damselfly 包含强大的机器学习功能&#xff0c;可帮助您识别照片及其主体&#xff0c;包括人脸…

使用Socks5代理保障Windows网络安全

摘要&#xff1a;Socks5代理是一种在Windows系统中保障网络安全的有效方法。本文将详细介绍什么是Socks5代理&#xff0c;以及如何在Windows系统中使用Socks5代理来加强网络安全。同时&#xff0c;我们还将探讨如何编写代码来使用Socks5代理来保障应用程序的网络安全。 正文&am…

报表VS分析:为什么报表做不完?老板到底想要什么?

各位数据的朋友&#xff0c;大家好&#xff0c;我是老周道数据&#xff0c;和你一起&#xff0c;用常人思维数据分析&#xff0c;通过数据讲故事。 上一讲和大家讲了分析模型中的战斗机——财务分析模型。通过奥威BI软件的行计算模型来开发财务分析报表异常地简单&#xff0c;…

安装Django

1. 在物理环境安装Django Python官方的PyPi仓库为我们提供了一个统一的代码托管仓库&#xff0c;所有的第三方库&#xff0c;甚至你自己写的开源模块&#xff0c;都可以发布到这里&#xff0c;让全世界的人分享下载 pip是最有名的Python包管理工具 。提供了对Python包的查找、…

Linux 动态库的制作与使用

目录 动态库的制作和使用 动态库的制作和使用 原始结构如下&#xff1a; 先进入calc文件&#xff0c;并生成与位置无关的.o文件 接着生成动态文件库&#xff0c;使用ll指令可以看到&#xff0c;库名为绿色&#xff0c;linux中绿色的文件一般都是可执行文件 将其生成的lib…

如何快速搭建一个SpringBoot项目

前面我们了解了SpringBoot背景和特点&#xff0c;本节我们主要介绍如何快速构建一个SpringBoot项目&#xff0c;以此来提升日常开发效率。 SpringBoot是搭建应用的手脚架&#xff0c;由Spring公司的核心团队在2013年开始研发、2014年4月发布第一个版本的全新开源的轻量级框架。…

如何平衡倾斜摄影的三维模型轻量化数据文件大小和质量效果?

如何平衡倾斜摄影的三维模型轻量化数据文件大小和质量效果&#xff1f; 倾斜摄影超大场景的三维模型数据文件大小的具体范围取决于多种因素&#xff0c;如原始数据的复杂度、轻量化处理的方式和压缩算法等。一般而言&#xff0c;经过轻量化处理后&#xff0c;数据文件大小可以减…

centos7安装nginx及uwsgi部署django项目

1、安装配置uwsgi pip install uwsgi 2、在项目根目录下创建image_ocr_uwsgi.ini配置文件 [uwsgi] # 对外提供http服务的端口 http :9000 # 用于和nginx进行数据交互的端口 socket 127.0.0.1:8001 # django程序的主目录 chdir /home/image_process/image_ocr/image_ocr #…

【计算机架构】响应时间和吞吐量 | 相对性能 | 计算 CPU 时间 | 指令技术与 CPI | T=CC/CR, CC=IC*CPI

目录 0x00 响应时间和吞吐量&#xff08;Response Time and Throughput&#xff09; 0x01 相对性能&#xff08;Relative Performance&#xff09; 0x02 执行时间测量&#xff08;Measuring Execution Time&#xff09; 0x03 CPU 时钟&#xff08;Clocking&#xff09; 0x…

【RabbitMQ】| 狮子带你(超详细)原生Java操作兔子队列

目录 一. &#x1f981; 前言二. &#x1f981; 原生Java操作RabbitMQⅠ. 简单模式1. 添加依赖2. 编写生产者3. 编写消费者 Ⅱ. 工作队列模式1. 编写生产者2. 编写消费者3. 实现 Ⅲ. 发布订阅模式1. 编写生产者2. 编写消费者 Ⅳ. 路由模式1. 编写生产者2. 编写消费者 Ⅴ. 通配符…

SpringCloud源码之Spring Cloud Common核心接口说明

spring cloud commons spring cloud提供的通用抽象包&#xff0c;组件的实现基本上都依赖于当前包的接口定义实现功能&#xff0c;下面就是梳理一下当前包中都提供了哪些比较重要的接口 1. 服务注册 1.1 DiscoveryClient DiscoveryClient 是一个顶级的接口类&#xff0c;用…

node项目的建立

文章目录 1.node项目的建立1.1项目初始化1.2 安装express1.3 初始化服务器 2.配置跨域2.1安装cors2.2cors的引入&#xff08;app.js中&#xff09; 3.初始化路由3.1新建文件3.2初始路由模块3.3app.js注册3.4 在postman测试 4.抽离路由处理模块3.1 在router_handler新建user.js3…

为什么LC谐振频率附近信号会被放大

这个是LC低通滤波电路&#xff0c; 它的增益曲线是这样的 很多同学不理解为什么谐振频率附近信号会被放大&#xff0c;今天就来聊一聊为什么谐振频率附近信号会被放大。 看到这个LC低通滤波电路&#xff0c;假设输入信号源内阻为Rs&#xff0c;L和C为理想电感和电容&#xff0…

Jmeter(五)_CSV Data参数化,Beanshell

一.CSV Data Set Config 准备好一个txt文件&#xff0c;写入如下内容&#xff0c;第一行可以不写&#xff0c;写了的话也会作为一组数据被运行&#xff1a; 然后把后缀名改为CSV&#xff0c;这样一个参数化文件就准备好了 然后打开jmeter&#xff0c;在需要使用这个参数化…

数据库系统-数据库查询实现算法

文章目录 一、一趟扫描算法1.1 算法概述1.2 算法逻辑&物理实现1.2.1 逻辑层面1.2.2 物理层面1.2.2.1 P11.2.2.2 P21.2.2.3 P31.2.2.4 P4 1.3 迭代器构造查询实现算法1.4 关系操作的一趟扫描算法1.4 基于索引的查询实现算法 二、两趟扫描算法2.1 两趟算法基本思想2.2 多路归…

Clickhouse分布式表引擎(Distributed)写入核心原理解析

Clickhouse分布式表引擎&#xff08;Distributed&#xff09;写入核心原理解析 Clickhouse分布式表引擎&#xff08;Distributed&#xff09;写入核心原理解析Clickhouse分布式表引擎&#xff08;Distributed&#xff09;查询核心原理解析 Distributed表引擎是分布式表的代名…