二叉搜索树

Posted 北川_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉搜索树相关的知识,希望对你有一定的参考价值。

目录

二叉搜索树概念

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

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

二叉搜索树节点的定义

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;
private:
	Node* _root = nullptr;

左右指针用来指向节点的左孩子和右孩子,_key用来存储节点的值,构造函数构造新节点。
搜索树的实现类给出根节点,完整代码中有实现类的具体代码。

二叉搜索树的插入

如果树为空则直接插入,如果树不为空,根据二叉搜索树性质查找插入位置,插入新节点。
例如要在上面的二叉搜索树中插入一个值为45的节点,根据二叉搜索树的性质查找插入位置:
与根节点的值进行比较,41 < 45,插入的位置应该在根节点的右边,
与右子树65进行比较, 65 > 45,插入的位置应该在65的左边,
与65的左树50进行比较,50 > 45,插入的位置应该在50的左边
50的左树为空,这就是45应该被插入的位置。
二叉搜索树插入代码实现:

bool Insert(const K& key)

	// 根为空
	if (_root == nullptr)
	
		_root = new Node(key);
		return true;
	
	// 根不为空
	// 用parent节点记录要插入节点的父节点
	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 < cur->_key)
		parent->_right = cur;
	else
		parent->_left = cur;
	

二叉搜索树的查找

根据二叉搜索树的性质进行查找,
如果根节点的值等于要查找的值,找到了,
如果根节点的值小于要查找的值,去它的右子树查找,
如果根节点的值大于要查找的值,去它的左子树查找。
例如在二叉搜索树中查找72

二叉搜索树查找代码实现:

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;

二叉搜索树的删除

删除操作较为复杂。

二叉搜索树中的节点无非就三种情况:
叶子节点(红色),
只有左孩子或只有右孩子的节点(绿色),
左右孩子都有的节点(蓝色)。
所以删除也分这三种情况:
1.叶子节点
2.只有一个孩子
3.两个孩子都有
先找到要删除的节点
不同的节点进行不同的删除操作

如果要删除的节点为叶子节点,让它的父亲指向空即可
例如删除叶子节点45

如果要删除的节点有一个左孩子或有一个右孩子(同时说明它的左右指针其中有一个指向空),找到要删除的节点,如果它在父亲的左边,让父亲的左指针指向它的孩子,如果它在父亲的右边,让父亲的右指针指向孩子,把它删除
例如节点50

叶子节点和只有一个孩子节点这两种情况可以算作一种,因为只有一个孩子节点,那它的左或者右指向空,而孩子节点的左右指针都指向空,可以把叶子节点和只有一个孩子的节点用一种逻辑处理。
最后左右孩子都有的节点(例如要删除蓝色65)

根据二叉搜索树的性质节点65的左边一定比65小,节点65的右边一定比65大。可以用左边的最大节点或者右边的最小节点替代它。
因为根据二叉搜索树的性质,左子树小右子树大,
左边的最大节点一定没有右孩子,不然它就不是最大的。
右边的最小节点一定没有左孩子,不然它就不是最小的。
用这两个节点代替要删除的节点,都满足二叉搜索树左子树比根小,右子树比根大的性质
所以这里删除65选择用它的右子树里最小的72代替它。
替代操作在写代码的时候就是把节点72的值赋值给节点65,然后删除节点72。

二叉搜索树删除代码实现:

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 (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* minRightParent = cur;		//记录右子树最小节点的父节点
				Node* minRight = cur->_right;	//记录右子树最小节点
				while (minRight->_left)		// 找到右子树最小节点
				
					minRightParent = minRight;
					minRight = minRight->_left;
				
				// 将右子树最小节点的值赋值给要删除节点的值
				cur->_key = minRight->_key;
				// 改变最小节点的父节点指针指向
				if (minRight == minRightParent->_left)
					minRightParent->_left = minRight->_right;
				else
					minRightParent->_right = minRight->_right;
				// 删除右边最小节点
				delete minRight;
			
			return true;
		
	
	// 没有要删除的节点
	return false;

二叉搜索树的应用

1.K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

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

2.KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。 该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系, 通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数, 统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:

  • <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key
  • 查询英文单词时,只需给出英文单词,就可快速找到与其对应的key

二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: log ⁡ 2 N \\log_2N log2N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N 2 \\fracN2 2N

完整代码

二叉搜索树实现
完整代码包括k模型,kv模型的实现,插入、删除、查找的递归实现。

以上是关于二叉搜索树的主要内容,如果未能解决你的问题,请参考以下文章

L3-010. 是否完全二叉搜索树

501. 二叉搜索树中的众数

501. 二叉搜索树中的众数

501. 二叉搜索树中的众数

天梯 - 是否完全二叉搜索树(判断序列插完是否是完全二叉树,并求出层序遍历)

Luogu-P2015 二叉苹果树