二叉树进阶【c++实现】【二叉搜索树的实现】

二叉树进阶

前言:

  1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  2. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
  3. 之所以不在之前讲,是因为有些模拟实现和oj题用c语言实现比较麻烦

1.二叉搜索树

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

  • 若左子树不为空,则所有左子树的节点值一定比跟的值小
  • 若右子树不为空,则所有右子树的节点值一定比根的值大
  • 所有左右子树都是二叉搜索树

image-20240921092326446

**搜索二叉树的价值:**其实就是搜索和排序

image-20240921091739527

1.1二叉搜索树的实现

对于二叉搜索树,和二叉树都一样需要定义一个二叉树节点,和二叉树本身

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

private:
	Node* _root = nullptr;
};
1.1.1二叉搜索树的查找

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

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

	// 二叉搜索树的查找
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}

		// 走到这里说明,此时的cur走到了nullptr都没有找到,说明key不存在于该二叉搜索树中
		return false;
	}
1.1.2二叉搜索树的插入

思路:

  1. 判断根是否为空,空的话直接插入
  2. 根不为空,就根据二叉搜索树的性质去找空节点插入。

代码实现如下:

bool insert(const K& key)
{
	// 先判断该树是否为空
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	// 让cur找到key要插入的位置(这个位置一定是nullptr)
	Node* cur = _root;
	Node* parent = nullptr;//双指针解决父节点和插入节点之间连接问题
	while (cur)
	{
		// 判断key该往左还是右边走
		// 判断完往那边走,记得让parent记住当前cur的位置,再让cur往下走
  			if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{ 
			// 二叉搜索树不允许出现数据重复,因此遇到相同的数据不能插入
			return false;
		}
	}

	// 此时cur找到了一个空的可以插入的位置
	cur = new Node(key);
	// cur节点被创建出来之后,父节点要连接cur节点
	if (parent->_key > key)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}

	return true;
}

测试代码:

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

image-20240921201623420

1.1.3中序遍历(排序)

在二叉搜索树当中,如果用中序遍历,恰好就是升序排序的结果。这也是为什么二叉搜索树又被叫做二叉排序树的原因。

代码如下:

	// 中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

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

	//cpp中一般实现递归都要通过子函数
	// 因为外边调用这个中序遍历接口的时候没办法直接传一个_root进来,_root是私有的
	void InOrder()
	{
		_InOrder(_root);
		//_InOrder(this->_root); // 等价于上面的
		cout << endl;
	}

测试代码:

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

	tree.InOrder(); // 1 3 4 6 7 8 10 13 14
1.1.4二叉搜索树的删除(重点)

正常来说一共会有四种情况,但是我们可以归为3类,如下图所示:

image-20240921232740921

对于只有左孩子的情况,还需要分类讨论:

image-20240921233600443

只有右孩子的情况也要分类讨论:

image-20240921234428021

两个孩子都有的情况是不同的,需要用到替换法:

找左子树的最大节点或者找右子树的最小节点

image-20240922001758055

代码实现如下:

	// 二叉搜索树的删除
	bool Erase(const K& key)
	{
		// 传了个空树就不用删除了
		if (_root == nullptr)
			return false;

		// 仍然采用双指针来连接父节点和新的孩子节点
		Node* parent = nullptr;
		Node* cur = _root;

		// 先找到key的位置
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 此时就是找到了key的位置,开始判断key所在的节点是那种情况
				//1.只有左孩子 
				//2.只有右孩子
				//3.双孩子节点

				//1.只有左孩子
				if (cur->_right == nullptr)
				{
					// 这里有个特殊情况,当删除的是只有左孩子的根节点时,下面会对parent的nullptr值解引用,报错、
					// 因此进行特殊处理
					if (_root == cur)
					{
						_root = cur->_left;
						delete cur;
						cur = nullptr;

						return true;
					}

					// 只有左孩子的情况下,还有两种情况需要分类讨论
					//1.cur是父节点的左孩子
					if (parent->_left == cur)
					{
						// 此时让cur的左孩子变成父节点的左孩子
						parent->_left = cur->_left;
					}
					else//2.cur是父节点的右孩子
					{
						// cur的左孩子成为父节点的右孩子
						parent->_right = cur->_left;
					}
					
					delete cur;
					cur = nullptr;
				}
				else if (cur->_left == nullptr) // 2.只有右孩子
				{
					// 这里有个特殊情况,当删除的是只有右孩子的根节点时,下面会对parent的nullptr值解引用,报错、
					// 因此进行特殊处理
					if (_root == cur)
					{
						// 直接让右孩子成为跟节点
						_root = cur->_right;
						delete cur;
						cur = nullptr;

						return true;
					}

					// 只有右孩子,还是有两种情况需要分类讨论
					//1.cur是父节点的右孩子
					if (parent->_right == cur)
					{
						// 让cur的右孩子变成父节点的右孩子
						parent->_right = cur->_right;
					}
					else // 2.cur是父节点的左孩子
					{
						// 让cur的右孩子变成父节点的左孩子
						parent->_left = cur->_right;
					}

					delete cur;
					cur = nullptr; // 这里不重置会调用cur这个已经析构的空间
				}
				else // 3.两个孩子都存在
				{
					// 该情况要使用替换法
					// 此时可以找左子树的最大节点或者是右子树的的最小节点

					// 这里用右子树的最小节点替换cur
					Node* rightMinParent = nullptr;
					Node* rightMin = cur->_right;
					// 这里不断的找右子树的最小节点
					while (rightMin->_left) // 当找到左孩子为空时,该节点就是右子树最小的。
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}

					//找到了之后要进行替换
					cur->_key = rightMin->_key;

					// 这里要排除,当右子树直接找到最小节点的时候,此时由于循环上面的循环没进去,rightMinParent没有赋值
					// 如果直接进行下面的判断,会直接对nullptr进行解引用导致报错
					if (rightMinParent == nullptr)
					{
						// 这种情况直接删除右孩子,就行了
						cur->_right = rightMin->_right;
						delete rightMin;
						rightMin = nullptr;

						return true;
					}

					// 此时要删除的节点转换到了rightMin上,这里就转换成了只有右孩子的情况(也可能是叶子节点,没有右孩子)
					// 因此要进行分类讨论,这里和上面对只有右孩子情况的处理是一样的,就不多说
					if (rightMinParent->_left == rightMin)
					{
						rightMinParent->_left = rightMin->_right;
					}
					else
					{
						rightMinParent->_right = rightMin->_right;
					}

					delete rightMin;
					rightMin = nullptr;

					return true;
				}
			}
		}

		// 走到这里就说明,key不存在
		return false;
	}

对于二叉搜索树有较多极端情况需要处理,因此需要对各种极端情况进行测试。

测试用例:

void testBSTree()
{
	BSTree<int> tree;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		tree.insert(e);
	}
	// 测试二叉搜索树的删除(对4种情况的节点都进行测试)【实际上处理只分了三种情况】
	tree.Erase(1);
	tree.InOrder();

	tree.Erase(14);
	tree.InOrder();

	tree.Erase(10);
	tree.InOrder();

	tree.Erase(8);
	tree.InOrder();

	tree.Erase(19);
	tree.InOrder();

	// 删空也要没问题才行
	for (auto e : a)
	{
		tree.Erase(e);
		tree.InOrder();
	}

	// 最好还要对两个特殊情况做测试:
	// 1.只有左子树的树,删除跟节点的第一个左孩子
	// 2.只有右子树的树,删除跟节点的第一个右孩子
	for (auto e : a)
	{
		tree.insert(e);
	}
	tree.Erase(10);
	tree.Erase(14);
	tree.Erase(13);

	// 此时该树是一个只有左孩子的树,删除3,即根节点第一个左孩子
	tree.Erase(3);
	tree.InOrder();

	for (auto e : a)
	{
		tree.insert(e);
	}
	tree.Erase(3);
	tree.Erase(1);
	tree.Erase(6);
	tree.Erase(4);
	tree.Erase(7);

	// 此时该树是一个只有右孩子的树,删10,即根节点第一个右孩子
	tree.Erase(10);
	tree.InOrder();

} 

1.2二叉搜索树的应用

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

比如:有一个单词add,检查是否拼写正确。

就让其所有单词构建一个二叉搜索树,然后再这个树里找add存不存在,不存在就说明拼写错误。

前面实现的二叉搜索树就是K模型

1.2.2KV模型
  • KV模型:每一个关键码Key,都有与之对应的值Value,也就是<Key,Value>键值对

这个KV模型在现实生活中非常常见:

  1. 比如高铁站通过身份证来验证你是否购买了票。

验证时就是读取你的身份证号码,在二叉搜索树中寻找你的号码是否存在,存在了之后去找你是否购买了票。最后还要在加一个人脸识别的验证。

在这里身份证号码就是Key,票就是Value。<身份证号码,票>构成Value

  1. 英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对
  2. 统计次数

KV模型下的二叉搜索树的代码实现:

其实这个没和K模型的代码实现没什么区别。

template<class K, class 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;
public:

	// 二叉搜索树的查找
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}

		// 走到这里说明,此时的cur走到了nullptr都没有找到,说明key不存在于该二叉搜索树中
		return nullptr;
	}

	// 二叉搜索树的插入
	bool insert(const K& key, const V& value)
	{
		// 先判断该树是否为空
		if (_root == nullptr)
		{
			_root = new Node(key, value);
			return true;
		}

		// 让cur找到key要插入的位置(这个位置一定是nullptr)
		Node* cur = _root;
		Node* parent = nullptr;//双指针解决父节点和插入节点之间连接问题
		while (cur)
		{
			// 判断key该往左还是右边走
			// 判断完往那边走,记得让parent记住当前cur的位置,再让cur往下走
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 二叉搜索树不允许出现数据重复,因此遇到相同的数据不能插入
				return false;
			}
		}

		// 此时cur找到了一个空的可以插入的位置
		cur = new Node(key, value);
		// cur节点被创建出来之后,父节点要连接cur节点
		if (parent->_key > key)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		return true;
	}

	// 二叉搜索树的删除
	bool Erase(const K& key)
	{
		// 传了个空树就不用删除了
		if (_root == nullptr)
			return false;

		// 仍然采用双指针来连接父节点和新的孩子节点
		Node* parent = nullptr;
		Node* cur = _root;

		// 先找到key的位置
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 此时就是找到了key的位置,开始判断key所在的节点是那种情况
				//1.只有左孩子 
				//2.只有右孩子
				//3.双孩子节点

				//1.只有左孩子
				if (cur->_right == nullptr)
				{
					// 这里有个特殊情况,当删除的是只有左孩子的根节点时,下面会对parent的nullptr值解引用,报错、
					// 因此进行特殊处理
					if (_root == cur)
					{
						_root = cur->_left;
						delete cur;
						cur = nullptr;

						return true;
					}

					// 只有左孩子的情况下,还有两种情况需要分类讨论
					//1.cur是父节点的左孩子
					if (parent->_left == cur)
					{
						// 此时让cur的左孩子变成父节点的左孩子
						parent->_left = cur->_left;
					}
					else//2.cur是父节点的右孩子
					{
						// cur的左孩子成为父节点的右孩子
						parent->_right = cur->_left;
					}

					delete cur;
					cur = nullptr;
				}
				else if (cur->_left == nullptr) // 2.只有右孩子
				{
					// 这里有个特殊情况,当删除的是只有右孩子的根节点时,下面会对parent的nullptr值解引用,报错、
					// 因此进行特殊处理
					if (_root == cur)
					{
						// 直接让右孩子成为跟节点
						_root = cur->_right;
						delete cur;
						cur = nullptr;

						return true;
					}

					// 只有右孩子,还是有两种情况需要分类讨论
					//1.cur是父节点的右孩子
					if (parent->_right == cur)
					{
						// 让cur的右孩子变成父节点的右孩子
						parent->_right = cur->_right;
					}
					else // 2.cur是父节点的左孩子
					{
						// 让cur的右孩子变成父节点的左孩子
						parent->_left = cur->_right;
					}

					delete cur;
					cur = nullptr; // 这里不重置会调用cur这个已经析构的空间
				}
				else // 3.两个孩子都存在
				{
					// 该情况要使用替换法
					// 此时可以找左子树的最大节点或者是右子树的的最小节点

					// 这里用右子树的最小节点替换cur
					Node* rightMinParent = nullptr;
					Node* rightMin = cur->_right;
					// 这里不断的找右子树的最小节点
					while (rightMin->_left) // 当找到左孩子为空时,该节点就是右子树最小的。
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}

					//找到了之后要进行替换
					cur->_key = rightMin->_key;

					// 这里要排除,当右子树直接找到最小节点的时候,此时由于循环上面的循环没进去,rightMinParent没有赋值
					// 如果直接进行下面的判断,会直接对nullptr进行解引用导致报错
					if (rightMinParent == nullptr)
					{
						// 这种情况直接删除右孩子,就行了
						cur->_right = rightMin->_right;
						delete rightMin;
						rightMin = nullptr;

						return true;
					}

					// 此时要删除的节点转换到了rightMin上,这里就转换成了只有右孩子的情况(也可能是叶子节点,没有右孩子)
					// 因此要进行分类讨论,这里和上面对只有右孩子情况的处理是一样的,就不多说
					if (rightMinParent->_left == rightMin)
					{
						rightMinParent->_left = rightMin->_right;
					}
					else
					{
						rightMinParent->_right = rightMin->_right;
					}

					delete rightMin;
					rightMin = nullptr;

					return true;
				}
			}
		}

		// 走到这里就说明,key不存在
		return false;
	}

	// 中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

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

	//cpp中一般实现递归都要通过子函数
	// 因为外边调用这个中序遍历接口的时候没办法直接传一个_root进来,_root是私有的
	void InOrder()
	{
		if (_root == nullptr)
		{
			cout << "该树为空" << endl;
			return;
		}

		_InOrder(_root);
		//_InOrder(this->_root); // 等价于上面的
		cout << endl;
	}

private:
	Node* _root = nullptr;
};

KV模型的应用:

  • 中英互译的词典<word, chinese>
	// 这里的Key除了是int类型,还可以是其他类型,只要这个类型能够支持比较大小就OK
	BSTree<string, string> dict;
	dict.insert("sort", "排序");
	dict.insert("string", "字符串");
	dict.insert("tree", "树");
	dict.insert("people", "人");
	dict.InOrder();

	string str;
	cout << "输入你要查找的单词:";
	while (cin >> str)
	{
		BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret) // 只要返回的不是nullptr就说明找到了
		{
			cout << str<< ": " << ret->_value << endl;
		}
		else
		{
			cout << "找不到该单词\n";
		}
	}

image-20240922144844512

  • 统计次数
// 下面是一个二叉搜索树非常善于做的事情
// 现在有个需求:统计下面字符串出现的次数
string strArr[] = { "苹果", "苹果", "苹果", "苹果", "苹果", "橘子", "橘子", "橘子", "香蕉" };
BSTree<string, int> countTree;
for (auto str : strArr)
{
	BSTreeNode<string, int>* ret = countTree.Find(str);
	if (ret)
	{
		// 已经存在了就++
		ret->_value++;
	}
	else
	{
		// 不存在就插入
		countTree.insert(str, 1);
	}
}	

cout << "countTree:\n";
countTree.InOrder();

image-20240922144926080

1.3二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

image-20240922145155670

可以看到,二叉搜索树的结构可能会偏向极端,如右图所示

在最好的情况下——二叉搜索树接近完全二叉树,此时的平均比较次数就是以2为底的log(N)

在最坏的情况下——二叉搜索树接近单支数,此时的平均比较次数就是N/2。

在最坏的情况下,此时的效率就和链表和顺序表那些数据结构没有区别了、

因此,对于这种情况下,解决办法就是——平衡树

  1. AVLTree
  2. 红黑树

这两个数据结构属于高阶的数据结构

苹果", “橘子”, “橘子”, “橘子”, “香蕉” };
BSTree<string, int> countTree;
for (auto str : strArr)
{
BSTreeNode<string, int>* ret = countTree.Find(str);
if (ret)
{
// 已经存在了就++
ret->_value++;
}
else
{
// 不存在就插入
countTree.insert(str, 1);
}
}

cout << “countTree:\n”;
countTree.InOrder();


[外链图片转存中...(img-nBO6tRwJ-1727020957466)]

### 1.3二叉搜索树的性能分析

**插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能**

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即**结点越深,则比较次数越多。**

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

[外链图片转存中...(img-2lVnlFof-1727020957466)]

可以看到,二叉搜索树的结构可能会偏向极端,如右图所示

**在最好的情况下——二叉搜索树接近完全二叉树,此时的平均比较次数就是以2为底的log(N)**

**在最坏的情况下——二叉搜索树接近单支数,此时的平均比较次数就是N/2。**

在最坏的情况下,此时的效率就和链表和顺序表那些数据结构没有区别了、

因此,对于这种情况下,解决办法就是——**平衡树**

1. AVLTree
2. 红黑树

这两个数据结构属于高阶的数据结构





评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值