C++ 二叉搜索树

一、二叉搜索树的概念​

二叉搜索树又称二叉排序树,它的定义非常明确:它要么是一棵空树,要么是具有以下三个核心性质的二叉树,这也是判断一棵二叉树是否为BST的关键依据:​

1.若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值;​

2.若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值;​

3.它的左、右子树也分别为二叉搜索树(递归定义)。​

这里需要重点说明:二叉搜索树是否支持插入相等的值,没有固定规则,完全取决于使用场景。后续我们学习的STL容器中,map、set不支持插入相等值,而multimap、multiset支持插入相等值,本质就是基于二叉搜索树的规则调整实现的。

直观示例

下面这棵树是典型的二叉搜索树,对照性质可直接验证。

8​

/ \​

3 10​

/ \ \​

1 6 14​

/ \ /​

4 7 13

二、二叉搜索树的性能分析

二叉搜索树的核心价值的是高效的增删查改操作,但性能好坏完全取决于树的结构,主要分为两种情况:

最优情况下 ,⼆叉搜索树为完全⼆叉树(或者接近完全⼆叉树),其⾼度为:log2N

最差情况下 ,⼆叉搜索树退化为单⽀树(或者类似单⽀),其⾼度为:N

所以综合⽽⾔⼆叉搜索树增删查改时间复杂度为: O(N) 那么这样的效率显然是⽆法满⾜我们需求的,我们后面会学到⼆叉搜索树的变形,平衡⼆ 叉搜索树AVL树和红⿊树,才能适⽤于我们在内存中存储和搜索数据。

另外需要说明的是,⼆分查找也可以实现O(logN) 2级别的查找效率,但是⼆分查找有两⼤缺陷:

  1. 需要存储在⽀持下标随机访问的结构中,并且有序。

  2. 插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数 据。

这⾥也就体现出了平衡⼆叉搜索树的价值。

三、⼆叉搜索树的插⼊

插⼊的具体过程如下:

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

  2. 树不空,按⼆叉搜索树性质,插⼊值⽐当前结点⼤往右⾛,插⼊值⽐当前结点⼩往左⾛,找到空位 置,插⼊新结点。

  3. 如果⽀持插⼊相等的值,插⼊值跟当前结点相等的值可以往右⾛,也可以往左⾛,找到空位置,插 ⼊新结点。(要注意的是要保持逻辑⼀致性,插⼊相等的值不要⼀会往右⾛,⼀会往左⾛)

cpp 复制代码
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

代码实现:

cpp 复制代码
//BinarySearch.h
#include<iostream>
using namespace std;

	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
	{
		using Node = BSTNode<K>;//和typedef Node = BSTNode<K>;一样的作用	
	public:
		//插入
		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;
		}

	private:
		Node* _root = nullptr;
	};

四、 ⼆叉搜索树的查找

  1. 从根开始⽐较,查找x,x⽐根的值⼤则往右边⾛查找,x⽐根值⼩则往左边⾛查找。

  2. 最多查找⾼度次,⾛到到空,还没找到,这个值不存在。

  3. 如果不⽀持插⼊相等的值,找到x即可返回。

  4. 如果⽀持插⼊相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。

代码实现:

cpp 复制代码
//查找
bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key > cur->_key)
		{
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}
	}
	return false;
}

五、⼆叉搜索树的删除

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

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

  1. 要删除结点N左右孩⼦均为空
  1. 要删除的结点N左孩⼦位空,右孩⼦结点不为空
  1. 要删除的结点N右孩⼦位空,左孩⼦结点不为空
  1. 要删除的结点N左右孩⼦结点均不为空,这是最复杂的情况,也是本章难点。

对应以上四种情况的解决⽅案:

  1. 把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样 的)

  2. 把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点

  3. 把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点

  4. ⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替换法删除。

对应要删除的N节点:

1.找到N的左子树的最大值进行替换,然后删除N;

2.找到N的右子树的最小值进行替换,然后删除N;

以上两种方法任意一个都可以。

假设使用方法2:

找到被删节点(cur)的右子树最小节点当替换节点(replace),替换后让replac的父亲节点(replaceparent)指向replace的右子树,最后删除replace。

情况1:replace在replaceparent的左子树,replaceparent就指向replace的右子树。

情况2:replace在replaceparent的右子树,replaceparent就指向replace的右子树。

代码实现:

cpp 复制代码
//删除
bool Erase(const K& key)
{
	if (Find(key)==0)
		return false;

	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
		{
			//删除	
			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)//被删节点右为空的情况
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{						
					if (parent->_left == cur)
					{
						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;
				}
				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)

二叉搜索树主要有两种使用场景,对应两种节点结构,我们分别说明并实现key-value场景的代码。

6.1 key搜索场景

核心特点:结构中只存储key(关键码),核心需求是判断key是否存在,支持增删查,但不支持修改key(修改key会破坏BST的有序性)。

典型应用场景
  • 小区无人值守车库:将业主车牌号作为key存入BST,车辆进入时扫描车牌,查找是否在BST中,在则抬杆,不在则禁止进入;

  • 英文单词拼写检查:将词库中所有单词作为key存入BST,读取文章中的单词,查找是否在BST中,不在则提示拼写错误。

我们前面实现的BSTree<K>,就是典型的key搜索场景。

cpp 复制代码
#include"BinarySearch.h"
int main()
{
	key_value::BSTree<string, string> dict;
	dict.Insert("left", "左边");
	dict.Insert("right", "右边");
	dict.Insert("insert", "插入");
	dict.Insert("string", "字符串");

	string str;
	while (cin >> str)
	{
		auto ret = dict.Find(str);
		if (ret)
		{
			cout << "->" << ret->_value << endl;
		}
		else
		{
			cout << "无此单词,请重新输入" << endl;
		}
	}
	return 0;
}

6.2 key-value搜索场景

核心特点:每个key对应一个value(值),节点中需要同时存储key和value,增删查仍以key为关键字,支持修改value(不支持修改key),核心需求是通过key快速找到对应的value。

典型应用场景
  • 简单中英互译字典:key为英文单词,value为中文释义,输入英文key,快速查找对应的中文释义;

  • 商场无人值守车库:key为车牌号,value为入场时间,出场时通过车牌号(key)查找入场时间,计算停车时长和费用;

  • 单词出现次数统计:key为单词,value为出现次数,读取单词时,若key不存在则插入(key, 1),若存在则将value++。

cpp 复制代码
int main()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
	"苹果", "香蕉", "苹果", "香蕉" };
	key_value::BSTree<string, int> countTree;

	for (const auto& str : arr)
	{
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的结点中水果对应的次数++
		auto ret = countTree.Find(str);
		if (ret == nullptr)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			// 修改value
			ret->_value++;
		}
	}
	countTree.Inorder();
	return 0;
}

七、全文总结

二叉搜索树是C++数据结构的重点,也是后续高级树形结构的基础,核心要点总结如下:

  1. 概念:二叉搜索树要么是空树,要么满足"左子树≤根,右子树≥根",左右子树也为BST;是否支持重复值,取决于使用场景。

  2. 性能:最优情况(完全二叉树)时间复杂度 \(O(log _{2} N)\),最差情况(单支树)退化到 \(O(N)\),后续需学习AVL树、红黑树优化。

  3. 核心操作: - 插入:按BST性质查找空位置,插入新节点,不支持重复值则返回false; - 查找:按BST性质缩小范围,最多查找树的高度次; - 删除:分4种场景,核心是"替换法"处理左右子树均非空的情况,删除后保持BST有序。

  4. 使用场景: - key场景:只判断key是否存在,不支持修改key; - key-value场景:通过key找value,支持修改value,不支持修改key。

  5. 关键验证:中序遍历结果为严格递增序列,是判断BST的核心方法。

二叉搜索树的难点在于删除操作的4种场景,建议大家多敲几遍代码,尤其是场景4的替换法,理解替代节点的选择逻辑和删除逻辑。掌握了二叉搜索树,后续学习AVL树、红黑树就会轻松很多,也能更好地理解STL容器的底层实现。

相关推荐
Season4504 小时前
C++11之正则表达式使用指南--[正则表达式介绍]|[regex的常用函数等介绍]
c++·算法·正则表达式
Tisfy4 小时前
LeetCode 2839.判断通过操作能否让字符串相等 I:if-else(两两判断)
算法·leetcode·字符串·题解
问好眼4 小时前
《算法竞赛进阶指南》0x04 二分-1.最佳牛围栏
数据结构·c++·算法·二分·信息学奥赛
Birdy_x4 小时前
接口自动化项目实战(1):requests请求封装
开发语言·前端·python
海海不瞌睡(捏捏王子)4 小时前
C++ 知识点概要
开发语言·c++
会编程的土豆4 小时前
【数据结构与算法】优先队列
数据结构·算法
桌面运维家5 小时前
VLAN配置进阶:抑制广播风暴,提升网络效率
开发语言·网络·php
一轮弯弯的明月6 小时前
Python基础-速通秘籍(下)
开发语言·笔记·python·学习
西西学代码6 小时前
Flutter---回调函数
开发语言·javascript·flutter