数据结构:搜索二叉树

前言

在前面我们已经学习了二叉树的基础操作,但是,仅仅是二叉树,没有太大的作用啊,存数据效果没有顺序表和链表好,那为啥还要学二叉树呢?
这不就来了嘛,给二叉树增加一些性质,作用不就出来了嘛。
本篇文章将介绍二叉树的进阶版本,给二叉树增加了搜索特性,搜索二叉树

搜索二叉树的概念和性质

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树

在这里插入图片描述

注意:要求整棵左子树的值都小于根节点的值,而不仅仅是要求左节点的值小于根节点的值,对于右子树也是如此

ps:当然也可以处理成左子树大,右子树小,不过普遍来说,都是构成左子树小,右子树大的。


搜索二叉树的操作

这里我们先给一个数组,用这个数组的数据构成搜索二叉树
在这里插入图片描述

搜索二叉树的遍历

根据搜索二叉树的性质,我们知道,左子树的值全部都小于根节点的值,右子树的值全部都大于根节点的值。

所以,当我们采用中序遍历就可以得到有=升序的序列。

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

	INORDER(root->_left);
	cout << root->_val << " ";
	INORDER(root->_right);
}

在这里插入图片描述

二叉搜索树的查找

假设要寻找的值是key

牢记搜索二叉树的性质,从根节点开始向下寻找,
当key > 当前节点的时候,去右子树寻找即可
当key < 当前节点的时候,去左子树寻找即可
找到了就返回true,没找到就返回false

bool find(const DataType& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key > cur->_val)
		{
			cur = cur->_right;//大了就去右子树找
		}
		else if (key < cur->_val)//小了就去左子树找
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}
	}

	return false;
}

在这里插入图片描述

注意:思考搜索二叉树查找的时间复杂度
是不是很多同学认为是O(logN)?
实际上并不是,应该查找高度次,但是高度就是logN了吗?
事实上,存在极端情况,二叉树会退化成单边二叉树,此时树的高度就是n - 1,
此时,时间复杂度是O(N)。


搜索二叉树的插入

记要插入值key

要插入首先要找到该在何处插入。

显然,这里的思路和查找类似
当key大了,就往右子树走
当key小了,就往左子树走
直到走到空的位置,可以插入。

找到位置后,用key构建一个新的节点,链接到到搜索树上去即可。

那么这里就需要寻找父节点了,所以,我们需要引入一个parent指针来标记父亲节点。

但是?究竟是插在父亲节点的左子树还是右子树呢?
去和父节点的值进行比较即可。
大于父节点的值,插在右边,小于父节点的值插在左边。

对于插入,如果搜索二叉树里面已经存在了该key值,那么就不重复插入,所以搜索二叉树就有了去重的特性

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

	Node* cur = _root;
	Node* parent = nullptr;

	while (cur)
	{
		if (key > cur->_val)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_val)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;//已经有了,就不重复插入
		}
	}

在这里插入图片描述

搜索二叉树的删除

搜索二叉树最难的部分就是删除,这里需要仔细体会

对于删除节点,我们会发现树里面有三种节点

  1. 当前节点没有子节点
  2. 当前节点只有一个子节点
  3. 当前节点有两个子节点

对于这三种情况我们都需要进行思考。

  1. 当前节点没有子节点
    此时我们只需要释放该节点,将父亲节点指向空即可
  2. 当前节点只有一个子节点
    a. 左子树为空
    将父亲节点指向右子树,释放当前节点即可
    b. 右子树为空
    将父亲节点指向左子树,释放当前节点即可
  3. 当前节点有两个子节点(最复杂的情况

    此时,我们就需要去找到一个合适的值,来替代当前节点。
    很明显,这个值一定要大于左子树,小于右子树
    那么,也就是,我们需要找到左子树的最大值,或者右子树的最小值来替代当前节点
    这里我们以寻找右子树的最小值为例。
    如何寻找右子树的最小值?
    右子树的最小值一定在右子树的最左边,否则就会有更小的值(这里读者理解不了的话,可以画个图理解一下)
    所以这里只需要设置一个指针rightMin,从cur->right开始一路向左,直到左子树为空停止。
    此时rightMin就是右子树的最小值。
    这是将rightMin的值给cur即可,然后删除rightMin节点
    这里删除非常简单,只需要让rightMin的父亲的左指向rightMin的右即可。(所以需要引入rightMinP指针记录rightMin的父亲)

算法实现过程中的一些细节:
(1)首先对于第三种情况,我们去寻找右子树的最小节点,就是寻找右子树的最左边,但是如果右子树没有左节点怎么办?
在这里插入图片描述
比如这里要删除8,右子树没有左节点,此时要进行特判,让cur的右指向rightMin的右。

(2)对于1,2两种情况,需要父亲节点指向子节点的左或者右
但是,对于根节点来说,根节点没有父亲,就会出现空指针问题。
在这里插入图片描述
对于10这个节点就没有父亲节点,
那么此时就需要将root 指向 cur的右
右子树为空类似。

bool erase(const DataType& key)
{
	if (_root == nullptr)
		return false;

	Node* cur = _root;
	Node* parent = nullptr;

	while (cur)
	{
		if (key > cur->_val)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_val)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			//找到了,开始删除
			if (cur->_left == nullptr)
			{
				//左边为空
				if (parent == nullptr)//特判一下
				{
					_root = cur->_right;
					delete cur;
					return true;
				}

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

				delete cur;
				return true;
			}
			else if (cur->_right == nullptr)
			{
				//右边为空
				if (parent == nullptr)
				{
					_root = cur->_left;
					delete cur;
					return true;
				}
				
				if (cur == parent->_left)
				{
					parent->_left = cur->_left;
				}
				else
				{
					parent->_right = cur->_left;
				}
				
				delete cur;
				return true;
			}
			else
			{
				//两个节点都为空
				Node* rightMin = cur->_right;
				Node* rightMinP = cur;

				//找右子树最小的节点
				while (rightMin->_left)
				{
					rightMinP = rightMin;
					rightMin = rightMin->_left;
				}

				cur->_val = rightMin->_val;

				if (rightMin == rightMinP->_right)
				{
					cur->_right = rightMin->_right;
				}
				else
				{
					rightMinP->_left = rightMin->_right;
				}

				delete rightMin;//这里不删cur
				return true;
			}
		}
	}

	return false;//找不到返回false;
 }

在这里插入图片描述

搜索二叉树的应用

搜索二叉树分为k型和kv型,上述就是以k型为例,当然,kv型类似,聪明的读者肯定能举一反三。

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

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

上面的例子就是k型,就不举例子了。

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

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

在这里插入图片描述


剩下的构造 ,析构,拷贝构造比较简单,这里就不讲解了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值