【高阶数据结构】二叉搜索树的插入、删除和查找(精美图解+完整代码)

news2024/12/28 18:44:43

🤡博客主页:醉竺

🥰本文专栏:《高阶数据结构》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨ 


目录

1. 二叉查找树的概念和性质

2. 二叉查找树的查找

2.1 递归代码实现

2.2 非递归代码实现

2.3 查找时间复杂度分析 

3. 二叉查找树的插入

3.1 递归代码实现 

3.2 非递归代码实现 

4. 二叉查找树的删除 (难点) 

(1) 被删除节点左子树为空

(2) 被删除节点右子树为空

(3) 被删除节点左右子树均不空

(4) 上述情况具体图解示例 

4.1 递归代码实现

4.2 非递归代码实现

5. 完整代码提供和运行结果 

5. 二叉查找树的其它操作(了解)

6. 二叉查找树的实际应用


1. 二叉搜索树的概念和性质

二叉查找树(BinarySearchTree,BST),又称为二叉查找树、二叉排序树,是一种对查找和排序都有用的特殊二叉树。存在的意义在于实现快速查找,同时,它也支持快速插入和删除。它是怎么做到这些的呢?

这些都依赖于二叉查找树的特殊结构。二叉搜索树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于该节点的值,而右子树中每个节点的值都大于该节点的值。 当然,左、右子树本身也是一棵二叉查找树。

下面几个树就是二叉搜索树,你一看应该就清楚了。

二叉搜索树的特性:左子树 < 根 < 右子树,即如果对二叉搜索树进行中序遍历,得到的结果就是一个有序的递增序列,也就是说内部存储的数据是已经排好序的,所以它也叫做二叉排序树(Binary Sort Tree)。
上图中的二叉搜索树按中序遍历序列,第一棵为“3,4,5,6,9,11”,第二棵为“8,11,12,17,19,23”,第三棵为“8,10 ,13,15,22”。   

二叉搜索树的性质可以总结如下。

二叉搜索树或是空树,或是满足如下性质的二叉树:

1)若其左子树非空,则左子树上所有节点的值均小于根节点的值。 

2)若其右子树非空,则右子树上所有节点的值均大于根节点的值。

3)其左右子树本身又各是一棵二叉查找树。 

下面,先看一看二叉搜索树的类模板定义代码,分为每个节点的定义,以及二叉搜索树的定义两个部分。  为后续相关操作做铺垫。

//树中每个节点的定义
template<class K> //K代表数据元素的类型
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

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

//二叉搜索树的定义
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

private:
	Node* _root = nullptr;

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

	~BSTree()
	{
		Destroy(_root);
	}

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

	// t1 = t3
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

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

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;

		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}

	//二叉树中序遍历代码(排序),方便测试时显示节点数据
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
};

2. 二叉搜索树的查找

因为二叉搜索树的中序遍历有序性,所以查找和二分查找类似,每次缩小查找范围,查找的效率较高。

图解查找步骤: 

例如,一棵二叉搜索树,如图 2-1 所示,查找关键字32。

 1)32 与二叉搜索树的树根 25 比较,32 > 25,则在右子树中查找,如图 2-2 所示。

2)32 与右子树的树根 69 比较,32<69,则在左子树中查找,如图 2-3 所示。

3)32 与左子树的树根 32 比较,相等,查找成功,返回该节点指针,如图 2-4 所示。 

2.1 递归代码实现

算法步骤:

1)若二叉搜索树为空,查找失败,返回空指针。

2)若二叉搜索树非空,将 key 与根节点的关键字 root->_key 比较:

•  若key == root->_key,查找成功,返回根节点指针;

•  若key > root->_key,则递归查找右子树。

•  若key < root->_key,则递归查找左子树; 

bool FindR(const K& key)
{
	return _FindR(_root, key);
}

bool _FindR(Node* root, const K& key)
{
	if (root == nullptr)
	{
		return false;
	}

	if (key > root->_key)
	{
		return _FindR(root->_right, key);
	}
	else if (key < root->_key)
	{
		return _FindR(root->_left, key);
	}
	else
	{
		return true;
	}
}

2.2 非递归代码实现

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。

b、最多查找高度次,走到空还没找到,这个值不存在。

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

2.3 查找时间复杂度分析 

我们前面说过,二叉搜索树的意义在于实现快速查找。无论对二叉搜索树做何种操作,首先把进行操作的节点找到才是最重要的。因此,这里的时间复杂度分析主要针对的是节点的查找操作。

先说 查找长度。在查找操作中,需要对比的节点次数就是查找长度,它反映了查找操作的时间复杂度。

图7的左侧是一棵满二叉树,如果要查找50这个节点,则需要分别与60、40、50这三个节点做对比,这意味着50这个节点的查找长度为3。而图7右侧这棵失衡的二叉树(斜树),要查找50这个节点,则需要分别与90、80、70、60、50这5个节点做对比,这意味着50这个节点的查找长度为5。

我们再引申到 平均查找长度ASL(Average Search Length)。它可以用来衡量整个二叉查找树的查找效率。

  • 上图 左侧图,查找节点60,查找长度为1,如果查找40、80这两个节点,查找长度为2,如果查找30、50、70、90这四个节点,查找长度为3。又因为图中有7个节点,所以所有节点的平均查找长度ASL = (1*1 + 2*2 + 3*4)/ 7 = 2.42。

  • 上图 右侧图,同理,ASL = (1*1 + 2*1 + 3*1 +4*1 + 5*1 + 6*1 + 7*1)/ 7 = 4。

可以看到,虽然图中2棵二叉查找树存储的数据相同,但 左侧的查找效率显然更高

刚刚是查找节点成功时的平均查找长度,那么查找节点失败时的平均查找长度该如何计算呢?我们将图中的二叉树变为扩展二叉树。

可以看到,如果查找失败,则最终的查找位置会停留在带有#标记的扩展节点上。

  • 图8左侧图,带有#标记的扩展节点一共是8个,也就是说查找节点时需要对比的3次节点值的情形是8种。所以查找节点失败时的平均查找长度ASL = (3*8)/ 8 = 3。

  • 图8右侧图,带有#标记的扩展节点一共是8个,同理,查找节点时需要对比1次节点值的情形是1种,需要对比2次节点值的情形是1种,以此类推。所以查找节点失败时的平均查找长度ASL = (1*1+2*1+3*1+4*1+5*1+6*1+7*2)/8 = 4.375。

显然,即便是查找节点失败时的平均查找长度,图7左侧二叉查找树的查找效率也是更高的。

不难看出, 查找长度与树的高度是成正比的,也就是说,二叉查找树的查找效率主要取决于树的高度。在查找操作中,需要对比的节点次数一定不会超过该树的高度。  

  • 如果是一棵满二叉树或者完全二叉树,那么根据二叉树的性质五,该二叉树的高度为\left \lfloor log{2}^{n} \right \rfloor+1。换句话说,对于有n个节点的二叉树,它的最小高度是\left \lfloor log{2}^{n} \right \rfloor+1,这意味着查找操作最好情况时间复杂度为O(log{2}^{n})(n代表该二叉树的节点数量)。
  • 如果一棵二叉树的高度和节点数相同,也就是一棵斜树,其高度为n,这意味着查找操作最坏情况时间复杂度为O(n), 看起来已经是一个链表了。

那么为了提高查找效率,应该尽可能地让二叉查找树的高度变得最小(尽可能接近\left \lfloor log{2}^{n} \right \rfloor+1)。也就是说,在创建二叉查找树时,应该尽可能让该二叉查找树保持左右节点的平衡,从而引出平衡二叉树的概念。所谓平衡二叉树,就是该树上任意节点的左子树和右子树深度之差不超过1。后续文章会讲解平衡二叉树。

总之有:

二叉查找树的查找时间复杂度和树的形态有关,分为最好情况、最坏情况和平均情况分析。

•  最好:二叉查找树的形态和二分查找的判定树相似,时间复杂度为O(logn)

•  最坏:二叉排序查找树的形态为单支树,退化为顺序查找,时间复杂度为O(n)

•  平均: n个节点的二叉查找树有n!棵(有的形态相同),平均情况下,时间复杂度为O(logn)


3. 二叉搜索树的插入

因为二叉搜索树的中序遍历有序性,首先要查找待插入关键字的插入位置,当查找不成功时,将待插入关键字作为新的叶子节点插入到最后一个查找节点的左孩子或右孩子。

算法步骤:

1)若二叉搜索树为空,则直接新增节点,赋值给root指针作为根节点,数据域为key,左右子树均为空。

2)若二叉搜索树非空,按二叉搜索数性质查找插入位置,插入新节点。即将key与根节点的关键字root->_key比较:

•  若key > root->_key,则将key 插入右子树;

•  若key < root->_key,则将key 插入左子树。

图解步骤: 

例如,一棵二叉搜索树,如下图所示,插入关键字30。 

1)30与树根25比较,30>25,则在25的右子树中查找,如图3-1所示。

2)30与右子树的树根69比较,30<69,则在69的左子树中查找,如图3-2所示。 

3)30与左子树的树根32比较,30<32,则在32的左子树中查找,如图3-3所示。

4)32的左子树为空,则将30作为新的叶子节点,插入32的左子树,如图3-4所示。 

3.1 递归代码实现 

bool InsertR(const K& key)
{
	return _InsertR(_root, key);
}

bool _InsertR(Node*& root, const K& key) // 注意第一个参数类型
{
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}

	if (key > root->_key)
		return _InsertR(root->_right, key);
	else if (key < root->_key)
		return _InsertR(root->_left, key);
	else
		return false;
}

3.2 非递归代码实现 

bool Insert(const K& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		parent = cur;

		if (key > cur->_key)
		{
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(key);
	if (key > parent->_key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

	return true;
}

4. 二叉搜索树的删除 (难点) 

二叉搜索树的删除操作相对要更复杂一些,针对所要删除的节点的子节点个数不同,有几种情况需要处理。

首先要在二叉查找树中找到待删除的节点,然后执行删除操作。假设指针p 指向待删除节点,指针f 指向 p 的双亲节点。根据待删除节点所在位置的不同,删除操作处理方法也不同,可分为3种情况:

(1) 被删除节点左子树为空

        如果被删除节点左子树为空,则令其右子树子承父业代替其位置即可。例如,在二叉查找树中删除P节点,如图4-1所示。

(2) 被删除节点右子树为空

        如果被删除节点右子树为空,则令其左子树子承父业代替其位置即可,如图4-2所示。

(3) 被删除节点左右子树均不空

        如果被删除节点的左子树和右子树均不空,则没办法再使用子承父业的方法了。根据二叉查找树的中序有序性,删除该节点时,可以用其直接前驱(或直接后继)的值替换到要删除的节点上,然后再删除其直接前驱(或直接后继)即可。

那么中序遍历序列中,一个节点的直接前驱(或直接后继)是哪个节点呢?  

        直接前驱:中序遍历中,节点 p 的直接前驱为其左子树的最右节点。即沿着 p 的左子树一直访问其右子树,直到没有右子树,就找到了最右节点,如图4-3(a) 所示。s 指向 p 的直接前驱,q 指向 s 的双亲。

        直接后继:中序遍历中,节点 p 的直接后继为其右子树的最左节点,如图4-3(b) 所示。s 指向p 的直接后继,q 指向s 的双亲。 

        以 p 的直接前驱 s 代替 p 为例,相当于令 s 节点的数据赋值给 p 节点,即 s 代替 p。然后删除 s 节点即可,因为 s 为最右节点,它没有右子树,删除后,左子树子承父业代替 s,如图4-4 所示。 

        例如,在二叉搜索树中删除 24。首先查找到 24 的位置 p,然后找到 p 的直接前驱 s(22)节点,令 22 赋值给 p 的数据域,删除 s 节点,删除过程如图4-5 所示。 

        删除节点之后是不是仍然满足二叉查找树的中序遍历有序性?
        需要注意的是,有一种特殊情况,即 p 的左孩子没有右子树,s 就是其左子树的最右节点(直接前驱),即 s 代替 p,然后删除 s 节点即可,因为 s 为最右节点没有右子树,删除后,左子树子承父业代替 s,如图4-6 所示。 

例如,在二叉搜索树中删除20,删除过程如图4-7 所示 


二叉查找树算法步骤:  

(4) 上述情况具体图解示例 

下面是上述情况具体图解例子:
情况(0),要删除的节点左右子树为空 :(情况0上述我并没有单独列出,因为此情况可以并入左子树为空或者右子树的情况内。因为无论用被删除节点的左子树还是右子树“子承父业”,都是空nullptr,因此可以并入上述(1)或者(2),不影响最后结果。)

情况(1)要删除的节点的左子树为空:

        在二叉搜索树中删除 32,首先查找到 32 所在的位置,判断其左子树为空,则令其右子树子承父业代替其位置,删除过程如图4-8 所示。 

情况(2)要删除的节点的右子树为空:

         在二叉搜索树中删除 69,首先查找到 69 所在的位置,判断其右子树为空,则令其左子树子承父业代替其位置,删除过程如图4-9 所示。

情况(3)要删除的节点的左右子树均不空:

        在二叉搜索树中删除 25,首先查找到 25 所在的位置,判断其左右子树均不空,则令其直接前驱(左子树最右节点 20)代替之,再删除其直接前驱 20 即可。删除 20 时,其左子树子承父业,删除过程如图4-10所示。 

4.1 递归代码实现

bool EraseR(const K& key)
{
	return _EraseR(_root, key);
}

bool _EraseR(Node*& root, const K& key) //注意第一个参数类型
{
	if (root == nullptr)
		return false;

	if (key > root->_key)
	{
		return _EraseR(root->_right, key);
	}
	else if (key < root->_key)
	{
		return _EraseR(root->_left, key);
	}
	else // 找到了节点,执行删除操作:
	{
		// 即将被删除节点的左孩子为空 (或者即将被删除节点的左孩子和右孩子都为空)
		if (root->_left == nullptr)
		{
			Node* del = root;
			root = root->_right;
			delete del;

			return true;

		}
		else if (root->_right == nullptr) //即将被删除节点的右孩子为空 (或者即将被删除节点的左孩子和右孩子都为空)
		{
			Node* del = root;
			root = root->_left;
			delete del;

			return true;
		}
		else // 即将被删除节点的左右孩子都不为空
		{
			Node* subLeft = root->_right;
			while (subLeft->_left)
			{
				subLeft = subLeft->_left; // 被删除节点的右子树的最左节点(最小节点)
			}

			swap(root->_key, subLeft->_key); 

			// 转换成在子树去递归删除
			return _EraseR(root->_right, key);
		}
	}
}

4.2 非递归代码实现

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_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* parent = cur;
				Node* subLeft = cur->_right;
				while (subLeft->_left)
				{
					parent = subLeft;
					subLeft = subLeft->_left;
				}

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

				if (subLeft == parent->_left)
					parent->_left = subLeft->_right;
				else
					parent->_right = subLeft->_right;

				delete subLeft;
			}

			return true;
		}
	}

	return false;
}

5. 完整代码提供和运行结果 

这里提供完整的代码实现,可以复制粘贴到编译器上运行调试:

BinarySearchTree.h 

#pragma once
#include<iostream>
using namespace std;

// 树中每个节点的定义
template<class K> // K代表数据元素类型
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

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

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

private:
	Node* _root = nullptr;

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

	~BSTree()
	{
		Destroy(_root);
	}

	BSTree(const BSTree<K>& t)
	{
		_root = Copy(t._root);
	}

	// t1 = t3
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

private:
	void Destroy(Node*& root) // 加引用是为了最后能够让真正的实参根节点也能置空。不加引用也可以,但是"root = nullptr;"这句代码就无效了。
	{
		if (root == nullptr)
			return;

		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}

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

public:
	// 二叉查找树的查找(非递归实现)
	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;
	}

	// 二叉查找树的插入(非递归实现)
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			parent = cur;

			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

	// 二叉查找树的删除(非递归实现)
	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_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* parent = cur;
					Node* subLeft = cur->_right;
					while (subLeft->_left)
					{
						parent = subLeft;
						subLeft = subLeft->_left;
					}

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

					if (subLeft == parent->_left)
						parent->_left = subLeft->_right;
					else
						parent->_right = subLeft->_right;

					delete subLeft;
				}

				return true;
			}
		}

		return false;
	}

public:
	// 二叉查找树的中序遍历(排序)递归实现,方便测试时显示节点数据
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 二叉查找树的查找(递归实现)
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	// 二叉查找树的插入(递归实现)
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

	// 二叉查找树的删除(递归实现)
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (key > root->_key)
		{
			return _FindR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return true;
		}
	}

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (key > root->_key)
			return _InsertR(root->_right, key);
		else if (key < root->_key)
			return _InsertR(root->_left, key);
		else
			return false;
	}

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _EraseR(root->_left, key);
		}
		else
		{
			// 删除
			if (root->_left == nullptr)
			{
				Node* del = root;
				root = root->_right;
				delete del;

				return true;

			}
			else if (root->_right == nullptr)
			{
				Node* del = root;
				root = root->_left;
				delete del;

				return true;
			}
			else
			{
				Node* subLeft = root->_right;
				while (subLeft->_left)
				{
					subLeft = subLeft->_left;
				}

				swap(root->_key, subLeft->_key);

				// 转换成在子树去递归删除
				return _EraseR(root->_right, key);
			}
		}
	}
};

Test_BinarySearchTree.h  

#include"BinarySearchTree_K.h"

// 测试非递归的主要操作
void test()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;

	for (auto e : a)
	{
		bt.Insert(e);
	}
	bt.InOrder();

	cout << bt.Find(6) << endl; // true(1)
	cout << bt.Find(666) << endl; // false(0)

	bt.Erase(14);
	bt.InOrder();

	bt.Erase(3);
	bt.InOrder();

	bt.Erase(8);
	bt.InOrder();
}

// 测试递归的主要操作
void testR()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;

	for (auto e : a)
	{
		bt.InsertR(e);
	}
	bt.InOrder();

	cout << bt.FindR(6) << endl; // true(1)
	cout << bt.FindR(666) << endl; // false(0)

	bt.EraseR(14);
	bt.InOrder();

	bt.EraseR(3);
	bt.InOrder();

	bt.EraseR(8);
	bt.InOrder();
}

// 测试拷贝构造和赋值运算符重载
void testBase()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.InsertR(e);
	}
	bt.InOrder();

	BSTree<int> cp1(bt);
	cp1.InOrder();

	BSTree<int> cp2;
	cp2 = bt;
	cp2.InOrder();
}

int main()
{
	test();
	cout << "----------------------------" << endl;
	testR();
	cout << "----------------------------" << endl;
	testBase();

	return 0;
}

运行结果:


5. 二叉搜索树的其它操作(了解)

接下来,我再为你补充一些二叉搜索树的其他常用操作。

  • 查找值最大 最小的节点

//查找值最大节点
Node* SearchMaxValuePoint()
{
	return SearchMaxValuePoint(root);
}

Node* SearchMaxValuePoint(Node* tNode)
{
	if (tNode == nullptr) //空树
		return nullptr;

	//从根节点开始往右侧找即可
	Node* tmpnode = tNode;
	while (tmpnode->_right != nullptr)
		tmpnode = tmpnode->_right;
	return tmpnode;
}

//查找值最小节点
Node* SearchMinValuePoint()
{
	return SearchMinValuePoint(root);
}

Node* SearchMinValuePoint(Node* tNode)
{
	if (tNode == nullptr) //空树
		return nullptr;

	//从根节点开始往左侧找即可
	Node* tmpnode = tNode;
	while (tmpnode->_left != nullptr)
		tmpnode = tmpnode->_left;
	return tmpnode;
}
  • 找出中序遍历序列中当前节点的前趋和后继节点

解决这个问题的方法有很多,书写的程序代码也各不相同。如果每个节点要有一个指向父节点的指针,那么解决起来可能更容易一些,如果没有指向父节点的指针,那么一般就要从根节点开始找起。

但不管怎样,一定要把握住两个原则。

  1. 当前节点的前趋节点一定是比当前节点值小的,也是再往前的一系列节点中最大的。

  2. 当前节点的后继节点一定是比当前节点值大的,也是再往后的一系列节点中节点值最小的。

//找按中序遍历的二叉查找树中当前节点的前趋节点
Node* GetPriorPoint_IO(Node* findnode)
{
	if (findnode == nullptr)
		return nullptr;

	Node* prevnode = nullptr;
	Node* currnode = root;  //当前节点,从根开始找
	while (currnode != nullptr)
	{
		if (currnode->_key < findnode->_key) //当前节点小
		{
			//(1)从一系列比当前要找的值小的节点中找一个值最大的当前趋节点
			//当前节点值比要找的  节点值小,所以当前节点认为有可能是前趋
			if (prevnode == nullptr)
			{
				//如果前趋节点还为空,那不防把当前节点认为就是前趋
				prevnode = currnode;
			}
			else //prevnode不为空
			{
				//既然是找前趋,那自然是找到比要找的值小的 一系列节点中 值最大的
				if (prevnode->_key < currnode->_key)
				{
					prevnode = currnode; //前趋自然是找一堆 比当前值小的 值中 最大的一个。
				}
			}
			//(2)继续逼近要找的节点,一直到找到要找的节点,找到要找的节点后,要找的节点的左节点仍旧可能是前趋
			currnode = currnode->_right;  //当前节点小,所以往当前节点的右子树转
		}

		else if (currnode->_key > findnode->_key) //当前节点值比要找的值大,所以当前节点肯定不会是要找的值的前趋
		{
			//当前节点大,所以往当前节点的左子树转
			currnode = currnode->_left;
		}

		else //(currnode->_key == findnode->_key) ,这个else其实可以和上个else合并,但为了清晰,就不合并了
		{
			//当前节点值  就是要找的节点值,那么 前趋也可能在当前节点的左子树中,所以往左子树转继续找看有没有更合适的前趋
			currnode = currnode->_left;
		}
	} //end while

	return prevnode;
}
//找按中序遍历的二叉查找树中当前节点的后继节点
Node* GetNextPoint_IO(Node* findnode)
{
	if (findnode == nullptr)
		return nullptr;

	Node* nextnode = nullptr;
	Node* currnode = root;  //当前节点,从根开始找
	while (currnode != nullptr)
	{
		if (currnode->_key > findnode->_key) //当前节点大
		{
			//(1)从一系列比当前要找的值大的节点中找一个值最小的当后继节点
			//当前节点值比要找的  节点值大,所以当前节点认为有可能是后继
			if (nextnode == nullptr)
			{
				//如果后继节点还为空,那不防把当前节点认为就是后继
				nextnode = currnode;
			}
			else //nextnode不为空
			{
				//既然是找后继,那自然是找到比要找的值大的 一系列节点中 值最小的
				if (nextnode->_key > currnode->_key)
				{
					nextnode = currnode; //后继自然是找一堆 比当前值大的 值中 最小的一个。
				}
			}
			//(2)继续逼近要找的节点,一直到找到要找的节点,找到要找的节点后,要找的节点的右节点仍旧可能是后继
			currnode = currnode->_left;  //当前节点大,所以往当前节点的左子树转
		}

		else if (currnode->_key < findnode->_key) //当前节点值比要找的值小,所以当前节点肯定不会是要找的值的后继
		{
			//当前节点小,所以往当前节点的右子树转
			currnode = currnode->_right;
		}

		else //(currnode->_key == findnode->_key) 
		{
			//当前节点值  就是要找的节点值,那么 后继也可能在当前节点的右子树中,所以往右子树转继续找看有没有更合适的后继
			currnode = currnode->_right;
		}
	} //end while
	return nextnode;
}

6. 二叉搜索树的实际应用

1. K模型K模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到的值。 上述所有操作就是K模型。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树。
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。 

2. KV模型每一个关键码 key,都有与之对应的值 Value,即<Key,Value>的键值对。该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文 <word,chinese> 就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word,count>就构成一种键值对。

这里提供一下KV模型的代码仅供学习参考,跟上述K模型基本一样,只是模板多了一个参数V。

BianrySearchTree_KV.h 

#pragma once
#include<iostream>
using namespace std;

// 树中每个节点的定义
template<class K, class V> // K V代表数据元素类型
struct BSTreeNode
{
	BSTreeNode<K, V>* _left;
	BSTreeNode<K, V>* _right;
	K _key;
	V _value;

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

template<class K, class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;

private:
	Node* _root = nullptr;

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

	~BSTree()
	{
		Destroy(_root);
	}

	BSTree(const BSTree<K, V>& t)
	{
		_root = Copy(t._root);
	}

	// t1 = t3
	BSTree<K, V>& operator=(BSTree<K, V> t)
	{
		swap(_root, t._root);
		return *this;
	}

private:
	void Destroy(Node*& root) // 加引用是为了最后能够让真正的实参根节点也能置空。不加引用也可以,但是"root = nullptr;"这句代码就无效了。
	{
		if (root == nullptr)
			return;

		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}

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

public:
	// 二叉查找树的中序遍历(排序)递归实现,方便测试时显示节点数据
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 二叉查找树的查找(递归实现)
	Node* FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	// 二叉查找树的插入(递归实现)
	bool InsertR(const K& key, const V& value)
	{
		return _InsertR(_root, key, value);
	}

	// 二叉查找树的删除(递归实现)
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << ":" << root->_value << endl;
		_InOrder(root->_right);
	}

	Node* _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		if (key > root->_key)
		{
			return _FindR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return root;
		}
	}

	bool _InsertR(Node*& root, const K& key, const V& value)
	{
		if (root == nullptr)
		{
			root = new Node(key, value);
			return true;
		}

		if (key > root->_key)
			return _InsertR(root->_right, key, value);
		else if (key < root->_key)
			return _InsertR(root->_left, key, value);
		else
			return false;
	}

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _EraseR(root->_left, key);
		}
		else
		{
			// 删除
			if (root->_left == nullptr)
			{
				Node* del = root;
				root = root->_right;
				delete del;

				return true;

			}
			else if (root->_right == nullptr)
			{
				Node* del = root;
				root = root->_left;
				delete del;

				return true;
			}
			else
			{
				Node* subLeft = root->_right;
				while (subLeft->_left)
				{
					subLeft = subLeft->_left;
				}

				swap(root->_key, subLeft->_key);

				// 转换成在子树去递归删除
				return _EraseR(root->_right, key);
			}
		}
	}
};

Test_BianrySearchTree_KV.h  

#include"BinarySearchTree_KV.h"

// 测试词典
void testDict()
{
	BSTree<string, string> dict;
	dict.InsertR("sort", "排序");
	dict.InsertR("left", "左边");
	dict.InsertR("right", "右边");
	dict.InsertR("insert", "插入");
	dict.InsertR("key", "关键词");

	string str;
	while (cin >> str)
	{
		BSTreeNode<string, string>* ret = dict.FindR(str);
		if (ret)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "没有此关建词!" << endl;
		}
	}
}

// 测试统计次数
void testCount()
{
	string arr[] = { "苹果", "栗子", "苹果", "苹果", "栗子", "木瓜", "荔枝", "葡萄", "木瓜", "西瓜", "桃子", "橘子", "西瓜" };
	BSTree<string, int> countTree;
	for (auto& e : arr)
	{
		BSTreeNode<string, int>* ret = countTree.FindR(e);
		if (ret == nullptr)
		{
			countTree.InsertR(e, 1);
		}
		else
		{
			ret->_value++;
		}
	}

	countTree.InOrder();
}

int main()
{
	// testDict();
	testCount();

	return 0;
}

运行结果: 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2151357.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【鸿蒙】HarmonyOS NEXT开发快速入门教程之ArkTS语法装饰器(上)

文章目录 前言一、ArkTS基本介绍1、 ArkTS组成2、组件参数和属性2.1、区分参数和属性的含义2.2、父子组件嵌套 二、装饰器语法1.State2.Prop3.Link4.Watch5.Provide和Consume6.Observed和ObjectLink代码示例&#xff1a;示例1&#xff1a;&#xff08;不使用Observed和ObjectLi…

Windows11家庭版修改用户密码策略为永不过期。

今天有个朋友找到我说&#xff0c;他的电脑密码老是过期然后需要修改&#xff0c;让我帮忙改一下密码策略&#xff0c;改为永不过期。 下面就来操作一下吧。 这里有个小小的坑&#xff0c;就是win11的家庭版是没有 gpedit.msc的&#xff0c;也就不能直接cmd打开本地策略便器&…

【WebGis开发 - Cesium】获取视野中心点,并设置顶视图视角

引言 项目开发过程中遇到一个需求&#xff0c;通过一个按钮切换视角为顶视图。 分析了一下这个模糊的需求&#xff0c;首先没有给出切换顶视图后俯视的区域范围&#xff0c;其次没有给出俯视点的高度。 这里可以粗略的认为当前的侧俯视的角度下观看的范围即为俯视的区域范围&am…

视频美颜SDK核心功能解析:打造高效直播美颜工具方案详解

随着直播行业的迅猛发展&#xff0c;用户对于直播画质和个人形象的要求越来越高。视频美颜SDK作为一项关键技术&#xff0c;已经成为各大直播平台和短视频应用的重要组成部分。通过实时美颜技术&#xff0c;用户能够在直播过程中呈现出更加理想的形象&#xff0c;从而提升直播体…

实验一:Windows下的IIS服务器配置和管理

第一次实验隐藏关很多&#xff0c;稍不留神服务器就寄了。 实验一完成后会有联网问题&#xff0c;问题解决详见番外篇。 实验内容 任务一&#xff1a; 1、建立一个基于主机名www.study.com的站点&#xff0c;站点的主目录为C:\inetpub\wwwroot&#xff0c;给站点建立一个虚拟…

Codeforces Round 973 (Div. 2) F1. Game in Tree (Easy Version)(思维题 博弈)

题目 思路来源 乱搞ac 题解 两个人的策略是一样的&#xff0c;把1到u的路径标记&#xff0c; 如果能走旁边的链&#xff08;也就是当前点&#xff0c;刨去标记链以外的子树中最长的链&#xff09;&#xff0c; 使得对面走剩余的连通块无法比你大&#xff0c;就走旁边的链&…

业务资源管理模式语言16

示例&#xff1a; 图25 描述了PayForTheResourceTransaction 的一个实例。其中&#xff0c;“Sale”扮演“Resource Transaction”&#xff0c;“Accounts Receivable”扮演“Payment”。 图25——PayForTheResourceTransaction 模式实例 相关模式&#xff1a; PayForTheRes…

特殊类的设计与类型转换

特殊类的设计 1.请设计一个不能被拷贝的类 拷贝只会放生在两个场景中&#xff1a;拷贝构造函数以及赋值运算符重载&#xff0c;因此想要让一个类禁止拷贝&#xff0c;只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。 C98 C98是怎么设计的呢&#xff1f; class Cop…

从零开始讲DDR(3)——DDRC与DDRPYH

一、DDR的使用 在之前的文章中我们介绍了DDR的基本概念&#xff0c;但是DDR内存的操作不仅仅是简单的数据读取和写入&#xff0c;它包括许多时序要求和信号调度。为了让DDR内存有效运作&#xff0c;系统需要在逻辑层和物理层之间进行大量的协作。我们拿出一张DDR的操作简化状态…

MySQL程序

目录 MySQL程序 常用的MySQL的程序 mysqld程序 mysql客户端 客户端命令的常用的选项 配置文件 配置文件语法 MySQL客户端命令 ​编辑 .sql 文件中执行SQL语句 mysqlcheck &#xff08;表维护程序&#xff09; Mysqldump&#xff08;数据库备份程序&#xff09; mysql…

单片机项目合集列表——Excel合集列表目录查阅(持续更新)

阿齐Archie《单片机项目合集》专栏项目 为方便查找本专栏的项目&#xff0c;特整理Excel合集列表供查阅&#xff08;可搜索或按系列查找&#xff09; 持续更新链接如下&#xff1a; 阿齐单片机项目合集 (kdocs.cn)https://www.kdocs.cn/l/cmrxCxJN05YN 打开链接如下Exce表所…

【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)

本文项目编号 T 041 &#xff0c;文末自助获取源码 \color{red}{T041&#xff0c;文末自助获取源码} T041&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

代码随想录Day50|图论Part01,leetcode题目:98. 所有可达路径

提示&#xff1a;DDU&#xff0c;供自己复习使用。欢迎大家前来讨论~ 文章目录 图论理论基础Part01图的基本概念图的种类 连通性连通图强连通图连通分量强连通分量 图的构造邻接矩阵邻接表 图的遍历方式 深度优先搜索理论基础DFS 与 BFS 区别dfs 搜索过程代码框架深搜三部曲为…

Superset 使用指南之优化数据可视化性能与扩展

1. Superset 概述 什么是 Apache Superset&#xff1f; Apache Superset 是一个开源、现代化的数据可视化和数据探索平台。它通过提供直观的用户界面&#xff0c;使用户能够轻松创建复杂的图表和仪表板&#xff0c;探索海量数据&#xff0c;同时避免传统商业智能&#xff08;…

Apache James配置连接达梦数据库

项目场景&#xff1a; Apache James配置连接达梦数据库&#xff0c;其他配置中不存在的数据库也可参考此方案。 配置步骤 1、把需要的jar包导入到James 把DmJdbcDriver18.jar复制到下面lib目录下 james-2.3.2\lib 2、 修改连接配置 james-2.3.2\apps\james\SAR-INF\confi…

k8s部署jenkins集群时,使用ThinBackup进行定期备份

一、背景 使用k8s部署jenkins集群的时候&#xff0c;一般会把$JENKSIN_HOME&#xff08;默认是/var/jenkins_home&#xff09;进行持久化。 volumeMounts:- name: jenkins-homemountPath: /var/jenkins_home这样&#xff0c;机器重启&#xff0c;保证不会丢失文件。 本文要讲…

杀死端口占用的进程

1、查看端口的进程&#xff0c;以9023为例 &#xff08;1&#xff09;方法1 netstat -tunpl|grep 9023 &#xff08;2&#xff09;方法2 ss -tulpan |grep 9023 &#xff08;3&#xff09;方法3 netstat -ntlp |grep 9023 &#xff08;4&#xff09;方法4 lsof -i:9023 …

Linux:虚拟文件系统/proc和self进程

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 /proc目录 在Linux操作系统中&#xff0c;目录/proc是一个虚拟文件系统&#xff0c;称为procfc&#xff0c;用于访问内核和系统的实时状态信息。这个文件系统不同于常规…

下载 B 站封面的正确方式

B 友们经常用一些很好看的图片作为视频封面&#xff0c;但是大部分都不会指出图片来源&#xff0c;为此我们可以下载封面原图&#xff0c;用于保存或者搜索源出处。 这里介绍几个我知道的方法&#xff0c;欢迎补充&#x1f914; ‍ 使用相关客户端 上一篇博客介绍了很多的能…

基于微型5G网关的酒店服务机器人应用

智能机器人在酒店中已经越来越常见&#xff0c;并且也是提升客户体验、提高服务效率的重要工具。然而&#xff0c;尽管这些机器人在自动化服务方面可以发挥着重要作用&#xff0c;但它们仍然面临着一些通信、组网和在线管理方面的痛点。 针对这些难题&#xff0c;可以通过部署微…