【DS】搜索二叉树


在数据结构初阶时,我们在讲解 的时候对二叉树进行了介绍—— 堆的实现及二叉树的介绍,但是一般的二叉树价值是不大的,而我们今天学习的搜索二叉树在其特性下,在查询方面有着不错的表现。

搜索二叉树的概念

二叉搜索树(BST: Binary Search Tree)又称二叉排序树搜索二叉树。它或者是一棵空树,或者是具有以下性质的二叉树:

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

搜索二叉树

  • 注意:搜索二叉树中是不支持数据冗余的,也就是说搜索二叉树中只能由唯一的值,不能有重复的值;否则也不符合搜索二叉树的特性。所以搜索二叉树具有对数据去重的功能
  • 二叉搜索树(BST)又称二叉排序树的原因是:在一棵搜索二叉树中,中序去遍历该树会得到一个升序序列

对于数据的处理,有增删查改的操作,在增删改这些操作上,我们学习的vector,list都有着不错的表现,但是在查找方面,以上结构都只能进行暴力遍历,效率不好;而基于搜索二叉树的性质特点及经过改良的平衡树在查找方面有着不错的效率,在平衡的状态下,查找一个值的次数为二叉树的高度次——log(N)。这也是学习搜索二叉树的原因。

搜索二叉树的实现

搜索二叉树的结构

如图所示:搜索二叉树的结构为链式二叉树;
BST
数据和相关指针都保存在节点中,所以需要一个节点类

节点类的成员有:

  • 数据:_key
  • 指向左孩子的左结点指针:_left
  • 指向右孩子的右结点指针:_right

对于节点类,只需要一个构造函数初始化成员即可。

template<class K>
struct BSTNode
{
	K _key;
	BSTNode<K>* _left;
	BSTNode<K>* _right;

	BSTNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{}
};

接下来就是我们的搜索二叉树的相关函数:对于BSTree,成员只有用来管理BSTree的节点指针_root

将节点typedef一下

typedef BSTNode<K> Node;
template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
public:
    //BST相关函数

private:
	Node* _root=nullptr;
};

构造函数

对于自定义类型,使用编译器默认生成的构造一颗空树即可

  • 由于我们接下来还要实现拷贝构造函数,拷贝构造也是构造,只要我们写了,编译器就不会自动生成,所以加个default强制生成。
BSTree() = default;//强制生成

你想自己写也是ok的。

	BSTree()
		:_root(nullptr)
	{}

拷贝构造

BSTree的拷贝构造也是深拷贝:
实现拷贝构造,我们借助一个Copy函数递归拷贝原树,使用前序的思想,逐个拷贝节点,返回时才链接。

publicBSTree(const BSTree<K>& bt)
	{
		_root = Copy(bt._root);
	}
	
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}
		//前序遍历
		Node* newnode = new Node(root->_key);//拷贝节点
		newnode->_left = Copy(root->_left);//遍历左树
		newnode->_right = Copy(root->_right);//遍历右树
		return newnode;//返回时才链接
	}

赋值重载

使用之前STL容器学习时的现代写法:形参会调用拷贝构造构造一个BSTree对象,调用swap交换两个对象的_root即可。

	BSTree<K>& operator=(BSTree<K>& bt)
	{
		swap(_root, bt._root);
		return *this;
	}

析构函数

与拷贝构造类似,借助一个Destroy函数,递归delete节点:Destroy以后序的思想delete释放节点。

public:
	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}

private:
	void Destroy(Node* root)
	{
		if (root == nullptr)//为空则返回上一层。
		{
			return;
		}
		Destroy(root->_left);//左子树
		Destroy(root->_right);//右子树
		delete root;//根
		//到这结束该层栈帧,自动返回上一层。
	}

查找

查找动图

查找比较简单,但是很重要,后续插入删除都需要查找;要根据BST的特性(大于根往右,小于根往左)遍历查找即可。

    bool Find(const K& key)
	{
		if (_root == nullptr)
		{
			return false;
		}
		//遍历
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_key)//大于往右
			{
				cur = cur->_right;

			}
			else if (key < cur->_key)//小于往左
			{
				cur = cur->_left;
			}
			else//说明相等,找到了
			{
				return true;
			}
		}
		return false;//没找到
	}

插入

插入动图

插入作为搜索二叉树的主要功能,其操作如下:

  • 树为空:则直接新增节点,该节点即为root。
  • 树不为空:按照BST的性质插入:
    • 大于根节点,插入到根节点的右子树,直到找到空的位置,插入成功。
    • 小于根节点,插入到根节点的左子树,直到找到空的位置,插入成功。
    • BST不支持重复数据,若树中已有该值,插入失败。
    bool Insert(const K& key)
	{
		if (_root == nullptr)//当前树为空树
		{
			_root = new Node(key);//该节点直接作为根节点
			return true;
		}
		Node* cur = _root;//用cur遍历
		Node* parent = nullptr;//需要知道连接在哪个节点
		while (cur)
		{
			if (key > cur->_key)//大于当前节点的key
			{
				parent = cur;
				cur = cur->_right;//往右子树走

			}
			else if (key < cur->_key)//小于当前节点的key
			{
				parent = cur;
				cur = cur->_left;//往左走
			}
			else//此处说明找到相等的值,BST不允许冗余,有去重作用
			{
				return false;//插入失败,返回false
			}
		}
		//到在这里说明找到位置了,开始插入。此时cur已为nullptr

		Node* node = new Node(key);//new新的节点
		if (key > parent->_key)//通过与parent比对key的大小,看看新节点应该挂在parent的左还是右节点
		{
			parent->_right = node;
		}
		else//不存在相等情况
		{
			parent->_left = node;
		}
		return true;//插入成功,返回true
	}

删除

删除1

删除操作的重点在于:删除指定key后仍维持BST的特性。
删除分三种情况:

1.cur是叶子节点(没有孩子),直接delete
2.cur只有一个孩子(左或右),让curparent带孩子。(1,2可归为一类处理)
3.cur有左右孩子,找人带(leftmax或者rightmin:遵循BST的特性)

对于cur有左右孩子的情况:解决方案是找人带。这样替换是不会破坏树的性质的。原因如下图的两种解释
替换原理
删除2

思路还是先用cur遍历找到要删除的key,再看看是以上三种情况的哪一种(操作时1,2一起处理)。

    bool Erase(const K& key)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		Node* parent = nullptr;//不删除值为key的节点
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;

			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else//找到了,cur就是要删除的节点
			{
				//删除分三种情况
				//1.cur是叶子节点(没有孩子),直接delete。
				//2.cur只有一个孩子(左或右),让cur的parent带孩子。(1,2可归为一类处理)
				//3.cur有左右孩子,找人带(leftmax或者rightmin:遵循BST的特性)

				//1,2.删除没有或只有一个孩子的节点
				if (cur->_left==nullptr)//cur只有右孩子
				{ 
					//处理涉及根的情况
					if (parent == nullptr)//说明此时只有两个节点,且删除的是根
					{
						_root = cur->_right;
					}
					else
					{
						//
						if (parent->_left == cur)//确定cur是父节点的左孩子还是有右孩子
						{
							parent->_left = cur->_right;//cur为叶子时,parent左孩子将为nullptr
						}                               //cur有一个节点时,parent左孩子将为cur->_right
						else
						{
							parent->_right = cur->_right;
						}
					}
			
					delete cur;//删除节点
					return true;//删除成功
				}
				//1,2.删除没有或只有一个孩子的节点
				else if (cur->_right==nullptr)//只有左孩子
				{
					if (parent == nullptr)
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_left == cur)//确定cur是父节点的左孩子还是有右孩子
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
					return true;
				}
				//3.删除有左右孩子的节点
				else
				{
				    //处理删除节点后还有很多节点的情况
					//方案->找人带:删除节点的左树的最大节点,或右树的最小节点(左树最右孩子,右树最左孩子)
					Node* rightmin = cur->_right;//rightmin为右树的最小值,也为替换节点
					Node* rightmin_p = cur;//记录替换节点的父节点
					while (rightmin->_left)//cur右树的最左节点为右树的最小值
					{
						rightmin_p = rightmin;
						rightmin = rightmin->_left;

					}
					//此时找到了rightmin了,将该值与要删除节点cur的值交换
					cur->_key = rightmin->_key;
					//管理rightmin的孩子,只有一个孩子或没有孩子那一套(且该孩子只能是rightmin的右孩子)
					if (rightmin_p->_left==rightmin)//要看看是父节点的左还是右
					{
						rightmin_p->_left = rightmin->_right;
					}
					else
					{
						rightmin_p->_right = rightmin->_right;
					}

					delete rightmin;//删除的是替换的节点,而不是原本值为key的节点
					return true;
				}
			}
		}
		//没找到
		return false;//删除失败
	}
  • 注意rightminrightmin_p的左孩子还是右孩子
    rightmin_p

中序打印

中序遍历搜索二叉树会得到一个升序序列,以此来检测实现的BST是否正确。

  • 中序遍历需要递归遍历,所以需要传入节点,但是类外无法访问私有成员_root。解决办法:对外开放的接口为无参的InOrder_InOrder(Node* root)作为InOrder的子函数,这样就能解决这个问题了,实际上是_InOrder在遍历BST
public:
	void InOrder()
	{
		//嵌套一层,类外不能访问私有成员
		_InOrder(_root);
		cout << endl;
	}
private:
	//嵌套一层
	void _InOrder(Node* root)//中序遍历搜索二叉树是有序的
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);//左子树
		cout << root->_key << " ";//根
		_InOrder(root->_right);//右子树
	}

搜索二叉树的应用

凭借着极快的查找速度,二叉搜索树有着一定的实战价值,最典型的有:key 查找模型 和 key_value 关联式模型

key模型

key 模型的应用场景主要为:在不在

  • 门禁系统
  • 检查文章中单词拼写是否正确
  • 高速收费系统(车牌的识别)

如下面的简单模拟门禁系统。
Key模型

  • 循环没有设置停止条件;可采用快捷键Ctrl+Z,再按下Enter退出。

key_value模型

key_value模型底层与上述的key模型有所不同,其类型为<key, value>的键值对pair
key_value 的模型:关联信息的应用

  • 如中英文互译字典。
  • 高铁站身份证入站。
  • 停车场收费系统

如下面的简单中英文互译字典
key_value

搜索二叉树的缺陷

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

对于二叉搜索树:我们每进行一次查找,若未查找到目标结点,则还需查找的树的层数就减少了一层,所以我们最坏情况下需要查找的次数就是二叉搜索树的深度,深度越深的二叉搜索树,比较的次数就越多。在平衡的状态下最坏搜索次数为高度次log(N)

但是,搜索二叉树的平衡是没有保证的

  • 最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其时间复杂度为:log(N)
  • 最差情况下,即原本就是有序序列,二叉搜索树将退化为单支树(或者类似单支),其时间复杂度为:O(N)

BST退化
如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?这就需要我们后面所要学习的平衡树AVL树,红黑树)来解决这个失衡问题了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值