C++ - 搜索二叉树

news2024/11/25 16:46:07

二叉搜索树的概念

 二叉搜索树,又称二叉排序树。它具有以下性质:

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

 如果是顺序结构插入删除排序的话,其实效率上来说,是不高的。假设插入和删除都需要排序的话,除了在尾部最后一个元素的操作,其他操作都是需要挪动数据的,我们都知道挪动数据所带来的低效的结果。

但是反观二叉搜索树,插入和查找,最多只执行层数次。而删除相对麻烦一点,但是只是我们实现上麻烦,真正在用起来的时候,查找,插入,删除这个写操作并不低,而且这些个操作并不需要挪动数据,每一个数据都是独立存储一个空间的。

二叉搜索树,虽然在很大程度上提升了效率,但是二叉搜索树的下线很低,但是最坏情况下也只是和 顺序存储当中一样 退化到 O(n):

 为了解决上述的极端情况,才有了后面的 AVL树 , 红黑树  ,B树系列  等等。

 二叉搜索树当中的操作实现

 基本框架

template <class 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
{
public:
	typedef BSTreeNode<K> Node;

	BSTree()
		:_node(nullptr)
	{
	}

	void _inOrder(Node* cur)
	{
		if (cur == NULL)
		{
			return;
		}

		_inOrder(cur->_left);
		cout << cur->_key << " ";
		_inOrder(cur->_right);
	}

	void inOrder()
	{
		_inOrder(_node);
	}
	


private:
	Node* _node;
};

 注意,上述实现的 inOrder ()中学查找函数,实现了一层封装,使用 inOrder()的子函数 _inOrder()来实现递归,因为递归需要传入头结点指针作为函数参数。但是我们在主函数的那种调用inOrder()这个函数的时候,因为使用了类的封装,把树的头结点指针封装到类当中了,我们拿不到,所以采用上述方式进行书写。

调用 inOrder()的例子请看 插入操作当中的 示例。

 二叉搜索树的插入

  非递归

 学习过数据结构的小伙伴都知道,刚开始学习树的时候,我们说关于树的增删查改是没有意义的,而且相比于 链表 和 顺序表的 增删查改还有更大的消耗。

但是,在二叉搜索树当中,增删查改就有了意义,因为在树当中,每一个结点,在树当中的位置不是随便放的,而是有一定顺序的。

 举个例子,如下述二叉搜索树,我们想要插入一个 值为  “11” 的结点,那么这个过程应该是这样的:

 先从 根结点开始比较, 11 比 8 大,应该插入在 8 的右边,但是 8 的右边有结点了,那么就接着比较,11 比 10 大,应该插入在 10 的右边,但是 10 右边还是有结点,接着比较。11 比 14 小,应该插入在左边,左边有结点,接着比较,11 比 13 小,插入在 13 左边。所以,11 应该插入在如下图所示位置:

 如果是插入 二叉搜索树当中 有的结点,比如插入 13 ,那么就插入失败,不给插入

 通过上述的过程,你就知道在搜索树当中插入一个结点,如果插入;

相比之下,二叉搜索树当中插入就有了意义,那么同样的,删查改也是同样的道理,具体我们后面再说明。

具体代码实现:
 

	bool insert(const K& key)
	{
		// 首先判断 此时是不是一个空树
		if (_node == nullptr)
		{
			_node = new Node(key);  // 是就直接 new 一个空间直接给给头结点指针
			return true;
		}

		Node* cur = _node;   // 作为循环迭代指针
		Node* perant = _node; // 方便查找 cur 的父亲指针
		// 循环 找到合适地方插入 key 结点
		while (cur)
		{
			// 判断 要插入结点的值大小
			if (cur->_key < key)
			{
				perant = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				perant = cur;
				cur = cur->_left;
			}
			else  // 此时相等,不能插入
			{
				return false;
			}
		}

		// 这里做 开空间,插入结点的操作
		// 这里采用 再一次判断 要插入结点的值大小 的方式确认插入那一边
		if (perant->_key < key)
		{
			perant->_right = new Node(key);
			return true;
		}
		else
		{
			perant->_left = new Node(key);
			return true;
		}
	
	}

示例:

void textforBSTree1()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> T;
	for (auto e : a)
	{
		T.insert(e);
	}

	T.inOrder();
}

输出:

1 3 4 6 7 8 10 13 14

二叉搜索树,中序遍历出来的结果是有序的。

 递归

 插入和寻找一样,先找到可以插入的地方,然后把结点插入即可,我们在函数开头位置,判断当前结点是否为空,为空说明这个位置就是可以插入的位置:

 但是像上述是不行的,node 传入的是 形参,不能改变父亲结点的指针指向。或者是说传入的已经是父亲结点指针指向的位置了。

所以,这我们需要用 “&” 把 父亲结点当中的 _right 或者  _left  指针带到下一层递归函数栈帧当中,从而修改指针指向:

 此时的 node 不在是父亲的 _right 或者  _left  指针 所指向的结点,node 现在就是 父亲结点的 _right 或者  _left  指针。

而,这种对于引用的时候效果,在循环当中,也就是非递归当中是无法实现的;之前我们在循环当中是用  cur = cur->_left 类似这样的形式来往下迭代的,但是 引用是不能这样修改的,因为引用本身不能修改指向内容,比如一个引用指向 d 对象,那么他就不能修改指向为 c 对象。 

 在JAVA 当中,引用可以修改指向,jAVA 当中的 引用就可以类似 cur = cur->_left 这样写。

在递归当中可以实现 类似 node= node->_left 一样的效果:

 因为,每一次调用函数都会创建一个新的栈帧出来,也就是说虽然看上去每一次递归调用函数之后,引用发生了变化,向后迭代了,但是其实下一层的引用和上一层的引用不是一个引用,每一次层递归都会创建一个新的引用。而这个新的引用指向下一个结点,也就相当于是向后迭代了。

 完整代码:
 

	bool insertR(K key)
	{
		return _insertR(_node , key);
	}

private:
    bool _insertR(Node*& node, K key)
	{
		if (node == nullptr)
		{
			node = new Node(key);
			return true;
		}

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

  二叉搜索树的查找

非递归 

 查找也是和插入一样的,遍历查找,当给的结果只有一个值的时候,在二叉搜索树当中的搜索路径只有一个,当最后找到结尾空的时候,说明这颗二叉搜索树当中没有这个值的结点。

 这里我们简单实现,直接返回 bool 值:

	bool Find(K key)
	{
		if (_node == nullptr)
		{
			return false;
		}

		Node* cur = _node;
		if (cur->_key < key)
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			cur = cur->_right;
		}
		else
		{
			return true;
		}
	}

 递归

	bool FindR(K key)
	{
		return _FindR(_node, key);
	}

private:
	bool _FindR(Node* node, K key)
	{
		if (node == nullptr)
			return false;

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

删除

 非递归

 上述两种操作都简单,这里难在删除操作。

 当我们要删除某一个结点的时候,这个结点有三种情况:没有孩子,只有一个孩子,有两个孩子。

当这个结点没有孩子,或者只有一个孩子的时候,都好办;

1.如果没有孩子,说明这个结点是叶子结点,那么我们直接 delete 这个结点,然后把 这个结点的父亲 对应的指向关系置空就行。

2.如果只有一个孩子,那么,只需要把这个孩子,从他父亲开始逐一比较大小,找到合适位置插入。注意,此时要删除的这个结点的孩子的孩子可能不止一个,也就是说要删除的这个结点的后面可能是一个子树,那么不用管,直接让这个子树跟着 这个 孩子结点一起寻找合适位置插入即可。(加单来说就是,要删除结点如果是在父亲的右边,就让 其孩子(子树)在父亲的右边;反之

 3.如果有两个孩子,这个时候就要使用替换法。也就是说,从整棵树当中寻找一个结点,能替代当前要删除这个结点的位置。在这颗树当中一定是有一个结点可以替代的。而找这种结点也是有规律的,一个搜索二叉树当中,要删除的这个结点位置左子树最大的结点右子树最小的结点  这两个结点是一定可以替换的。所以我们就要找这两个结点。

对于左子树最大的结点,就在 要删除的这个结点位置的 左子树当中的最右边一个结点 就是最大结点(从要删除的结点位置的左子树根结点开始,往 _right 方向一致遍历,知道某一结点的 _right 指针为 nullptr ,那么这个结点就是最右边的结点):

 而 右子树最小的结点,就是右子树当中 最左边的结点(从要删除的结点位置的右子树根结点开始,往 _left 方向一致遍历,知道某一结点的 _left 指针为 nullptr ,那么这个结点就是最左边的结点):

 我们代码采用的方式是,找到替代值之后,把根结点的值和 替代结点 的值进行替换,然后在从根结点开始找到原来替代结点位置,删除该结点。

注意,当我们交换完 根结点和 替代结点之后,寻找 原本替代结点位置的时候,不能用递归 erase(删除结点操作函数,也就是现在我们正在写的函数)来递归寻找。有人就会想,既然 要删除的值已经在 erase()函数参数位置给出,那么直接调用 erase()函数就行了。

其实不是,比如上述图中的例子,要删除8,假设此时使用 7 来替代的,那么7就在根结点处,8就在原本 7 所在位置处,当递归调用 erase()函数寻找 8 的时候,因为根结点是 7 ,会想右子树当中去寻找,显然右子树当中是没有 8 的,此时就发生了错误。

 在上述问题的基础之上,还引发出一个问题,如下图所示:

 当我们删除了 3 这个结点之后,又出现之前说到的 一个孩子的情况,所以此时我们还是需要判断一下这种情况。所以此时,我们在给 之前父亲结点指针的初始值不能给 nullptr:

 4.还有一种情况,如下图所示,当要删除的结点 是 整颗二叉搜索数的 根结点,且这颗二叉搜索树只有一颗子树的时候,我们上述说的三种情况都不能完成这一操作:
 

 此时,就只能直接把 8 删除,让 10 作为这颗二叉搜索树 新的 根结点。

 erase()函数全部代码:

	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)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}// 右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
				} // 左右都不为空 
				else
				{
					// 找替代节点
					Node* parent = cur;  // 不能给 nullptr
					Node* leftMax = cur->_left;
					while (leftMax->_right)
					{
						parent = leftMax;
						leftMax = leftMax->_right;
					}

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

					if (parent->_left == leftMax)
					{
						parent->_left = leftMax->_left;
					}
					else
					{
						parent->_right = leftMax->_left;
					}

					cur = leftMax;
				}

				delete cur;
				return true;
			}
		}

		return false;
	}

递归

 递归实现在要删除结点左右孩子都存在的时候,在事项上要简单一些,在寻找替代结点,和删除结点进行值交换之后,可以直接调用递归(erase())来把key值的结点删除掉(此时key值结点已经被交换到叶子结点处)。

而,在非递归当中不能递归调用erase()函数因为,调用的话,会直接错过要删除结点所在子树;而递归当中就不会,为递归当中传入的指针参数是 以 引用的方式传入的,直接传入的就是上一个结点指向这个结点的别名(也就是现在结点的父亲结点直线该结点的指针别名)。

在非递归当中,把替换结点个要删除的结点进行交换之后,这整棵树就不再是 二叉搜索树了;但是递归当中,替换之后,递归是在左子树(或者是右子树)当中进行寻找,而此时左子树还是二叉搜索树,所以可以进行寻找。

两个孩子都有的代码部分:
 

				Node* leftMax = root->_left;
				while (leftMax->_right)
				{
					leftMax = leftMax->_right;
				}

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

				return _EraseR(root->_left, key);

完整代码:
 

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

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

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

			// 1、左为空
			// 2、右为空
			// 3、左右都不为空
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else
			{
				Node* leftMax = root->_left;
				while (leftMax->_right)
				{
					leftMax = leftMax->_right;
				}

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

				return _EraseR(root->_left, key);
			}

			delete del;
			return true;
		}
	}

还需要注意的点是,在最后递归调用erase()函数时候,虽然 root->left 和 LeftMax 两个指针在传参的时候之后,看似实现效果是一样,但是传参的时候不能用 LeftMax 作为参数:

 如下述情况就不行了:

 当你要删除 8 这个结点 ,那么 3  就是左子树当中的最大结点,如果传入的是 LeftMax ,下一层递归erase()函数当中的 root 就是 LeftMax,按照我们上述实现的删除逻辑,就乱套了。

 删除整个树

 删除就要编译整个树,要删除的话,采用后序来遍历是最好的,也就是从叶子结点开始往前面删,保证删除每一个结点,关系不会乱套。

 而且,我们在选择传入参数的时候,可以采用结点指针引用的方式,这种方式相当于是二级指针的效果,但是引用用起来要比二级指针要好用很多。

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

		Destroy(root->_left);
		Destroy(root->_right);

		delete root;  
		// 因为root 是引用,这里修改可以直接修改到 树当中的指针指向位置
		root = nullptr;
	}

Destroy 函数可以直接用于 二叉搜索树的析构函数当中:

	~BSTree()
	{
		Destroy(_node);
	}


拷贝构造函数(深拷贝)

 因为是另开空间,对于拷贝,还是要进行深拷贝,不然程序就会奔溃。

树的深拷贝的话,我们采用前序遍历的方式来创建新树和遍历老树。在旧树访问的同时,创建新树当中的结点。

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

private:
	Node* Copy(Node* root)
	{
		// 递归终止条件,也就是构建叶子结点的左右根指针
		if (root == nullptr)
			return nullptr;

		// 前序遍历,先创建根结点,在构建这个根结点左子树和右子树
		Node* newNode = new Node(root->_key);
		Node* _left = Copy(root->_left);
		Node* _right = Copy(root->_right);

		// 最后返回根结点指针
		return newNode;
	}

赋值重载运算符

 这里采用简单的现代写法,叫编译器帮我们利用拷贝构造函数来构造出临时对象,然后我们只需要交换根结点指针,把当前空对象不要的指针和指针维护的空间交给编译器管理的临时对象来自动调用析构函数帮我们销毁,而我们就使用临时对象构建的数:
 

	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root); // 交换
		return *this; // 返回当前对象
	}

 完整代码参考:

#pragma once

template <class 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
{
public:
	typedef BSTreeNode<K> Node;

	BSTree()
		:_node(nullptr)
	{
	}

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

	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root); // 交换
		return *this; // 返回当前对象
	}

	~BSTree()
	{
		Destroy(_node);
	}


	void inOrder()
	{
		_inOrder(_node);
	}

	bool Find(K key)
	{
		if (_node == nullptr)
		{
			return false;
		}

		Node* cur = _node;
		if (cur->_key < key)
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			cur = cur->_right;
		}
		else
		{
			return true;
		}
	}

	bool FindR(K key)
	{
		return _FindR(_node, key);
	}

	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)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}// 右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
				} // 左右都不为空 
				else
				{
					// 找替代节点
					Node* parent = cur;  // 不能给 nullptr
					Node* leftMax = cur->_left;
					while (leftMax->_right)
					{
						parent = leftMax;
						leftMax = leftMax->_right;
					}

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

					if (parent->_left == leftMax)
					{
						parent->_left = leftMax->_left;
					}
					else
					{
						parent->_right = leftMax->_left;
					}

					cur = leftMax;
				}

				delete cur;
				return true;
			}
		}

		return false;
	}
	
	bool insert(const K& key)
	{
		// 首先判断 此时是不是一个空树
		if (_node == nullptr)
		{
			_node = new Node(key);  // 是就直接 new 一个空间直接给给头结点指针
			return true;
		}

		Node* cur = _node;   // 作为循环迭代指针
		Node* perant = _node; // 方便查找 cur 的父亲指针
		// 循环 找到合适地方插入 key 结点
		while (cur)
		{
			// 判断 要插入结点的值大小
			if (cur->_key < key)
			{
				perant = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				perant = cur;
				cur = cur->_left;
			}
			else  // 此时相等,不能插入
			{
				return false;
			}
		}

		// 这里做 开空间,插入结点的操作
		// 这里采用 再一次判断 要插入结点的值大小 的方式确认插入那一边
		if (perant->_key < key)
		{
			perant->_right = new Node(key);
			return true;
		}
		else
		{
			perant->_left = new Node(key);
			return true;
		}
	
	}

	bool insertR(K key)
	{
		return _insertR(_node , key);
	}

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

private:
	void _inOrder(Node* cur)
	{
		if (cur == NULL)
		{
			return;
		}

		_inOrder(cur->_left);
		cout << cur->_key << " ";
		_inOrder(cur->_right);
	}

	Node* Copy(Node* root)
	{
		// 递归终止条件,也就是构建叶子结点的左右根指针
		if (root == nullptr)
			return nullptr;

		// 前序遍历,先创建根结点,在构建这个根结点左子树和右子树
		Node* newNode = new Node(root->_key);
		Node* _left = Copy(root->_left);
		Node* _right = Copy(root->_right);

		// 最后返回根结点指针
		return newNode;
	}

	bool _FindR(Node* node, K key)
	{
		if (node == nullptr)
			return false;

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

	bool _insertR(Node*& node, K key)
	{
		if (node == nullptr)
		{
			node = new Node(key);
			return true;
		}

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

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

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

			// 1、左为空 
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			// 2、右为空
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			// 3、左右都不为空
			else
			{
				Node* leftMax = root->_left;
				while (leftMax->_right)
				{
					leftMax = leftMax->_right;
				}

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

				return _EraseR(root->_left, key);
			}

			delete del;
			return true;
		}
	}

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

		Destroy(root->_left);
		Destroy(root->_right);

		delete root;  
		// 因为root 是引用,这里修改可以直接修改到 树当中的指针指向位置
		root = nullptr;
	}
private:
	Node* _node;
};

二叉搜索树的应用
 

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

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。


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

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;


再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对.

下述就是在上述二叉搜索树实现之后,对这个数据结构的运用: 



// 改造二叉搜索树为KV结构
template<class K, class V>
struct BSTNode
{
	BSTNode(const K& key = K(), const V& value = V())
		: _pLeft(nullptr), _pRight(nullptr), _key(key), _Value(value)
	{}
	BSTNode<T>* _pLeft;
	BSTNode<T>* _pRight;
	K _key;
	V _value
};
template<class K, class V>
class BSTree
{
	typedef BSTNode<K, V> Node;
	typedef Node* PNode;
public:
	BSTree() : _pRoot(nullptr) {}
	PNode Find(const K& key);
	bool Insert(const K& key, const V& value)
		bool Erase(const K& key)
private:
	PNode _pRoot;
};
void TestBSTree3()
{
	// 输入单词,查找单词对应的中文翻译
	BSTree<string, string> dict;
	dict.Insert("string", "字符串");
	dict.Insert("tree", "树");
	dict.Insert("left", "左边、剩余");
	dict.Insert("right", "右边");
	dict.Insert("sort", "排序");
	// 插入词库中所有单词
	string str;
	while (cin >> str)
	{
		BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret == nullptr)
		{
			cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
		}
		else
		{
			cout << str << "中文翻译:" << ret->_value << endl;
		}
	}
}
void TestBSTree4()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
	"苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的节点中水果对应的次数++
		//BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.InOrder();
}

 我们看到,二叉搜索树这个数据模型还是有很多作用的,在将来我们需要使用二叉搜索树模型的时候,不需要自己去手搓一个二叉搜索树,一是自己难写,而是我们写出来了二叉搜索树可能有问题,性能也不好;库当中有现成的,比如 set , map。set 就是 key 模型,map 是 key - value模型。

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

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

相关文章

核心实验16_端口镜像_ENSP

项目场景&#xff1a; 核心实验16_端口镜像_ENSP 实搭拓扑图&#xff1a; 具体操作&#xff1a; 交换机: [garliccc]observe-port 1 interface GigabitEthernet 0/0/3 /设置0/0/3为观察口 [garliccc]int g0/0/2 [garliccc-GigabitEthernet0/0/2]port-mirroring to observe-po…

Java后端简历指南(应届)

⭐简单说两句⭐ 作者&#xff1a;后端小知识 CSDN个人主页&#xff1a;后端小知识 &#x1f50e;GZH&#xff1a;后端小知识 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; Java后端简历指南&#xff08;应届&#xff09; 文章目录 Java后端简…

人离自动断电设备的功能要求

人离开自动断电石家庄光大远通电气有限公司用电器待机能耗往往是一种不易被发现的“隐藏的浪费”&#xff0c; 如果将一户家庭的空调、洗衣机、电视、微波炉、电饭煲五类电器进行计算&#xff0c;待机功率在12W到15W&#xff0c;待机能耗0.2度到0.33度电。每年能耗73度到124.45…

Nginx 中 location 和 proxy_pass 斜杠/ 问题

location 的斜杠问题比较好理解&#xff0c;不带斜杠的是模糊匹配。例如&#xff1a; location /doc 可以匹配 /doc/index.html&#xff0c;也可以匹配 /docs/index.html。 location /doc/ 强烈建议使用这种 只能匹配 /doc/index.html&#xff0c;不能匹配 /docs/index…

Python入门 | 如何判断多个条件

入门教程、案例源码、学习资料、读者群 请访问&#xff1a; python666.cn 大家好&#xff0c;欢迎来到 Crossin的编程教室 &#xff01; 之前我们已经了解了如何在 Python 中进行条件判断&#xff08;《是真是假&#xff1f;》&#xff09;&#xff0c;以及根据判断的结果执行不…

浅谈基于LoRa通信技术的建筑能耗监测系统及模块

安科瑞 华楠 摘要&#xff1a;本文提出采用LoRa通信技术开发设计建筑能耗监测系统的建议&#xff0c;通过系统&#xff0c;该系统功能完善、界面友好、通信稳定&#xff0c;在建筑能耗监测领域中有较高的推广价值。 关键词&#xff1a;LoRa通信&#xff1b;建筑能耗&#xff…

C#根据excel文件中的表头创建数据库表

C#根据excel文件中的表头创建数据库表 private void button1_Click(object sender, EventArgs e){string tableName tableNameTextBox.Text;string connectionString "";using (OpenFileDialog openFileDialog new OpenFileDialog()){openFileDialog.Filter &quo…

Stable Diffusion - 配置 WebUI 升级至 v1.6.0 版本与 VirtualENV 环境配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132177882 图像基于 哥特风格 绘制&#xff0c;参考 哥特 (Goth) 风格服装与背景的 LoRA 配置 Stable Diffusion WebUI 是一款基于 Stable Diffus…

uniapp自定义水印相机

uniapp自定义水印相机 背景实现UI实现功能实现全部实现代码 尾巴 背景 上一篇文章实现了uniapp中给页面添加水印&#xff0c;今天我们实现一个自定义水印相机&#xff08;最近跟水印杠上了&#xff0c;哈哈&#xff09;。主要使用了camera组件来实现取景框预览&#xff0c;最后…

华为云云耀云服务器L实例评测 | 使用UnixBench对华为云云耀云服务器L实例性能测试

文章目录 1 云耀云服务器L实例1.1 简介1.2 使用详情页&#xff1a;远程登录&#xff1a;本地连接&#xff1a;1. 重置密码2.设置安全组 MobaXterm本地连接 2 UnixBench性能测试2.1 UnixBench 的介绍和使用方法2.2 CentOS 7.6系统中编译和安装UnixBench的步骤&#xff1a;2.3 测…

java导出Mysql表信息生成Word文档

一、背景描述 系统上线或者交付&#xff0c;或者需要提供整理数据库表信息&#xff0c;如果一个个整理未免麻烦&#xff0c;接下来一个demo示例如何用JAVA导出Mysql数据库表信息生成Word文档。 传入null导出全部表 传指定表只导出指定表

pmp项目管理考试是什么?适合哪些人学?

PMP&#xff0c;简单点说&#xff0c;就是美国PMI为考察项目管理人士的专业能力而设立的考试。 该流程以知识和任务驱动型指南评估从业者的能力&#xff0c;同时确定项目经理能力行业标准&#xff0c;包括各项知识、任务和技能的特点、重要性与运用频率。&#xff08;考纲原文…

OpenHarmony创新赛|赋能直播第三期

开放原子开源大赛OpenHarmony创新赛赋能直播间持续邀请众多技术专家一起分享应用开发技术知识&#xff0c;本期推出OpenHarmony应用开发之音视频播放器和三方库的使用和方法&#xff0c;助力开发者掌握多媒体应用技术的开发能力和使用三方库提升应用开发的效率和质量&#xff0…

基于springboot的新闻门户网站

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

jenkins+newman+postman持续集成环境搭建

目录 一、Newman简介 二、Newman应用 三、安装newman 四、Html报告插件安装 五、安装nodejs&#xff1a; 六、 Jenkins集成步骤 一、Newman简介 Newman是一款基于Node.js开发的&#xff0c;可以运用postman工具直接从命令运行和测试postman集合 二、Newman应用 环境准备…

低代码开发趋势:利用Zoho Creator构建安全可靠的手机应用指南

在数字化时代&#xff0c;手机应用的重要性不言而喻。无论是企业还是个人用户&#xff0c;手机应用已经成为了连接世界、提升效率和改善生活的不可或缺的工具。然而&#xff0c;传统的手机应用开发通常需要大量的时间、资源和技术知识&#xff0c;对于许多人来说&#xff0c;这…

CSS绘制各种三角形

思路 从盒模型来看&#xff0c;如果content宽高为0, border有宽度&#xff0c;那么上下左右的border就会填充满整个盒子&#xff0c;于是四个方向的border都是三在这里插入图片描述 角形形状&#xff0c;这样想要哪边就显示哪边 效果 代码 html <!DOCTYPE html> <…

手写RPC框架--12.异常重试

异常重试 异常重试a.异常重试b.RPC的自我保护1) 介绍2) 实现令牌桶限流器 c.熔断器d.实现服务端的限流e.实现客户端的熔断f.流量隔离1) 介绍2) 实现 异常重试 a.异常重试 1.为什么需要异常重试&#xff1f; 当发起一次 yrpc 调用&#xff0c;去调用远程的一个服务&#xff0c…

【数据结构】包装类简单认识泛型

文章目录 1 包装类1.1 基本数据类型和对应的包装类1.2 装箱和拆箱 2 什么是泛型3 引出泛型3.1 语法 4 泛型类的使用4.1 语法4.2 示例4.3 类型推导(Type Inference) 5 泛型的上界5.1 语法5.2 示例5.3 复杂示例 6 泛型方法6.1 定义语法6.2 示例6.3 使用示例-可以类型推导6.4 使用…

Spring之文件上传下载,jrebel,多文件上传

文件上传,文件下载jrebel&多文件上传 1.文件上传,文件下载 文件上传 1.spring-xml配置多功能视图解析器 2.前端标记表单为多功能表单enctype”mutipart/form-data“ 3.后端可以直接利用mutipartFile类&#xff0c;接受前端传递到后台的文件 4.将文件转成流&#xff0c;然后…