C++进阶—二叉搜索树

news2024/11/24 7:07:35

目录

0. 前言

1. 二叉搜索树概念

2. 二叉搜索树操作

3. 二叉搜索树的实现

3.1 非递归实现插入操作Insert

3.2 二叉搜索树中序遍历递归实现(排序)

3.3 非递归实现查找操作Find

3.4 非递归实现删除操作Erase

3.5 递归实现插入操作InsertR

3.5 递归实现查找操作FindR

3.6 递归实现删除操作EraseR(递归引用的价值)

4.二叉搜索树的拷贝赋值析构&&其他操作

4.1 二叉树的拷贝构造

4.2 二叉搜索树的赋值

4.3 二叉搜索树的析构

4.4 二叉搜索树的其他操作

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


0. 前言

以下是前面有关二叉树的文章:

二叉树和堆

二叉树链式结构的实现

二叉树层序遍历

更新C++二叉树进阶是因为:

  1.         map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  2.         二叉搜索树的特性了解,有助于更好的理解map和set的特性
  3.         二叉树中部分面试题稍微有点难度,在前面学习不容易接受,且时间长容易忘
  4.         有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

因此最近有关二叉树搜索树的文章,是对二叉树部分进行收尾总结。

1. 二叉搜索树概念

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

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

 

2. 二叉搜索树操作

 

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

1. 二叉搜索树的查找

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

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

2. 二叉搜索树的插入具体过程如下:

         a. 树为空,则直接新增节点,赋值给root指针

         b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

如下插入0、9、16

3. 二叉搜索树的删除

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

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

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:

        假设删除的节点为cur,parent为cur的父节点,绘图分析

        情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除         

 

        情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除         

 

        情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点 中,再来处理该结点的删除问题--替换法删除

 

3. 二叉搜索树的实现

注:C++11新增用法,构造函数关键字default,强制生成默认构造

​
#pragma once
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

template<class K>
class BSTreeNode {
public:
	BSTreeNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{
	}

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

//key
template<class K>
class BinarySearchTree {
	typedef BSTreeNode<K> Node;
public:
	/*BinarySearchTree() 
		:_root(nullptr)
	{

	}*/
	//强制编译器生成默认构造 —— c++11用法,default
	BinarySearchTree() = default;
private:
	Node* _root = nullptr;
};

二叉受搜索树采用链式结构,左右孩子进行实现

3.1 非递归实现插入操作Insert

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

3.2 二叉搜索树中序遍历递归实现(排序)

二叉搜索树也叫做二叉排序树,因为其结构性质导致了按照中序遍历,便可以得到有序数据!

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

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

注:由于递归需要穿参,遍历二叉树左右子树,而首要传参为根节点,根节点便是this的成员变量,可以直接访问,且缺省参数不可以使用成员变量(缺省参数必须是常量),如果在外传参,需要使用getRoot获取根节点,在传递,使用不便,因此只需要在内部套一层private修饰的成员函数,只可以类内部访问,使其在使用上和普通成员函数无差异!!!

3.3 非递归实现查找操作Find

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

3.4 非递归实现删除操作Erase

bool Erase(const K & key) {
		if (_root == nullptr) {
			return false;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur) {
			if (cur->_key > key) {
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key) {
				parent = cur;
				cur = cur->_right;
			}
			else {
				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;
					cur = nullptr;
					return true;
				}
				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;
						}
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				else {
					//替换法删除 -- 右数最小节点
					Node* replace = cur->_right;
					Node* replaceParent = cur;
					while (replace->_left != nullptr) {
						replaceParent = replace;
						replace = replace->_left;
					}
					std::swap(cur->_key, replace->_key);
					if (replaceParent->_left == replace) {
						replaceParent->_left = replace->_right;
					}
					else {
						replaceParent->_right = replace->_right;
					}
					delete replace;
					return true;
				}
			}
		}
		return false;

	}

注: 采用替换法删除数据,因为二叉搜索树的性质,导致其元素如果重复便会插入失败,因此说明其可以自动去重,在删除时,由于每个元素数据不同,采用替换的方式更为高效易懂

3.5 递归实现插入操作InsertR

public:
    bool InsertR(const K& key) {
		if (_root == nullptr) {
			_root = new Node(key);
			return true;
		}
		else {
			return _InsertR(_root, key);
		}
	}
private:
    bool _InsertR(Node*& root, const K& key) {
		if (root == nullptr) {
			root = new Node(key);
			return true;
		}

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

3.5 递归实现查找操作FindR

public:
	bool FindR(const K& key) const {
		return _FindR(_root, key);
	}
private:
	bool _FindR(Node* root,const K& key) const {
		
		if (root == nullptr) {
			return false;
		}
		if (key > root->_key) {
			return _FindR(root->_right, key);
		}
		else if (key < root->_key) {
			return _FindR(root->_left, key);
		}
		else {
			return true;
		}
	}

3.6 递归实现删除操作EraseR(递归引用的价值)

递归下引用传值的价值:

public:
	bool EraseR(const K& key) {
		return _EraseR(_root, key);
	}
private:
	bool _EraseR(Node*& root ,const K& key) {
		if (root == nullptr) {
			return false;
		}
		if (key > root->_key) {
			return  _EraseR(root->_right, key); 
		}
		else if (key < root->_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* replace = root->_right;
				while (replace->_left != nullptr) {
					replace = replace->_left;
				}
				std::swap(root->_key, replace->_key);
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;
		}
	}

4.二叉搜索树的拷贝赋值析构&&其他操作

4.1 二叉树的拷贝构造

二叉树的拷贝因每个节点都是new出来的,因此需要采用深拷贝,而拷贝不能改变树的结构,并保持节点左孩子有孩子指向相同,可以使用递归方式,采用先序遍历,先开辟根节点,再使根节点的左孩子指向左子树,有右孩子指向右子树递归,构建树

public:
        BinarySearchTree(const BinarySearchTree<K>& bst) {
			_root = _copy(bst._root);
		}
private:
        Node* _copy(Node* root) {
			if (root == nullptr) {
				return nullptr;
			}
			Node* copyRoot = new Node(root->_key);
			copyRoot->_left = _copy(root->_left);
			copyRoot->_right = _copy(root->_right);
			return copyRoot;
		}

注:虽然自定义类型构造函数生成的默认构造即可满足需求,但是拷贝构造完成的是值拷贝,会造成double free,因此重写拷贝构造时,拷贝构造也是构造,编译器不在生成默认构造,使用C++11新增的default关键字,强制编译器生成默认构造!

4.2 二叉搜索树的赋值

默认生成的赋值同样完成的是值拷贝,因此需要重写赋值操作,而赋值可以根据拷贝构造并使用现代写法,完成赋值运算

public:
		BinarySearchTree<K>& operator=(BinarySearchTree<K> bst) {
			std::swap(_root, bst._root);
			return *this;
		}

4.3 二叉搜索树的析构

因为使用了堆区空间,析构时需要遍历整棵树,而析构应采用后序遍历,使用分治思想,先析构左子树,在析构右子树,最终析构根节点,否者会找不到左右子树,会造成内存泄露!

		void _destory(Node*& root) {
			if (root == nullptr) {
				return;
			}
			_destory(root->_left);
			_destory(root->_right);
			delete root;
			root = nullptr;
		}
	public:
		~BinarySearchTree() {
			_destory(_root);
		}

4.4 二叉搜索树的其他操作

求叶子节点个数、节点个数、树深度

​
public:
		int depth() const {
			return _depth(_root);
		}
		int nodeCount() const {
			return _nodeCount(_root);
		}
		int leafSize() const {
			return _leafSize(_root);
		}
private:
		int _depth(Node* root) const {
			if (root == nullptr) {
				return 0;
			}
			int leftDepth = _depth(root->_left) + 1;
			int rightDepth = _depth(root->_right) + 1;
			return  leftDepth > rightDepth ? leftDepth : rightDepth;
		}
		int _nodeCount(Node* root) const {
			if (root == nullptr) {
				return 0;
			}
			int leftCount = _nodeCount(root->_left) + 1;
			int rightCount = _nodeCount(root->_right) + 1;
			return leftCount + rightCount - 1;
		}
		int _leafSize(Node* root) const {
			if (root == nullptr) {
				return 0;
			}
			if (root->_left == nullptr && root->_right == nullptr) {
				return 1;
			}
			int leftSize = _leafSize(root->_left);
			int rightSize = _leafSize(root->_right);
			return leftSize + rightSize;
		}

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

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

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

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

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

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?后续学习的二叉平衡搜索树、AVL树和红黑树

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

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

相关文章

c++读取文件之---yaml-cpp使用

实际项目总会遇到有很多超参数的情况&#xff0c;用常规的结构体等无法有效的涵盖所有&#xff0c;为了方便用户进行配置使用&#xff0c;因此使用yaml的方式进行编辑配置&#xff0c;因此去调研使用了yaml-cpp的使用方法。 1、yaml-cpp下载和编译 下载方式很简单&#xff0c…

openfeign实现远程调用

一 openfeign简介 Feign 是声明性(注解)web 服务客户端它使编写 web 服务客户端更加容易请创建一个接口并对其进行注解.它具有可插入注解支持&#xff0c;包括Feign注解和JAXRS注解Feign 还支持可插拔编码器和解码器。Spring cloud 添加了对Spring MVC注解的支持&#xff0c;并…

chatgpt赋能python:Python编译成SO文件和反编译的介绍

Python编译成SO文件和反编译的介绍 什么是SO文件&#xff1f; SO文件&#xff0c;也称为共享对象文件&#xff0c;是一种二进制文件格式&#xff0c;用于在多个应用程序之间共享代码和数据。在Unix和类Unix系统中&#xff0c;它们通常是共享库文件的形式。因此&#xff0c;SO…

chatgpt赋能python:Python编译成可执行文件:让你的代码更加优雅高效

Python编译成可执行文件&#xff1a;让你的代码更加优雅高效 Python作为世界上最受欢迎的编程语言之一&#xff0c;拥有着丰富的库、面向对象的语法和简单易懂的语法结构。然而&#xff0c;在开发Python应用程序时&#xff0c;受限于Python的解释性&#xff0c;导致程序的效率…

Qt实现自定义控件能够以插件的方式加载到Qt设计师

目录 1、自定义部件/控件2、改进法3、插件法3.1、创建工程3.2、工程目录3.3、修改插件类的代码3.3.1、HexSpinBox类的头文件3.3.2、HexSpinBox类的源文件3.3.3、HexSpinBox类的UI文件3.3.4 需要的注意的事项 3.4、生成动态库 4、测试插件能否正常使用4.1、测试Qt设计师能否识别…

认识@Validated 和 @Valid

对于web应用来说&#xff0c;对方法参数的校验是十分重要的&#xff0c;参数校验的是否全面&#xff0c;直接决定整个方法的健壮性。 除了使用麻烦的if判断校验参数&#xff0c;还可以使用Validated 和 Valid注解来进行优雅地参数校验&#xff0c;让参数校验和写诗一样优雅。 …

手机移动 APP测试流程及测试点

1 APP测试基本流程 1.1 流程图 1.2 测试周期 测试周期可按项目的开发周期来确定测试时间&#xff0c; 一般测试时间为两三 周(即 15 个工作日)&#xff0c;根据项目情况以及版本质量可适当缩短或延长 测试时间。正式测试前先向主管确认项目排期。 1.3 测试资源 测试任务开始…

移动云智能算力调度平台,谱写算力互联互通新篇章

中国移动算力网络建设取得新进展&#xff01;移动云智能算力调度平台验证了多云服务商间异构算力的统一调度能力&#xff0c;联动国家级超算中心、智算中心&#xff0c;促进东部业务灵活使用西部算力&#xff0c;作为未来算力互联网的统一调度平台将持续开展技术攻关与应用创新…

linux系统LNMP架构部署

文章目录 一、Nginx编译安装1、关闭防火墙&#xff0c;安全机制2、安装依赖包3、创建运行用户与组4、解压包、编译安装路径5、编译安装6、优化路径7、添加 Nginx 系统服务、赋权 二、安装 MySQL 服务1、安装环境依赖包2、创建运行用户与组3、解压包、编译安装路径4.编译安装5、…

chatgpt赋能python:用Python编程计算BMI,轻松掌握健康

用Python编程计算BMI&#xff0c;轻松掌握健康 作为一个现代人&#xff0c;保持健康的体态是我们每个人都需要关注的问题。那么&#xff0c;如何快速地计算自己的BMI呢&#xff1f;Python编程可以帮助我们轻松地实现这个目标。 什么是BMI&#xff1f; BMI全称为Body Mass In…

【Linux操作系统】互斥的4个概念以及认识信号量

文章目录 进程互斥的4个概念认识信号量认识接口理解IPC 信号量主要用于同步和互斥的&#xff0c;下面先来看看什么是同步和互斥。 进程互斥的4个概念 我们把大家都能看到的资源&#xff0c;称为公共资源。并且要想实现进程间通信&#xff0c;首要条件就是要让互相通信的进程看…

一文了解kubernetes部署:API部署过程

API部署 准备工作 一、镜像制作 请参考&#xff1a;《API镜像制作》 二、为k8s配置docker私服密钥 请参考&#xff1a;《配置docker私服密钥》 部署API 修改yaml文件 vi/opt/kubernetes/api/config-server.yaml vi/opt/kubernetes/api/api.yaml 1、修改api相应image值为您的镜…

【深度学习】日常笔记7

可以通过在⽹络中加⼊⼀个或多个隐藏层来克服线性模型的限制&#xff0c;使其能处理更普遍的函数关系类型。要做到这⼀点&#xff0c;最简单的⽅法是将许多全连接层堆叠在⼀起。每⼀层都输出到上⾯的层&#xff0c;直到⽣成最后的输出。 上面红框的公式其实换个角度是没错的。实…

总结911

目标规划&#xff1a; 月目标&#xff1a;6月&#xff08;线性代数强化9讲&#xff0c;考研核心词过三遍&#xff09; 周目标&#xff1a;线性代数强化5讲&#xff0c;英语背3篇文章并回诵&#xff0c;检测 每日规划 今日已做 1.回诵之前文章 2.每日长难句&#xff0c;句句…

Redis五种数据结构底层编码结构

String String是Redis中最常见的数据存储类型&#xff1a; 其基本编码方式是RAW&#xff0c;基于简单动态字符串&#xff08;SDS&#xff09;实现&#xff0c;存储上限为512mb。如果存储的SDS长度小于44字节&#xff0c;则会采用EMBSTR编码&#xff0c;此时object head与SDS是…

Mysql(Linux数据库或者在Navicate中)

Mysql数据库组成 服务端:主要存储数据,并接收用户发过来的SQL语句,并执行结果返回给客户端 客户端:下发用户要执行的sql语句,并显示服务器返回的执行结果 命令行数据库连接方式 mysql -h 数据库 IP -P 端口号 -u 数据库登录用户名 -p 数据库登录密码 -h不加表示为本机,-P不…

编译原理一:编译器工作流

文章目录 1. 编译器工作流1.1. 解析&#xff08;Parsing&#xff09;1.2. 遍历&#xff08;Traversal&#xff09;1.3 转换(Transformation)1.4 代码生成(Code Generation) 1. 编译器工作流 编译器是将一种语言转化为另一种语言的程序。在编译器工作流中&#xff0c;通常可以分…

git上传云效codeup

为了标识身份&#xff0c;建议先完成 Git 全局设置 git config --global user.name "xxx" git config --global user.email "xxxxxxqq.com" 1.删除本地 .git文件夹 2.云效上 添加库-新建代码库 3.在 git bash 里 按照 建好的代码库 下方的 命令行指引-…

chatgpt赋能python:Python编译成二进制文件:优化代码执行效率

Python编译成二进制文件&#xff1a;优化代码执行效率 介绍 随着Python编程的不断普及&#xff0c;越来越多的开发者选择Python作为开发工具。然而&#xff0c;Python解释器需要读取并解释源代码&#xff0c;这种解释方式在执行效率上存在瓶颈。为了提高执行效率&#xff0c;…

io.netty学习(十三)Netty 解码器

目录 前言 编解码概述 编解码器概述 Netty 内嵌的编码器 解码器 ByteToMessageDecoder 抽象类 ReplayingDecoder 抽象类 MessageToMessageDecoder 抽象类 总结 前言 编码和解码&#xff1a;数据从一种特定协议格式到另一种格式的转换。 处理编码和解码的程序通常被称…