搜索二叉树详解

发布于:2023-01-09 ⋅ 阅读:(459) ⋅ 点赞:(0)

目录

前言

一、二叉搜索树概念

二、二叉搜索树的操作

1、查找

2、插入

2.1插入非递归实现

2.2插入递归实现

3、删除 

3.1删除非递归实现

3.2、删除递归实现 

三、二叉搜索树的应用 

1. K模型

2. KV模型

四、二叉搜索树的性能分析 


前言

哈喽,小伙伴们大家好。今天我来给大家介绍一种新的二叉树结构——搜索二叉树。搜索二叉树是后面学习AVL树和红黑树的基础,学好它对于后面学习一些复杂的数据结构很有帮助。那么事不宜迟,我们赶快开始吧。


一、二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者遵循以下特征:

  • 如果它的左子树不为空,则左子树上节点的所有值都要小于根节点。
  • 如果它的右子树不为空,则右子树上节点的所有制都要大于根节点。
  • 左右子树也同样是二叉搜索树。
  • 二叉搜索树中的节点不能有相同的值。

二叉树节点代码如下:

template<class T>
struct BSTreeNode
{
	BSTreeNode(const T& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{

	}
	BSTreeNode<T>* _left;
	BSTreeNode<T>* _right;
	T _key;
};

 二、二叉搜索树的操作

1、查找

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

代码如下:

Node* find(const T& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->right;
			}
			else
			{
				return cur;
			}
		}
	}

2、插入

2.1插入非递归实现

  • 树为空,则直接插入节点,赋值给root指针。
  • 树不为空,则根据要插入节点数值的大小,依靠搜索二叉树的特性找到合适的位置插入。

 代码如下:

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

		else
		{
			Node*temp=new Node(key);
			Node* cur = _root;
			Node* parent = cur;
			//去找合适的插入位置,直到遇到空截至
			while (cur)
			{
				parent = cur;//把上一个节点保存起来,方便找到空后连接
				if (key < cur->_key)
				{
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					cur = cur->_right;
				}
				else
				{
					return false;//不允许相同大小的数出现
				}
			}
			if (key < parent->_key)
			{
				parent->_left = temp;
			}
			else
			{
				parent->_right = temp;
			}
			return true;
		}
	}

2.2插入递归实现

递归实现可以有效地简化代码,这里用到一个很巧妙的思想,root用引用传参,直接传递了父节点的左右指针,和非递归实现相比更加简洁,不需要再新加一个prev记录。

代码如下:

	//root用引用传参,相当于root是上一个节点的左右指针,直接指向要插入的节点即可
	bool _insertR(Node*& root, const T& key)
	{
		if (root == NULL)
		{
			root = new Node(key);
			return true;
		}

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

//插入递归实现,由于用户不能拿到_root,所以这里需要嵌套实现
	bool insertR(const T& key)
	{
		return _insertR(_root,key);
	}

3、删除 

3.1删除非递归实现

搜索二叉树的删除有些复杂,可以分为以下三种情况:

a. 要删除的节点的左右子节点都为空。

b. 要删除节点的左右子节点有一个为空。

c. 要删除节点的左右子节点都不为空。

其中a和b可以合并为一种情况,这种情况直接删除该节点,然后让父节点直接继承子节点即可。

c情况比较麻烦,不能直接删除该节点,否则会造成整个树的结构错乱。 这时候我们可以采用替换法,根据大小关系找到一个可以顶替要删除节点位置的节点,这个节点一般是左树的最右节点或有树的最左节点,把它和要删除的节点位置进行交换,要删除的节点就成了叶子节点或者只有一个节点,此时再删除它就回归到了第一种情况,再递归一次即可。

代码如下:

bool erase(const T& key)
	{
		//先找到传进来的值,由于需要保存父节点,所以不能使用find函数
		Node* cur = _root;
		Node* parent = cur;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
                //找到了,开始删除。
				//情况1,左为空或右为空或都为空,直接删除,把子节点交给父节点收养
				if (cur->_left == nullptr)
				{
					//先判断要删的是不是根节点,如果是根节点需要把根节点位置转移
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;

						}
						else
						{
							parent->_right = cur->_right;

						}
					}
					delete cur;
				}
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				//情况2,左右都不为空。
				//把找到的节点当成根节点,从左边找最右或者从右边找最左,替换根节点
				else
				{
					//从右边找最左
					Node* midleft = cur->_right;
					while (midleft->_left)
					{
						midleft = midleft->_left;
					}
					//保存最左节点的值
					T min = midleft->_key;
					//递归删除最左节点
					erase(min);
					cur->_key = min;
				}
				return true;
			}
		}
        return false;
	}

3.2、删除递归实现 

和非递归相比,第一种情况稍微简化了一点,不需要再用prev指针记录父节点。第二种情况进行交换后再递归调用删除函数即可。

    bool _eraseR(Node*& root, const T& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (key < root->_key)
		{
			return _eraseR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _eraseR(root->_right, key);
		}
		else
		{
		    //找到了,开始删除
			//情况1,左右至少有一个为空
			if (root->_left == nullptr)
			{
				Node* temp = root;
				root = root->_right;
				delete temp;
			}
			else if (root->_right == nullptr)
			{
				Node* temp = root;
				root = root->_left;
				delete temp;
			}
			//情况2,左右都不为空
			else
			{
				//从右边找最左值
				Node* minright = root->_right;
				while (minright->_left)
				{
					minright = minright->_left;
				}
				T min = minright->_key;
				//把最左值删了之后再用它代替根
				_eraseR(root->_right, min);//这里必须传root->_right,不能传minright,否则构不成引用
				root->_key = min;
			}
			return true;
		}
	//删除递归实现
	bool eraseR(const T& key)
	{
		return _eraseR(_root, key);
	}

三、二叉搜索树的应用 

1. K模型

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

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

2. KV模型

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

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

四、二叉搜索树的性能分析 

二叉搜索树查找,插入,删除一般都要进行高度次,在一般情况下时间复杂度是O(logN)。但是时间复杂度要考虑最坏情况,当二叉树内所有节点的节点都只有一个时,就会近似成一条线,此时高度等于元素个数,时间复杂度为O(N)。

 当二叉搜索树退化为单支树或类似单支树时,其性能就失去了,所以我们需要学习更高级的树。


总结

本章主要讲解了二叉搜索树的原理和实现,搜索二叉树虽然在一定程度上加快了搜素效率,但其仍存在缺陷(当为单支时),而之后的AVL树和红黑树就很好的解决了这一问题,在后续文章中我将进行介绍。今天的内容就到这里啦,感谢阅读。山高路远,来日方长,我们下次见~