【C++】二叉搜索树

目录

前言

接着【C++】面向对象三大特性之一------多态详情请点击查看,今天来介绍另外一个内容------二叉搜索树

一、二叉搜索树概念

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

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

二、二叉搜索树性能分析

  1. 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:logN

  2. 最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:N

  3. 所以综合而言二叉搜索树增删查改时间复杂度为:O(N)

  4. 效率显然是无法满足我们需求的,我们后续介绍⼆叉搜索树的变形,平衡二叉搜索树AVL树和红黑树,才能适用于我们在内存中存储和搜索数据(logN)

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

二叉搜索树的实现,我们使用模板实现,声明定义不分离的写法,因此我们创建BinarySearchTree.h来实现二叉搜索树的相关函数,test.cpp测试实现的函数

  1. 首先我们需要一个struct节点作为二叉搜索树中的节点BSTreeNode,由于这个节点中的成员变量需要被经常访问,所以我们将其定义为strcut。这个节点中存储左右节点的指针即_left和_right,同时还存储有一个K类型的变量_key
  2. 二叉搜索树节点BSTreeNode,我们需要将节点内容初始化。那么接收K类型的数据key,由于这个K的类型可能是自定义类型,传值传参消耗大,所以我们采用引用传参,同时由于我们不对这个数据key进行修改,那么我们采用const进行修饰这个数据key,我们在构造函数的初始化列表中对左右节点指针初始化为空,并且让接收的数据赋值给节点中存储数据的变量_key
  3. 定义一个二叉搜索树的类模板BSTree,初始化只需要将节点初始化为根节点,为了便于定义和使用节点我们使用typedef将节点类型BSTreeNode< K >重命名为Node便于使用
  4. 使用这个节点类型声明出一个根节点的指针_root,在初始的时候,我们将这个根节点的指针置为空即可
cpp 复制代码
template<class K>
struct BSTNode
{
	K _key;
	BSTNode<K>* _left;
	BSTNode<K>* _right;
	BSTNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{ }
};

template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
public:
	BSTree()
		:_root(nullptr)
	{ }
private:
	Node* _root;
};

1、插入

  1. 树为空,则直接新增结点,赋值给root指针
  2. 树不为空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点
  3. 注意:在这里我们暂时只实现不同值的插入
  4. 在插入一个值时当我们根据二叉搜索树性质找到当前值应该插入的位置时,我们需要new一个节点(插入的值key),然后判断该插入父节点的左边还是右边
  5. 因此当我们根据性质向下遍历的key值该插入哪个位置的时候,我们不仅需要一个cur节点来比较当前节点的_key值和key的大小,还需要保存cur节点的父亲节点
cpp 复制代码
bool Insert(const K& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	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;
}

2、中序遍历

  1. 二叉搜索树使用中序遍历去遍历二叉搜索树,那么遍历后是有序的,即升序,这也是二叉搜索树的一个重要性质,所以二叉搜索树也叫做二叉排序树
  2. 下面的我实现了中序遍历的代码,但是现在有一个问题,InOrder函数必须要传入根节点,但是Node* _root是私有的,我们无法在类外面访问。
cpp 复制代码
void InOrder(Node* root)
{
	if (root == nullptr)
		return;
	InOrder(root->_left);
	cout << root->_key << " ";
	InOrder(root->_right);
}

BSTree<int> t;
//.....
t.InOrder();//无法获得_root
  1. 为了解决外部无法获得私有成员变量,我们可以实现一个GetRoot函数来获得根节点,但是此方法比较麻烦,我们也可以将void InOrder(Node* root)函数改名为void _InOrder(Node* root),直接外层就是调用t.InOrder(),再在t.InOrder()函数里面去调用void _InOrder(Node* root)
cpp 复制代码
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}

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

3、查找

  1. 从根开始比较,查找x,x比根的值大则往右边走查找,x比根值小则往左边走查找
  2. 最多查找高度次,走到空,还没找到,这个值不存在
  3. 因为我们现在不支持插入相等的值,找到x即可返回
  4. 如果支持插入相等的值,意味着有多个x存在,⼀般要求查找中序的第一个x(后面会具体讲解)
cpp 复制代码
bool 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 true;
		}
	}
	return false;
}

4、删除

查找元素是否在二叉搜索树中,如果不存在,则返回false

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

  1. 要删除结点N左右孩子均为空,把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是一样的)
  2. 要删除的结点N左孩子位空,右孩子结点不为空,把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
  3. 要删除的结点N右孩子位空,左孩子结点不为空,把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点
  4. 要删除的结点N左右孩子结点均不为空,无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除
cpp 复制代码
bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		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;
			}
			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;
			}
			else
			{
				//右子树最小节点
				Node* replaceParent = cur;
				Node* replace = cur->_right;
				while (replace->_left)
				{
					replaceParent = replace;
					replace = replace->_left;
				}
				swap(cur->_key, replace->_key);
				if(replaceParent->_left == replace)
				    replaceParent->_left = replace->_right;
				else
					replaceParent->_right = replace->_right;
				delete replace;
			}
			return true;
		}
	}
	return false;
}

四、二叉搜索树key和key/value使用场景

1、key的搜索场景

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了

  1. 小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录⼊后台系统,车辆进⼊时扫描车牌在不在系统中,在则抬杆,不在则提⽰非本小区车辆,无法进入
  2. 检查⼀篇英文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示

2、key/value的搜索场景

每⼀个关键码key,都有与之对应的值value,value可以任意类型对象 。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应value。key/value的搜索场景实现的⼆叉树搜索树支持修改,但是不支持修改key ,修改key破坏搜索树性质了,可以修改value

  1. 简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输⼊英文,则同时查找到了英文对应的中文
  2. 商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场
  3. 统计文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数

五、key/value二叉搜索树代码实现

  1. key_value结构二叉搜索树的模拟实现和key结构的模拟实现高度相似,只不过key_value结构的二叉搜索树中存储是两个值,一个是_key,一个是_value,那么为了和二叉搜索树的key结构进行区分,小编将key_value的结构的递归的模拟实现放在key_value的命名空间中
cpp 复制代码
namespace key_value
{
	template<class K, class V>
	struct BSTNode
	{
		K _key;
		V _value;
		BSTNode<K, V>* _left;
		BSTNode<K, V>* _right;
		BSTNode(const K& key, const V& value)
			:_key(key)
			,_value(value)
			, _left(nullptr)
			, _right(nullptr)
		{
		}
	};

	template<class K, class V>
	class BSTree
	{
		typedef BSTNode<K, V> Node;
	public:
	private:
		Node* _root;
	};
}

1、插入

对比key结构的插入,key/value不仅需要插入key,也需要插入value

cpp 复制代码
bool Insert(const K& key, const V& value)
{
	if (_root == nullptr)
	{
		_root = new Node(key, value);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	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;
}

2、中序遍历

中序遍历和key结构一样使用key值遍历,但是key/value结构遍历之后还能访问修改value的值

cpp 复制代码
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << root->_key << " " << root->_value;
	_InOrder(root->_right);
}

3、查找

查找还是和key结构一样通过key查找,不需要传入value查找,找到后不是返回bool类型,而是返回找到的这个节点指针,这样能够访问到value,这样就可以修改value

cpp 复制代码
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;
}

4、删除

删除逻辑和key结构完全一样,传入要删除的key值,找到了就删除(和value值无关),但是交换替换值的时候注意不仅要交换key,也要交换value

cpp 复制代码
bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		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;
			}
			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;
			}
			else
			{
				//右子树最小节点
				Node* replaceParent = cur;
				Node* replace = cur->_right;
				while (replace->_left)
				{
					replaceParent = replace;
					replace = replace->_left;
				}
				swap(cur->_key, replace->_key);
				swap(cur->_value, replace->_value); // 还需要交换_value
				if (replaceParent->_left == replace)
					replaceParent->_left = replace->_right;
				else
					replaceParent->_right = replace->_right;
				delete replace;
			}
			return true;
		}
	}
	return false;
}

5、拷贝构造函数

拷贝构造函数传入的是需要拷贝一棵二叉搜索树,再在Copy函数中传入该棵树的根节点,递归拷贝,先拷贝根节点,再左节点,再右节点

cpp 复制代码
BSTree(const BSTree<K, V>& t)
{
	_root = Copy(t._root);
}

Node* Copy(Node* root)
{
	if (root == nullptr)
		return nullptr;
	Node* newRoot = new Node(root->_key, root->_value);
	newRoot->_left = Copy(root->_left);
	newRoot->_right = Copy(root->_right);
	return newRoot;
}

6、赋值运算符重载

将t二叉搜索树赋值当前二叉搜索树,直接交换两棵树的_root节点

cpp 复制代码
BSTree<K, V>& operator=(BSTree<K, V> t)
{
	swap(_root, t._root);
	return *this;
}

7、析构函数

析构我们使用递归来析构各个节点,由于析构函数没有参数且删除各个节点需要传入根节点来删除,所以我们将Destroy函数实现递归删除,再析构函数调用Destroy函数

cpp 复制代码
~BSTree()
{
	Destroy(_root);
	_root = nullptr;
}

void Destroy(Node* root)
{
	if (root == nullptr)
		return;
	Destroy(root->_left);
	Destroy(root->_right);
	delete root;
}

六、源代码

源码点击查看

相关推荐
Savior`L2 小时前
二分算法及常见用法
数据结构·c++·算法
深海潜水员2 小时前
OpenGL 学习笔记 第一章:绘制一个窗口
c++·笔记·学习·图形渲染·opengl
JIngJaneIL2 小时前
基于Java非遗传承文化管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
mmz12073 小时前
前缀和问题(c++)
c++·算法·图论
南部余额3 小时前
踩坑与解惑:深入理解 SpringBoot 自动配置原理与配置排除机制
java·spring boot·自动配置原理·import
ULTRA??3 小时前
初学protobuf,C++应用例子(AI辅助)
c++·python
旖旎夜光3 小时前
list实现(7)(上)
c++
不会c嘎嘎3 小时前
深入理解 C++ 异常机制:从原理到工程实践
开发语言·c++
only-qi3 小时前
Redis如何应对 Redis 大 Key 问题
数据库·redis·缓存