【C++】二叉搜索树

二叉搜索树

1、二叉搜索树的概念

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

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

2、二叉搜索树的操作

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

2.1 二叉搜索树结点的创建

cpp 复制代码
template<class K>
struct BSTNode
{
	K _key;
	BSTNode<K>* _left;
	BSTNode<K>* _right;

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

2.2 二叉搜索树的框架

cpp 复制代码
template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
	
public:

private:
	Node* _root = nullptr;
};

2.3 Insert(插入)

cpp 复制代码
bool Insert(const K& key)
{

  //如果根节点是空指针,那么就把新节点直接赋值给根节点_root
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	//如果不为空查找
	//定义父节点指针,作用是在遍历时记住cur上一层,最后新节点要挂在parent的左或右
	Node* parent = nullptr;
	//从根节点开始遍历整棵树
	Node* cur = _root;
	while (cur) // cur不为空,说明当前还有节点可以比较
	{
		// 情况1:当前节点键值 < 要插入的key
    if (cur->_key < key)
    {
        parent = cur;       // 把当前节点记为候选父节点
        cur = cur->_right;  // key更大,去右子树继续找
    }
    // 情况2:当前节点键值 > 要插入的key
    else if (cur->_key > key)
    {
        parent = cur;       // 记为父节点
        cur = cur->_left;   // key更小,去左子树继续找
    }
    // 情况3:相等,树中已有相同key
    else
    {
        return false;       // 插入失败,重复元素不允许
    }
	}

	//插入节点,和父节点连接
	cur = new Node(key);  
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

	return true;
}

2.4 Find(查找)

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;
}

从根开始查找,比根大则去右边查找,比根小则去左边查找,最多查找次数是树的高度

2.5 InOrder(中序遍历)

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

这里的中序遍历是用递归的方式来实现的,如果想中序遍历二叉树,就要把根结点_root传给这个函数,但根结点_root是私有成员变量,用户访问不到。但类里面可以访问私有成员,所以可以在类里面可以实现一个中序遍历的子函数_InOrder,在子函数中实现中序遍历的逻辑,然后在提供一个中序遍历的函数接口InOrder,由它调用_InOrder。这样用户就可以使用中序遍历

2.6 Erase(删除)

删除大体分四种情况:

  1. 要删除的节点N没有子节点
  2. 要删除的节点N只有右孩子
  3. 要删除的节点N只有左孩子
  4. 要删除的节点N既有左孩子也有右孩子

解决方案:

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

情况2:把N节点的父亲对应孩子指针指向N的右孩子,直接删除N节点

情况3:把N节点的父亲对应孩子指针指向N的左孩子,直接删除N节点

情况4:用替代法删除,删除一个节点,可以找另一个节点继承当前的位置,找的节点一定要满足一个条件:左子树的最大节点或右子树的最小节点

cpp 复制代码
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  
		{
		  //删除四大场景
		  
		  //分支1:cur左孩子为空,覆盖两种场景
		  //场景1:无任何子节点  场景2:只有右孩子
			if (cur->_left == nullptr)
			{
				if (cur != _root)
				{
					//判断cur是父亲节点的左孩子
					if (cur == parent->_left)
					{
						// 左为空,父亲节点左指针直接指向cur的右子树
						// 场景1叶子:cur->_right是nullptr,等价父左指针置空,断开叶子连接
						// 场景2只有右孩子:父节点直接接上cur的右子树
						parent->_left = cur->_right;
					}
					//cur是父节点的右孩子
					else
					{
						//父节点右指针直接指向cur的右子树
						parent->_right = cur->_right;
					}
				}
				
				// cur是整棵树唯一根节点,没有父节点
				else
				{
					// 根节点更新为cur的右孩子
					// 场景1单根叶子:cur->_right=nullptr,整棵树直接置空
					//场景2只有右孩子:直接让右孩子作为根节点
					_root = cur->_right;
				}
				// 释放待删除节点内存,防止内存泄漏
				delete cur;
			}
			
			//分支2:cur右孩子为空
			//场景3:只有只有左孩子
			else if (cur->_right == nullptr)
			{
				//判断:待删节点cur不是根,有父节点
				if (cur != _root)
				{
					// cur是父的左孩子
					if (cur == parent->_left)
					{
						//父节点左指针直接接上cur的左子树,跳过待删节点
						parent->_left = cur->_left;
					}
					//cur是父的右孩子
					else
					{
						//父右指针直接接上cur的左子树
						parent->_right = cur->_left;
					}
				}
				//cur是根节点,只有左子树
				else
				{
					//树根直接更新为cur的左孩子
					_root = cur->_left;
				}
				//释放待删节点内存
				delete cur;
			}
			
			//分支3:cur左右孩子都不为空
			//场景4:既有左孩子也有右孩子
			else
			{
				//思路:不能直接删cur,交换cur和右子树最小值节点的key,再删除最小值节点
				Node* minRightParent = cur;  //记录右子树最小值节点的父节点,初始为cur
				//这里minRightParent不能设置为空,因为minRight可能为空
				Node* minRight = cur->_right;  //进入cur的右子树,寻找最小值
				//循环一路向左,找到右子树最左节点
				while (minRight->_left)
				{
					minRightParent = minRight;  //更新最小值节点的父
					minRight = minRight->_left;  //持续向左遍历
				}

				//交换两个节点
				swap(cur->_key, minRight->_key);

				//删除最小值节点minRight
				if (minRight == minRightParent->_left)
					//最小值是父的左孩子,父左指针接上最小值的右子树
					minRightParent->_left = minRight->_right;
				else
					//最小值是父的右孩子,父右指针接上最小值的右子树
					minRightParent->_right = minRight->_right;

				delete minRight;
			}

			return true;
		}
	}

	return false;
}

3、总代码

cpp 复制代码
#pragma once

template<class K>
struct BSTNode
{
	K _key;
	BSTNode<K>* _left;
	BSTNode<K>* _right;

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

// key
template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
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;
	}

	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;
	}

	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
			{
				// 准备删除
				if (cur->_left == nullptr)
				{
					if (cur != _root)
					{
						// 左为空,父亲指向我的右
						if (cur == parent->_left)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
					else
					{
						_root = cur->_right;
					}

					delete cur;
				}
				else if (cur->_right == nullptr)
				{
					if (cur != _root)
					{
						// 右为空,父亲指向我的左
						if (cur == parent->_left)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
					else
					{
						_root = cur->_left;
					}

					delete cur;
				}
				else
				{
					// 左右都不为空,找子树中适合的节点替代我
					Node* minRightParent = cur;
					Node* minRight = cur->_right;
					while (minRight->_left)
					{
						minRightParent = minRight;
						minRight = minRight->_left;
					}

					swap(cur->_key, minRight->_key);

					if (minRight == minRightParent->_left)
						minRightParent->_left = minRight->_right;
					else
						minRightParent->_right = minRight->_right;

					delete minRight;
				}

				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);
	}

private:
	Node* _root = nullptr;
};

4、两种模型

4.1 K模型

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值。例如:

  • 小区无人值守车库,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入
  • 检查⼀篇英文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示

4.2 key/value搜索场景

每一个关键码key,都有与之对应的值value,value可以任意类型对象。即<key,value>的键相对。例如:

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

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

将节点改成<key,value>类型,即键值对模型

cpp 复制代码
template<class K, class V>
struct BSTNode
{
	K _key;
	V _value;

	// pair<K, V> _kv;

	BSTNode<K, V>* _left;
	BSTNode<K, V>* _right;

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

二叉搜索树类模板

cpp 复制代码
	template<class K, class V>
	class BSTree
	{
		typedef BSTNode<K, V> Node;
	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 (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;
		}

		//查找
		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 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
				{
					// 准备删除
					if (cur->_left == nullptr)
					{
						if (cur != _root)
						{
							// 左为空,父亲指向我的右
							if (cur == parent->_left)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						else
						{
							_root = cur->_right;
						}

						delete cur;
					}
					else if (cur->_right == nullptr)
					{
						if (cur != _root)
						{
							// 右为空,父亲指向我的左
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						else
						{
							_root = cur->_left;
						}

						delete cur;
					}
					else
					{
						// 左右都不为空,找子树中适合的节点替代我
						Node* minRightParent = cur;
						Node* minRight = cur->_right;
						while (minRight->_left)
						{
							minRightParent = minRight;
							minRight = minRight->_left;
						}

						swap(cur->_key, minRight->_key);

						if (minRight == minRightParent->_left)
							minRightParent->_left = minRight->_right;
						else
							minRightParent->_right = minRight->_right;

						delete minRight;
					}

					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 << endl;
			_InOrder(root->_right);
		}

	private:
		Node* _root = nullptr;
	};
}

int main()
{
	string arr[] = { "苹果", "西瓜", "苹果","苹果","苹果","苹果", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	key_value::BSTree<string, int> t;
	for (auto& str : arr)
	{
		auto pNode = t.Find(str);
		if (pNode == nullptr)
		{
			t.Insert(str, 1);
		}
		else
		{
			pNode->_value++;
		}
	}
	t.InOrder();

	return 0;
}

输出结果:

香蕉:2

苹果:9

西瓜:2

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

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

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

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

相关推荐
哎呦,帅小伙哦1 小时前
C++工程实战: 预处理宏、宏重定义避坑、LOG_TAG业务实践与__COUNTER__深度解析
c++
IVVi0jToe1 小时前
高效C++线程池设计与实现
java·c++·安全
^yi1 小时前
【C++】内存管理
开发语言·c++
云栖梦泽2 小时前
从 IMX415 摄像头驱动理解 Media Controller 框架
linux·开发语言·c++·嵌入式硬件
C++ 老炮儿的技术栈2 小时前
MFC 自定义纯色居中文字进度条控件
c语言·数据库·c++·windows·算法·c·visual studio
researcher-Jiang2 小时前
Manacher算法学习与应用
c++·学习·算法
凯瑟琳.奥古斯特3 小时前
二分查找解力扣1011最优运载能力
开发语言·c++·算法·leetcode·职场和发展
WZF-Sang3 小时前
TCP和UDP协议
linux·服务器·网络·c++·学习·tcp/ip·udp
Tim_103 小时前
【C++】014、左值与右值的区别
开发语言·c++