【数据结构】二叉搜索树

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

文章目录

  • 一、初识二叉搜索树
    • [1. 概念](#1. 概念)
    • [2. 性能分析](#2. 性能分析)
  • 二、二叉搜索树的基本操作
    • [1. 插入操作](#1. 插入操作)
    • [2. 查找操作](#2. 查找操作)
    • [3. 删除操作](#3. 删除操作)
  • 三、二叉搜索树的实现
  • [四、二叉搜索树的 key 和 key/value 搜索](#四、二叉搜索树的 key 和 key/value 搜索)
    • [1. key 搜索](#1. key 搜索)
    • [2. key/value 搜索](#2. key/value 搜索)
    • [3. key/value 二叉搜索树的实现](#3. key/value 二叉搜索树的实现)
  • 总结

一、初识二叉搜索树

1. 概念

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

  1. 若它的左子树不为空 ,则左子树 上所有结点的值都小于等于根结点的值。
  2. 若它的右子树不为空 ,则右子树 上所有结点的值都大于等于根结点的值。
  3. 它的左右子树也分别为二叉搜索树

二叉搜索树中可以支持插入相等的值 ,也可以不支持插入相等的值,具体看使用场景定义:

在 C C C++ S T L STL STL 标准库中, m a p / s e t / m u l t i m a p / m u l t i s e t map/set/multimap/multiset map/set/multimap/multiset 系列容器底层就是二叉搜索树 ,其中 m a p / s e t map/set map/set 不支持插入相等值, m u l t i m a p / m u l t i s e t multimap/multiset multimap/multiset 支持插入相等值。

2. 性能分析

最优情况 下,二叉搜索树为完全二叉树 (或者接近完全二叉树),其高度为: log ⁡ 2 N \log_2N log2N;
最差情况 下,二叉搜索树退化为单支树 (或者类似单支),其高度为: N N N;

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

因此,这样的效率显然是无法满足我们需求的,所以后面又引申出了二叉搜索树的变形:平衡二叉搜索树 ( A V L AVL AVL 树、红黑树 和 B B B 树系列),才能适用于我们在内存中存储和搜索数据

【注意】为什么已经有二分查找了,还需要二叉搜索树呢?

虽然二分查找 也可以实现 O ( log ⁡ 2 N ) O(\log_2N) O(log2N) 级别的查找效率,但是二分查找有两大缺陷

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

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

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


二、二叉搜索树的基本操作

1. 插入操作

二叉搜索树的插入操作最坏时间复杂度 为 O ( N ) O(N) O(N)。(有序数组

插入的具体过程如下:

  1. 树为空 ,则直接新增结点 ,赋值给 r o o t root root 指针:
cpp 复制代码
if (_root == nullptr)
{
	_root = new node(key);
	return true;
}
  1. 树不空 ,按二叉搜索树性质,插入值比当前结点大往右 走,插入值比当前结点小往左走:
cpp 复制代码
node* parent = nullptr;
node* cur = _root;
while (cur)
{
	if (key < cur->_key)
	{
		parent = cur;
		cur = cur->_left;
	}
	else if (key > cur->_key)
	{
		parent = cur;
		cur = cur->_right;
	}
	else
	{
		return false;
	}
}
  1. 找到空位置,插入新结点
cpp 复制代码
cur = new node(key);
if (key < parent->_key)
{
	parent->_left = cur;
}
else
{
	parent->_right = cur;
}
return true;

注意:如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(这里我选择的是不支持相等的值插入

原数组:

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

插入一个元素 16 16 16:

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

完整代码示例:

cpp 复制代码
bool insert(const K& key)
{
	if (_root == nullptr)
	{
		_root = new node(key);
		return true;
	}
	node* parent = nullptr;
	node* cur = _root;
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}
	cur = new node(key);
	if (key < parent->_key)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	return true;
}

2. 查找操作

二叉搜索树的查找操作 和插入操作一样,最坏时间复杂度 为 O ( N ) O(N) O(N)。(有序数组

  1. 从根开始比较,查找 x x x, x x x 比根的值大则往右边走查找, x x x 比根值小则往左边走查找。

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

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

代码示例:

cpp 复制代码
bool find(const K& key)
{
	node* cur = _root;
	while (cur)
	{
		if (key < cur->_key)
		{
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			cur = cur->_right;
		}
		else
		{
			return true;
		}
	}
	return false;
}
  1. 支持插入相等 的值,意味着如果有多个 x x x 存在时,一般要求查找中序的第一个 x x x。如下图,查找 3 3 3,要找到 1 1 1 的右孩子的那个 3 3 3 返回。

3. 删除操作

由于二叉搜索树的删除操作 需要先进行查找操作,因此最坏时间复杂度 也为 O ( N ) O(N) O(N)。(有序数组

首先查找元素是否在二叉搜索树中 ,如果不存在,则返回 f a l s e false false。

cpp 复制代码
node* parent = nullptr;
node* cur = _root;
while (cur)
{
	if (key < cur->_key)
	{
		parent = cur;
		cur = cur->_left;
	}
	else if (key > cur->_key)
	{
		parent = cur;
		cur = cur->_right;
	}
	else
	{
		// 找到了,删除cur结点
		return true;
	}
}
return false;

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

  1. 要删除结点 N N N 左右孩子均为空0 0 0 个孩子);

  2. 要删除的结点 N N N 左孩子为空 ,右孩子结点不为空(1 1 1 个孩子):

  1. 要删除的结点 N N N 右孩子为空 ,左孩子结点不为空(1 1 1 个孩子):
  1. 要删除的结点 N N N 左右孩子结点均不为空2 2 2 个孩子):

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

  1. 把 N N N 结点的父亲对应孩子指针指向空,直接删除 N N N 结点。(情况 1 1 1 可以当成 2 2 2 或者 3 3 3 处理,效果是一样的,这里当作情况 2 2 2 来处理)

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

cpp 复制代码
// 1.左为空(都为空也被包含在内)
if (cur->_left == nullptr)
{
	if (cur == _root)	// parent == nullptr
	{
		_root = cur->_right;
	}
	else
	{
		if (parent->_left == cur)
		{
			parent->_left = cur->_right;
		}
		else
		{
			parent->_right = cur->_right;
		}
	}
	delete cur;
}
  1. 把 N N N 结点的父亲对应孩子指针指向 N N N 的左孩子,直接删除 N N N 结点。
cpp 复制代码
// 2.右为空
else if (cur->_right == nullptr)
{
	if (cur == _root)	// parent == nullptr
	{
		_root = cur->_left;
	}
	else
	{
		if (parent->_left == cur)
		{
			parent->_left = cur->_left;
		}
		else
		{
			parent->_right = cur->_left;
		}
	}
	delete cur;
}
  1. 无法直接删除 N N N 结点,因为 N N N 的两个孩子无处安放,只能用替换法 删除。找 N N N 左子树的值最大结点 R R R(最右结点)或者 N N N 右子树的值最小结点 R R R(最左结点)替代 N N N,因为这两个结点中任意一个,放到 N N N 的位置,都满足二叉搜索树的规则。替代 N N N 的意思就是 N N N 和 R R R 的两个结点的值交换,转而变成删除 R R R 结点, R R R 结点符合情况 2 2 2 或情况 3 3 3,可以直接删除。

注意 :二叉搜索树有一个性质,最小结点就是最左结点,最大结点就是最右结点

cpp 复制代码
// 3.左右都不为空
else
{
	node* replaceParent = cur;
	node* replace = cur->_left;	// 左子树的最大结点(最右结点)
	while (replace->_right)
	{
		replaceParent = replace;
		replace = replace->_right;
	}
	cur->_key = replace->_key;
	if (replaceParent->_right == replace)	// 进了循环
	{
		replaceParent->_right = replace->_left;
	}
	else									// 没进循环
	{
		replaceParent->_left = replace->_left;
	}
	delete replace;
}

三、二叉搜索树的实现

由于这个数据结构是用 C C C++ 代码来模拟实现的,因此采用了模板来定义的二叉搜索树类,所以不能将声明和定义分离,因此这里分为了两个文件: B i n a r y S e a r c h T r e e . h BinarySearchTree.h BinarySearchTree.h 来模拟实现并封装一个二叉搜索树的模板类 , t e s t . c p p test.cpp test.cpp 用来测试

原理部分上一章已经交代清楚了,这里给出完整代码:

  1. B i n a r y S e a r c h T r e e . h BinarySearchTree.h BinarySearchTree.h:
cpp 复制代码
#pragma once
#include<iostream>

using namespace std;

namespace key
{
	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>;
	public:
		bool insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new node(key);
				return true;
			}
			node* parent = nullptr;
			node* cur = _root;
			while (cur)
			{
				if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					return false;
				}
			}
			cur = new node(key);
			if (key < parent->_key)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			return true;
		}
	
		bool find(const K& key)
		{
			node* cur = _root;
			while (cur)
			{
				if (key < cur->_key)
				{
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}
			return false;
		}
	
		bool erase(const K& key)
		{
			node* parent = nullptr;
			node* cur = _root;
			while (cur)
			{
				if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					// 1.左为空(都为空也被包含在内)
					if (cur->_left == nullptr)
					{
						if (cur == _root)	// parent == nullptr
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}
					// 2.右为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)	// parent == nullptr
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					// 3.左右都不为空
					else
					{
						node* replaceParent = cur;
						node* replace = cur->_left;	// 左子树的最大结点(最右结点)
						while (replace->_right)
						{
							replaceParent = replace;
							replace = replace->_right;
						}
						cur->_key = replace->_key;
						if (replaceParent->_right == replace)	// 进了循环
						{
							replaceParent->_right = replace->_left;
						}
						else									// 没进循环
						{
							replaceParent->_left = replace->_left;
						}
						delete replace;
					}
					return true;
				}
			}
			return false;
		}
	
		void inorder()
		{
			_inorder(_root);
			cout << endl;
		}
	private:
		void _inorder(node* _root)
		{
			if (_root == nullptr)
			{
				return;
			}
			_inorder(_root->_left);
			cout << _root->_key << " ";
			_inorder(_root->_right);
		}
		node* _root = nullptr;
	};
}
  1. t e s t . c p p test.cpp test.cpp:
cpp 复制代码
#include"BinarySearchTree.h"

namespace key
{
	void test()
	{
		int a[] = { 8, 9, 5, 4, 1, 3, 2, 7, 6 };
	
		BSTree<int> bst;
	
		for (int i = 0; i < 9; i++)
		{
			bst.insert(a[i]);
			bst.inorder();
		}
	
		for (int i = 8; i >=0 ; i--)
		{
			bst.erase(a[i]);
			bst.inorder();
		}
	
		for (int i = 0; i <= 9; i++)
		{
			bst.find(i) == true ? cout << i << " find" << endl : cout << i << " no find" << endl;
		}
	}
}

int main()
{
	key::test();

	return 0;
}

四、二叉搜索树的 key 和 key/value 搜索

1. key 搜索

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

2. key/value 搜索

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

3. key/value 二叉搜索树的实现

k e y / v a l u e key/value key/value 二叉搜索树本质上还是通过关键码 k e y key key 来进行增 / / /删 / / /查等操作,只是需要多记录每一个 k e y key key 所对应的值 v a l u e value value,形成一种映射关系,所以只需要在之前的 k e y key key 搜索树上稍作修改即可。

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
	{
		using node = BSTNode<K, V>;
	public:
		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 (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					return false;
				}
			}
			cur = new node(key, value);
			if (key < parent->_key)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			return true;
		}

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

		bool erase(const K& key)
		{
			node* parent = nullptr;
			node* cur = _root;
			while (cur)
			{
				if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					// 1.左为空(都为空也被包含在内)
					if (cur->_left == nullptr)
					{
						if (cur == _root)	// parent == nullptr
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}
					// 2.右为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)	// parent == nullptr
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					// 3.左右都不为空
					else
					{
						node* replaceParent = cur;
						node* replace = cur->_left;	// 左子树的最大结点(最右结点)
						while (replace->_right)
						{
							replaceParent = replace;
							replace = replace->_right;
						}
						cur->_key = replace->_key;
						if (replaceParent->_right == replace)	// 进了循环
						{
							replaceParent->_right = replace->_left;
						}
						else									// 没进循环
						{
							replaceParent->_left = replace->_left;
						}
						delete replace;
					}
					return true;
				}
			}
			return false;
		}

		void inorder()
		{
			_inorder(_root);
			cout << endl;
		}
	private:
		void _inorder(node* _root)
		{
			if (_root == nullptr)
			{
				return;
			}
			_inorder(_root->_left);
			cout << _root->_key << ":" << _root->_value << " ";
			_inorder(_root->_right);
		}
		node* _root = nullptr;
	};
}

测试应用 1 1 1(简易中英字典):

输入(英文,中文 )的映射关系,以英文为依据( k e y key key),来查找对应中文( v a l u e value value)的值:

cpp 复制代码
void test1()
{
	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;
		}
	}
}

测试应用 2 2 2(统计字符出现次数):

先查找水果在不在搜索树中:

  1. 不在 :说明水果第一次出现,则插入 < < < 水果 , 1 > 1> 1>

  2. :则查找到的结点中水果对应的次数 + 1 +1 +1

cpp 复制代码
void test2()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		auto ret = countTree.find(str);
		if (ret == nullptr)
		{
			countTree.insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.inorder();
}

总结

本文先是对二叉搜索树进行了一个基本介绍:其作为一种经典的数据结构,既有链表的快速插入与删除操作的特点,又有数组快速查找的优势,所以应用十分广泛,例如在文件系统数据库系统 一般会采用这种数据结构进行高效率的排序与检索操作

然后,介绍并实现了二叉搜索树的三大基本操作:增删查( k e y key key 不能修改,修改就无意义了);

最后,又提到了二叉搜索树的两个应用场景,其中,在 C C C++ S T L STL STL 标准库中, s e t set set 的容器的底层结构就是 k e y key key 值查找 ,而 m a p map map 容器的底层结构是 k e y / v a l u e key/value key/value 键值对查找

相关推荐
Chandler241 小时前
Go:方法
开发语言·c++·golang
爱代码的小黄人3 小时前
深入解析系统频率响应:通过MATLAB模拟积分器对信号的稳态响应
开发语言·算法·matlab
whoarethenext4 小时前
qt的基本使用
开发语言·c++·后端·qt
是僵尸不是姜丝6 小时前
每日算法:洛谷U535992 J-C 小梦的宝石收集(双指针、二分)
c语言·开发语言·算法
虾球xz7 小时前
游戏引擎学习第220天
c++·学习·游戏引擎
愚润求学7 小时前
【C++】Stack && Queue && 仿函数
c++·stl·deque·queue·stack·priority queue
New个大鸭8 小时前
ATEngin开发记录_4_使用Premake5 自动化构建跨平台项目文件
c++·自动化·游戏引擎
寒页_8 小时前
2025年第十六届蓝桥杯省赛真题解析 Java B组(简单经验分享)
java·数据结构·经验分享·算法·蓝桥杯
smile-yan9 小时前
拓扑排序 —— 2. 力扣刷题207. 课程表
数据结构·算法·图论·拓扑排序
空雲.9 小时前
牛客周赛88
数据结构·c++·算法