【C++进阶】探秘二叉搜索树

头像
🚀个人主页:奋斗的小羊
🚀所属专栏:C++
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述


前言

本篇文章将介绍一种功能更加强大的二叉树——二叉搜索树。
相比于普通的二叉树,二叉搜索树在很多方面都有优势,尤其是在查找数据上效率明显提高,并且通过中序遍历二叉搜索树它所存储的数据是有序的。


💥一、二叉搜索树

💥1.1 特点

二叉搜索树,是一种特殊的二叉树结构,它或为空树,或者满足以下性质:

  • 左子树性质: 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
  • 右子树性质: 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
  • 中序遍历性质: 对二叉搜索树进行中序遍历(左根右),则遍历的结果是一个按升序排列的有序序列

在这里插入图片描述

二叉搜索树和普通二叉树的框架差不太多,需要一个节点类,再封装一个完成二叉搜索树功能的类就行。

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:
	//查找
	bool Find(const K& key)
	{}
	
	//插入
	bool Insert(const K& key)
	{}

	//删除
	bool Erase(const K& key)
	{}

	//中序遍历
	void InOrder()
	{}
private:
	//用一个节点指针来管理二叉搜索树,缺省值为nullptr
	Node* _root = nullptr;
};

💥1.2 基本操作

💥1.2.1 插入

  • 树为空: 直接新增节点,连接到根节点上
  • 树不为空: 从根节点开始,将要插入的值与根节点的值进行比较。如果小于根节点的值,则进入左子树继续比较;如果大于根节点的值,则进入右子树继续比较。如果当前节点为空,则在该位置插入新节点。

插入数据要始终保持二叉搜索树的特点。另外二叉搜索树中的数据通常不允许重复
,所以如果要插入的数据在二叉搜索树中已经存在,则插入错误。

| 实现:

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->_left;
		}
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}
	if (parent->_key > key)
	{
		parent->_left = new Node(key);
	}
	else
	{
		parent->_right = new Node(key);
	}
}

💥1.2.2 查找

从根开始比较查找,比根大则往右边查找,比根小则往左边查找。最多查找高度次,走到到空还没找到,这个值不存在。

| 实现:

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;
}
  • 查找过程的平均时间复杂度和最坏时间复杂度分别是O(log n)和O(n)

其中n是树中节点的数量。然而,在最坏的情况下(即树退化为链表时),时间复杂度会变为O(n)。为了保持查找效率,通常会使用平衡二叉搜索树(如AVL树、红黑树等),它们通过自平衡操作来确保树的高度保持在对数级别,在后续的文章中会介绍。

在这里插入图片描述


💥1.2.3 删除

在二叉搜索树中删除一个节点是一个稍微复杂的过程,因为它需要确保删除操作后树仍然保持二叉搜索树的属性。

  1. 查找要删除的节点: 首先,通过二叉搜索树的查找操作找到要删除的节点
  2. 处理三种情况:
  • 情况1: 如果要删除的节点是叶子节点,则直接删除该节点,并将其父节点指向它的指针设为nullptr
  • 情况2: 如果要删除的节点只有一个子节点(左子节点或右子节点),则将其父节点指向它的指针重定向到它的子节点,然后删除该节点
  • 情况3: 如果要删除的节点有两个子节点,则可以找到该节点的右子树中的最小节点(或左子树中的最大节点,但通常使用右子树中的最小节点,因为它更简单且保持了树的平衡),然后将该最小节点的值复制到要删除的节点中,并删除右子树中的那个最小节点(这实际上变成了情况1或情况2)

在这里插入图片描述

在这里插入图片描述

从上面两种不同的情况来看,找到目标节点右子树中的最小节点后,这个最小节点是它父节点的左孩子还是右孩子都是有可能的,所以在替换掉目标节点的值后,删除右子树中的最小节点之前,需要判断一下右子树中的最小节点的孩子是跟在其父节点的左还是右。

| 实现:

bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else//找到要删除的节点
		{
			//没有孩子或一个孩子
			if (cur->_left == nullptr)
			{
				if (_root->_key == key)
				{
					_root = _root->_right;
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else if (parent->_right == cur)
					{
						parent->_right = cur->_right;
					}
				}
				delete cur;
				cur = nullptr;
				return true;
			}
			else if (cur->_right == nullptr)
			{
				if (_root->_key == key)
				{
					_root = _root->_left;
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else if (parent->_right == cur)
					{
						parent->_right = cur->_left;
					}
				}
				delete cur;
				cur = nullptr;
				return true;
			}
			//有两个孩子
			else
			{
				//用要删除节点的右子树的最小节点代替删除节点
				parent = cur;
				Node* MinNode = cur->_right;
				while (MinNode->_left)
				{
					parent = MinNode;
					MinNode = MinNode->_left;
				}
				cur->_key = MinNode->_key;
				if (parent->_left == MinNode)
				{
					parent->_left = MinNode->_right;
				}
				else
				{
					parent->_right = MinNode->_right;
				}
				delete MinNode;
				return true;
			}
		}
	}
	return false;
}

找到要删除的节点后,删除之前有几个值得特别注意的点:

  • 要删除的节点没有孩子或只有一个孩子可以归为同一种情况处理
  • 要删除的节点可能是当前的根节点,如果是根节点要单独特殊处理,因为根节点没有父节点,和其他节点的删除处理方法不一样
  • 如果目标节点有两个孩子,则需要先找到其右子树中的最小节点,让最小节点的值替换掉目标节点的值,这个最小节点一定是叶子节点或者只有一个右孩子(两种情况可以视为同一情况处理),最小节点的右孩子要根据最小节点是其父节点的左孩子还是右孩子来确定接在哪里

💥1.2.4 中序遍历

二叉搜索树的中序遍历和普通二叉树的中序遍历是一样的。

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

但是这样写有一个问题,当我们调用中序遍历函数时,需要传一个二叉搜索树的根节点指针,而_rootBSTree类中的私有成员变量,在类外面不能使用,所以我们需要想一些办法。
既然类的私有成员变量在类外面不能使用,那我们就不在类外面使用了,可以通过函数调用在类里面间接的使用_root成员变量。

template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
public:
	//查找
	bool Find(const K& key)
	{}
	
	//插入
	bool Insert(const K& key)
	{}

	//删除
	bool Erase(const K& key)
	{}

	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	Node* _root = nullptr;
};

💥1.2.5 二叉搜索树的简单实现

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:
	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 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->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		if (parent->_key > key)
		{
			parent->_left = new Node(key);
		}
		else
		{
			parent->_right = new Node(key);
		}
	}

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else//找到要删除的节点
			{
				//没有孩子或一个孩子
				if (cur->_left == nullptr)
				{
					if (_root->_key == key)
					{
						_root = _root->_right;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						else if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				else if (cur->_right == nullptr)
				{
					if (_root->_key == key)
					{
						_root = _root->_left;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				//有两个孩子
				else
				{
					//用要删除节点的右子树的最小节点代替删除节点
					parent = cur;
					Node* MinNode = cur->_right;
					while (MinNode->_left)
					{
						parent = MinNode;
						MinNode = MinNode->_left;
					}
					cur->_key = MinNode->_key;
					if (parent->_left == MinNode)
					{
						parent->_left = MinNode->_right;
					}
					else
					{
						parent->_right = MinNode->_right;
					}
					delete MinNode;
					return true;
				}
			}
		}
		return false;
	}

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

💥1.3 默认成员函数

💥1.3.1 拷贝构造

二叉搜索树不需要我们显示去写构造,根节点指针给缺省值nullptr,用编译器默认生成的构造就可。而拷贝构造需要我们自己去实现深拷贝,因为编译器默认生成的拷贝构造是浅拷贝,不符合我们的要求。

但是,我们知道编译器生成默认构造的规则是:我们不写构造函数,编译器就会生成一个默认构造函数,而拷贝构造也是构造,我们写了拷贝构造后编译器就不再生成默认构造了,所以我们还是需要写一个默认构造,只需要形式的写一个就行。可以考虑用:BSTree() = default;。当我们使用 =default 时,意思是告诉编译器:“我知道这个成员函数可以有默认实现,请帮我生成它。”

二叉搜索树的拷贝构造用递归实现会比较简单,需要传根节点的指针,而拷贝构造函数我们知道传过来的是对象的引用,所以我们可以再包装一下,写一个拷贝函数。

//默认构造
BSTree() = default;

//拷贝构造
BSTree(const BSTree<K>& t)
{
	_root = copy(t._root);
}

Node* copy(Node* root)
{
	if (root == nullptr)
	{
		return nullptr;
	}
	Node* NewRoot = new Node(root->_key);
	NewRoot->_left = copy(root->_left);
	NewRoot->_right = copy(root->_right);
	return NewRoot;
}

走的是后序遍历拷贝二叉树。


💥1.3.2 析构

同样的,析构函数不需要传参数,而我们想析构二叉搜索树就必须传根节点指针,那么也可以用销毁节点的函数再包装一下。

~BSTree()
{
	Destroy(_root);
	_root = nullptr;
}

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

走的也是后序遍历析构二叉搜索树。


💥1.4 二叉搜索树的应用

💥1.4.1 Key模型

在K模型中,每个节点仅存储一个关键码(即Key),这个关键码用于确定节点在树中的位置,并满足二叉搜索树的性质。

  • 查找: 给定一个关键码,在二叉搜索树中查找是否存在对应的节点。查找过程从根节点开始,根据关键码与节点关键码的比较结果,决定是向左子树查找还是向右子树查找,直到找到对应的节点或确定不存在该关键码。
  • 应用场景: 可以用来拼写检查, 在文本编辑器或浏览器中,可以使用二叉搜索树的K模型来存储字典中的单词,以便快速检查用户输入的单词是否存在拼写错误。

我们上面实现的二叉搜索树就是一个简单的Key模型。


💥1.4.2 Key-Value模型

定义:

  • KV模型中的每个节点包含一个键值(Key)和一个数据值(Value),键值用于确定节点在树中的位置,数据值用于存储节点的附加信息
  • 节点的键值仍然必须满足二叉搜索树的性质,但数据值可以是任意类型或对象

应用场景:

  • 简单词典: 在英汉词典中,英文单词作为Key,其对应的中文解释作为Value。用户可以通过输入英文单词快速找到其对应的中文含义
  • 车库收费: 车进入车库时存储车牌号作为Key,进入的时间作为Value,当车从车库出来时按车牌号查找对应进入车库的时间
  • 计数器: 在需要统计元素出现次数的场景中,可以将元素作为Key,其出现的次数作为Value

这里我们以上面实现的二叉搜索树为基础,实现一个简单词典的KV模型。

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:
	BSTree() = default;
	
	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;
	}
	
	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}
	
	void Destroy(Node* root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
	}
	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 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->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		if (parent->_key > key)
		{
			parent->_left = new Node(key, value);
		}
		else
		{
			parent->_right = new Node(key, value);
		}
	}

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else//找到要删除的节点
			{
				//没有孩子或一个孩子
				if (cur->_left == nullptr)
				{
					if (_root->_key == key)
					{
						_root = _root->_right;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						else if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				else if (cur->_right == nullptr)
				{
					if (_root->_key == key)
					{
						_root = _root->_left;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				//有两个孩子
				else
				{
					//用要删除节点的右子树的最小节点代替删除节点
					parent = cur;
					Node* MinNode = cur->_right;
					while (MinNode->_left)
					{
						parent = MinNode;
						MinNode = MinNode->_left;
					}
					cur->_key = MinNode->_key;
					if (parent->_left == MinNode)
					{
						parent->_left = MinNode->_right;
					}
					else
					{
						parent->_right = MinNode->_right;
					}
					delete MinNode;
					return true;
				}
			}
		}
		return false;
	}

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

请添加图片描述


💥1.4.3 性能分析

KV模型的二叉搜索树在查找、插入和删除操作上的性能通常取决于树的高度。在最坏的情况下(即树蜕化为单边树时),这些操作的时间复杂度会退化为O(n)。
可以使用平衡二叉搜索树来提高性能:

  • AVL树: 任何节点的两个子树的高度最大差别为一,这通过自平衡操作(如左旋、右旋)来维持
  • 红黑树: 一种自平衡二叉查找树,它通过每个节点附加一个表示颜色的位(红色或黑色)和通过确保树满足某些性质(如从根到叶子的最长的可能路径不多于最短的可能路径的两倍长)来保持平衡

关于平衡二叉搜索树的介绍还请在我专栏 C++ 中查找相关文章。
在平均情况下,这些操作的时间复杂度可以接近O(log n),这使得KV模型的二叉搜索树成为一种高效的数据结构。


本篇文章的学习就到这了,如果您觉得在本文中有所收获,还请留下您的三连支持哦~

头像
评论 302
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值