【C++】二叉搜索树

二叉搜索树

在这里插入图片描述

一、二叉搜索树的概念

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

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

总结:任意一颗子树都满足左子树的值 < 根 < 右子树的值。

示例:

image-20230320213755156

二叉搜索树又称二叉排序树,且任何一颗子树都满足左子树的值 < 根 < 右子树的值,由此我们进行中序遍历(左子树 根 右子树)得到的就是一个升序序列。


二、二叉搜索树的实现

要实现一颗二叉搜索树,要实现两个类,一个是结点类,用于存放节点值、左指针、右指针。第二个类专门用于二叉搜索树的增删查改。

1.结点类

结点类主要包含如下内容:

  1. 成员变量:节点值、左指针、右指针。
  2. 只需要一个构造函数对成员变量的初始化即可。
template<class K>
struct BSTreeNode
{
     
     
	BSTreeNode<K>* _left; //左指针
	BSTreeNode<K>* _right;//右指针
	K _key;//节点值
	BSTreeNode(const K& key)//构造函数
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{
     
     }
};

2.二叉搜索树的基本框架

此类主要用于增删查改。

扫描二维码关注公众号,回复: 14611228 查看本文章
  • 基本框架:
//二叉搜索树
template<class K>
class BSTree
{
     
     
	typedef BSTreeNode<K> Node;
public:
    //复制节点
    Node* Copy(Node* root);

    //删除节点
    void Destory(Node* root);

	//构造函数
	BSTree();

	//拷贝构造函数
	BSTree(const BSTree<K>& t);

	//赋值运算符重载函数
	BSTree<K>& operator=(BSTree<K> t);

	//析构函数
	~BSTree();

	//插入函数
	bool Insert(const K& key);

	//删除函数
	bool Erase(const K& key);

	//查找函数
	Node* Find(const K& key);
    
    //中序遍历
    void InOrder();
private:
    //插入函数子函数递归版
    bool _InsertR(Node*& root, const K& key);
    //删除函数子函数递归版
    bool _EraseR(Node*& root, const K& key);
    //查找函数子函数递归版
    bool _FindR(Node* root, const K& key);
	//中序遍历子函数递归版
	void _InOrder(Node* root);
private:
	Node* _root; //指向二叉搜索树的根结点
};

3.二叉搜索树的基本实现

3.1.默认成员函数

3.1.1构造函数

这里的构造函数直接让编译器默认生成就可以,不需要自己实现,但是后面的拷贝构造函数写了之后编译器就不会默认生成了,但是我们可以强制让它默认生成构造函数,不过要利用C++11的特性。或者也可以用传统方法构造根节点。

强制编译器自己生成构造函数,忽视拷贝构造带来的影响
//BSTree() = default;//C++11才支持
//构造函数
BSTree()
	:_root(nullptr)
{
     
     }

3.1.2.拷贝构造函数

注意这里的拷贝构造完成的是深拷贝,这里我们直接用前序递归的方式创建一颗与原来一样的二叉树即可。而递归前序拷贝结点的方式这里我们专门封装一个Copy函数,方便我们复用。

Node* Copy(Node* root)
{
     
     
	if (root == nullptr)
		return nullptr;
	Node* CopyNode = new Node(root->_key);//拷贝根结点
	//递归创建拷贝一棵树
	CopyNode->_left = Copy(root->_left);//递归拷贝左子树
	CopyNode->_right = Copy(root->_right);//递归拷贝右子树
	return CopyNode;
}
//拷贝构造函数--深拷贝
BSTree(const BSTree<K>& t)
{
     
     
	_root = Copy(t._root);
}

3.1.3.赋值运算符重载函数

传统写法:完成所给二叉搜索树的拷贝即可。

//传统写法
BSTree<K>& operator=(const BSTree<K>& t)
{
     
     
	if (this != &t) //防止自己给自己赋值
	{
     
     
		_root = Copy(t._root); //拷贝t对象的二叉搜索树
	}
	return *this; //支持连续赋值
}

现代写法:写法很巧妙,假设把t2赋值给t1,t2传参的时候直接利用传值传参调用拷贝构造生成t,t就是t2的拷贝,此时再调用swap函数交换t1和t 的_root根结点即可,而拷贝构造出来的t会在赋值运算符重载结束后自动调用自己的析构函数完成释放。

//赋值运算符重载函数 t1 = t2
BSTree<K>& operator=(BSTree<K> t)//t就是t2的拷贝
{
     
     
	//现代写法
	swap(_root, t._root);
	return *this;
}

3.1.4.析构函数

析构函数是为了释放二叉搜索树的所有结点,这里我们优先采用后序的递归释放,可以采用封装一个Destory函数来专门用于递归删除结点,如下:

void Destory(Node* root)
{
     
     
	if (root == nullptr)
		return;
	//通过递归删除所有结点
	Destory(root->_left);//递归释放左子树中的结点
	Destory(root->_right);//递归释放右子树中的结点
	delete root;
}
//析构函数
~BSTree()	
{
     
     
	Destory(_root);//复用此函数进行递归删除结点
	_root = nullptr;
}

3.2.中序遍历

中序遍历的核心宗旨是左子树 -> 根结点 -> 右子树,这里我们采用递归的方式去实现中序遍历

  • 代码如下:
//中序遍历 -- 递归	
void InOrder()
{
     
     
	_InOrder(_root);
	cout << endl;
}
//中序遍历的子树
void _InOrder(Node* root)
{
     
     
	if (root == nullptr)
		return;
	_InOrder(root->_left);//递归到左子树
	cout << root->_key << " ";//访问根结点
	_InOrder(root->_right);//递归到右子树
}

3.3.Insert插入函数

3.3.1.非递归实现

结合二叉搜索树的性质,插入的实现非常简单(注意重复的值不允许再次插入,默认不允许冗余)。主要分为两类:

1、如果是空树,直接把插入的结点作为根结点即可

2、如果不是空树,则按如下规则讨论:首先得找到待插入的值的合适位置,其次找到位置后,将插入的值与此树链接起来

  • 1、寻找待插入的值的合适位置:

定义cur指针从根结点开始(cur指针用于找到待插入的合适位置),定义parent指针最开始为nullptr(parent指针用于找到位置后的链接操作),把待插入的结点值定位key。遍历cur指针

  1. 若key > cur指向的结点值,让parent走到cur的位置,让cur指针走到右子树,指向_right的位置,继续遍历。
  2. 若key < cur指向的结点值,让parent走到cur的位置,让cur指针走到左子树,指向_left的位置,继续遍历。
  3. 若key = cur指向的结点值,说明待插入的结点值与此树当前结点值重合,插入结点失败。返回false。

image-20230328172258879

遍历结束后,说明已经找到要插入的合适的位置(某一颗子树的尾部),接着指向第二步:

  • 2、将插入的值与父亲链接起来:

链接的步骤很简单,确保链接位置即可:

  1. 若插入的值比父亲的值大,链接在父亲的右边
  2. 若插入的值比父亲的值小,链接在父亲的左边

image-20230328172652429

  • 代码如下:
//Insert非递归
bool Insert(const K& key)
{
     
     
	if (_root == nullptr)//若一开始树为空
	{
     
     
		_root = new Node(key);//直接申请值为key的结点作为二叉搜索树的根结点
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	//1、找寻插入的合适位置
	while (cur)
	{
     
     
		if (cur->_key < key)//若key大于当前结点值
		{
     
     
			parent = cur;
			cur = cur->_right;//让cur走向左子树
		}
		else if (cur->_key > key)//若key小于当前结点值
		{
     
     
			parent = cur;
			cur = cur->_left;//让cur走向右子树
		}
		else
		{
     
     
			return false;//若key等于当前结点值,说明插入的值不合法,返回false
		}
	}
	//2、进行与父亲的链接
	cur = new Node(key);
	if (parent->_key < key)
	{
     
     
		parent->_right = cur;//比父亲的值大连接在右子树
	}
	else
	{
     
     
		parent->_left = cur;//比父亲的值小链接在左子树
	}
	return true;
}
  • **补充:**搜索二叉树以相对有序的方式插入会比较坑,因为高度太高。

3.3.2.递归实现

这里依旧是分为两大步骤走,1、先递归到合适位置,确定插入的值链接在何处,2、找到位置后链接即可。

  • 1、递归找到插入的正确位置

这里虽是递归,不过走的形式和非递归的找到正确位置整体思路大差不差:

  1. 若key > root指向的结点值,让root递归到右子树继续遍历。
  2. 若key < root指向的结点值,让root递归到左子树继续遍历。
  3. 若key = root指向的结点值,说明待插入的结点值与此树当前结点值重合,插入结点失败。返回false。

image-20230328173327988

当root结点递归到nullptr时,即可进行下一步:链接。

  • 2、找到位置后,进行链接插入的结点

先前非递归版本的链接过程中为了要找到新插入结点和父亲的链接关系,我们特地创建了parent指针,让cur结点在不断的遍历中更新parent的指向以此时刻保持parent为cur的父亲,这样链接关系就确认好了,不过这里的递归实现我们并不给与一个parent指针,而是采用一个巧妙的方法:参数为指针的引用!

  • 图示说明:

image-20230328185137905

通过这里可以看出**传指针的引用已然达到没有父指针,胜似父指针的效果!!!**

//递归版删除
bool EraseR(const K& key)
{
     
     
	return _EraseR(_root, key);
}
//插入的子树
bool _InsertR(Node*& root, const K& key)//Node*&为指针的引用
{
     
     
	if (root == nullptr)
	{
     
     
		root = new Node(key);//当root为空,把自己创建成新结点
		return true;
	}
	if (root->_key < key)
		return _InsertR(root->_right, key);//如果比key小,转换到右子树去插入
	else if (root->_key > key)
		return _InsertR(root->_left, key);//如果比key大,转换到左子树去插入
	else
		return false;//如果相等,就返回false
}

3.4.Erase删除函数

3.4.1.非递归实现

二叉搜索树的删除函数最为复杂,这里我们主要通过两大步骤进行删除的操作:

  1. 遍历找到待删值的位置
  2. 删除找到的位置并链接父亲与剩下的结点

接下来针对这两大步骤展开讨论:

  • 一、先找到要删除的结点:

首先定义cur指针指向根结点(cur指针用于找到待删除结点的位置),定义parent指针指向nullptr(parent指针用于删除后的链接操作),定义key为删除结点的值,按如下规则进行遍历:

  1. 若key > cur指向结点的值,让parent走到cur的位置,让cur走到右子树进行遍历
  2. 若key < cur指向结点的值,让parent走到cur的位置,让cur走到左子树进行遍历
  3. 若key = cur指向结点的值,接下来进行删除结点和链接的操作。

image-20230328190253218

此时可以指向第二部,删除找到的位置并链接父亲与剩下的结点。

  • 二、删除结点并链接父亲与剩下的结点:

当我删去结点后,一个最值得考虑的问题是,如果待删值还有孩子怎么办呢,因此还要考虑到链接父亲与孩子的问题,并且又要进行如下分类:

  1. 待删值只有一个孩子 – 左为空 or 右为空
  2. 待删值两个孩子都在 – 替换法删除

接下来同样是进行展开讨论:

1、待删值只有一个孩子 – 左为空 or 右为空

我们按如下四步走:

  1. 如果左孩子为空且删除的值为根结点,直接更新根结点为右孩子(右孩子为空,就相反操作)
  2. 如果父亲的左孩子为待删值,将父亲的左孩子指向待删值指向的右孩子
  3. 如果父亲的左孩子不是待删值,将父亲的右孩子指向待删值指向的右孩子
  4. 删除待删的结点

image-20230328191545715

2、待删值两个孩子都在 – 替换法删除

替换法删除的目的在于我删除目标结点后,让左子树或右子树其中一个叶结点到删除的位置上来,又要保持其删除后依旧是一个二叉搜索树的特性(左子树 < 根 < 右子树),这就要用到替换法。

准备工作如下:

  1. 定义Parent指针为cur指针的位置(Parent指针用于链接要删除结点的孩子)
  2. 定义minRight指针为cur的右孩子结点指针的位置(minRight用于找到右子树的最小值)

具体替换法的操作如下:

  1. 遍历minRight找到待删结点右子树的最小值(或左子树的最大值结点),中途不断更新myParent
  2. 找到后,利用swap函数交换此最小值结点的值(minRight-> _key)和待删结点的值(cur-> _key)
  3. 交换后,链接父亲Parent指针与minRight结点的孩子
  4. 最后记得delete删除minRight结点

image-20230328194330469

注意:若整个操作两大步骤遍历一遍找不到要删除的值,直接返回false。

  • 代码如下:
//Erase删除
bool Erase(const K& key)
{
     
     
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
     
     
		//1、先找到要删除的结点
		if (cur->_key < key)
		{
     
     
			parent = cur;
			//让parent始终为cur的父亲
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
     
     
			parent = cur;
			//让parent始终为cur的父亲
			cur = cur->_left;
		}
		else
		{
     
     
			//找到了,分两类情况讨论:
			//1、待删值只有一个孩子 -- 左为空 or 右为空
			//2、待删值两个孩子都在 -- 替换法删除
			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* Parent = cur;//右子树的根可能就是minRight,所以这里Parent不能为nullptr,
				//因为此时不会进入while循环导致Parent就一直为nullptr,最后删除的时候造成野指针的非法访问
				Node* minRight = cur->_right;
				while (minRight->_left)
				{
     
     
					Parent = minRight;
					//让Parent始终为minRight的父亲
					minRight = minRight->_left;
				}
				swap(minRight->_key, cur->_key);//或者cur->_key = minRight->_key;
				//链接父亲Parent和要删除的结点的孩子
				if (Parent->_left == minRight)
				{
     
     
					//如果Parent的左孩子为待删值,让Parent的左孩子指向minRight的右
					minParent->_left = minRight->_right;
				}
				else
				{
     
     
					//如果Parent的右孩子为待删值,让Parent的右孩子指向minRight的右
					Parent->_right = minRight->_right;
				}
				//删除要删的结点
				delete minRight;
			}
			return true;
		}
	}
	//遍历一遍找不到要删除的值,直接返回false
	return false;
}

改进方法:

方法二在遇到待删除结点的左右子树均不为空的情况时的处理方式与方法一不同,方法二在找到待删除结点右子树当中值最小的结点后,先将minRight的值记录下来,然后再重新调用删除函数删除二叉树当中的minRight,当minRight被删除后再将原待删除结点的值改为minRight的值,这样也完成了左右子树均不为空的结点的删除。

//Erase删除
bool Erase(const K& key)
{
     
     
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
     
     
		//1、先找到要删除的结点
		if (cur->_key < key)
		{
     
     
			parent = cur;
			//让parent始终为cur的父亲
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
     
     
			parent = cur;
			//让parent始终为cur的父亲
			cur = cur->_left;
		}
		else
		{
     
     
			//找到了,分两类情况讨论:
			//1、待删值只有一个孩子 -- 左为空 or 右为空
			//2、待删值两个孩子都在 -- 替换法删除
			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* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
				//寻找待删除结点右子树当中值最小的结点
				while (minRight->_left)
				{
     
     
					//一直往左走
					minRight = minRight->_left;
				}
				minKey = minRight->_key; //记录minRight结点的值
				Erase(minKey); //minRight代替待删除结点被删除
				cur->_key = minKey; //将待删除结点的值改为代替其被删除的结点的值,即minRight
			}
			return true;
		}
	}
	//遍历一遍找不到要删除的值,直接返回false
	return false;
}

总结:这种方法的本质就是复用Erase函数,但是有的同学可能会说我们的Erase函数还没写完呢?其实不影响,我们这种情况只针对替换法删除,因为替换法会替换待删除节点值和其右子树的最小值,右子树的最小值的左子树一定为空,所以可以调用Erese函数,因为待删值只有一个孩子的条件已经写好了,所以可以复用。实在是巧妙!!!

3.4.2.递归实现

这里和非递归的主要实现思路大差不差,也是分为先找到删除的合适结点位置,找到后将其删除并确保链接关系正确这两大步骤。接下来,详细讨论下:

  • 一、先找到要删除的结点:

找到要删除的结点很简单,非递归是通过遍历的方式,只不过这里利用了递归来解决:

  1. 若当前结点root为空,说明此删除的结点不存在,返回false
  2. 若key > root指向的结点值,让root递归到右子树继续遍历。
  3. 若key < root指向的结点值,让root递归到左子树继续遍历。

image-20230328195323711

如果递归到key = root的结点值,接下来即可进入第二大步骤:删除此结点 + 链接父子关系。

  • 2、删除此结点 + 链接父子关系:

当我删去结点后,面临和非递归的删除同样一个问题:如果待删值还有孩子怎么办呢,因此还要考虑到链接父亲与孩子的问题,并且又要进行如下分类:

  1. 待删值只有一个孩子 – 左为空 or 右为空 or 左右均为空
  2. 待删值两个孩子都在 – 替换法删除
  • 这里的核心写法和插入的递归实现一样,传参要传指针的引用,接下来,这两种删除情况我都会详细讲解下如何利用好传参要传指针的引用

1、待删值只有一个孩子 – 左为空 or 右为空 or 左右均为空

我们按如下三步走:

  • 1、先把要删除的结点指针root保存为del
  • 2、如果root的左孩子为空,执行root = root->_right;

此时的root为指针的引用,即父结点的左指针或右指针,假设root为父结点的右指针。执行此段代码的意思是让父结点的右孩子指针(root)链接到root的右孩子,即可天然借助指针的引用建立了父子的链接关系

  • 3、如果root的右孩子为空,执行root = root->_left;

这种情况和上面无任何区别,只是链接方向变了,思路均一样。下面给出图示说明:

image-20230328201059015

2、待删值两个孩子都在 – 替换法删除

准备工作如下:

  1. 先把要删除的结点指针root保存为del
  2. 定义minRight指针为root的右孩子结点指针的位置(minRight用于找到右子树的最小值)

具体替换法的操作如下:

  1. 遍历minRight找到待删结点右子树的最小值(或左子树的最大值结点)
  2. 找到后,利用swap函数交换此最小值结点的值(minRight->_ key)和待删结点的值(root->_key)
  3. 交换后,到子树复用递归删除:return _ EraseR(root->_right, key);意思是利用递归删除

注意:上述第2步,交换后破坏了二叉搜索树的结构,我们无法使用递归从root节点开始删除节点3,但是这时我们使用子递归函数传递root的右子树,在节点3、6、7这棵树中删除节点3,在这颗子树中不违反二叉搜索树的结构。

图示说明:

image-20230328203349108

//递归版删除
bool EraseR(const K& key)
{
     
     
	return _EraseR(_root, key);
}
//删除的子树
bool _EraseR(Node*& root, const K& key)
{
     
     
	//1、递归查找删除的位置
	if (root == nullptr)
	{
     
     
		//如果是空就返回false
		return false;
	}
	if (root->_key < key)
	{
     
     
		return _EraseR(root->_right, key);//如果比key小,转换到右子树去插入
	}
	else if (root->_key > key)
	{
     
     
		return _EraseR(root->_left, key);//如果比key大,转换到左子树去插入
	}
	//2、确认链接关系
	else
	{
     
     
		Node* del = root;//提前保存root结点的位置
		//开始删除
		if (root->_left == nullptr)
		{
     
     
			//如果左为空
			root = root->_right;
		}
		else if (root->_right == nullptr)
		{
     
     
			//如果右为空
			root = root->_left;
		}
		else
		{
     
     
			Node* minRight = root->_right;//minRight用于找到右子树的最小值
			while (minRight->_left)
			{
     
     
				minRight = minRight->_left;
			}
			swap(root->_key, minRight->_key);
			return _EraseR(root->_right, key);
		}
		delete del;
		return true;
	}
}

巧妙做法:

方法一在遇到待删除结点的左右子树均不为空的情况时,采用的处理方法如下:

  • 使用minParent标记根结点右子树当中值最小结点的父结点。
  • 使用minRight标记根结点右子树当中值最小的结点。

当找到根结点右子树当中值最小的结点时,先根结点的值改为minRight的值,之后直接判断此时minRight是minParent的左孩子还是右孩子,然后对应让minParent的左指针或是右指针转而指向minRight的右孩子(注意:minRight的左孩子为空),最后将minRight结点进行释放即可。

//递归版删除
bool EraseR(const K& key)
{
     
     
	return _EraseR(_root, key);
}
//删除的子树
bool _EraseR(Node*& root, const K& key)
{
     
     
	//1、递归查找删除的位置
	if (root == nullptr)
	{
     
     
		//如果是空就返回false
		return false;
	}
	if (root->_key < key)
	{
     
     
		return _EraseR(root->_right, key);//如果比key小,转换到右子树去插入
	}
	else if (root->_key > key)
	{
     
     
		return _EraseR(root->_left, key);//如果比key大,转换到左子树去插入
	}
	//2、确认链接关系
	else
	{
     
     
		Node* del = root;//提前保存root结点的位置
		//开始删除
		if (root->_left == nullptr)
		{
     
     
			//如果左为空
			root = root->_right;
		}
		else if (root->_right == nullptr)
		{
     
     
			//如果右为空
			root = root->_left;
		}
		else
		{
     
     
			Node* minRight = root->_right;//minRight用于找到右子树的最小值
			while (minRight->_left)
			{
     
     
				minRight = minRight->_left;
			}
			minKey = minRight->_key; //记录minRight结点的值
			_EraseR(root->_right, minKey); //删除右子树当中值为minkey的结点,即删除minRight
			root->_key = minKey; //将根结点的值改为minRight的值
		}
		delete del;
		return true;
	}
}

总结:此方法和Erase删除函数的非递归实现的第二种方法本质是一样的,这里就不赘述了。


3.5.Find查找函数

3.5.1.非递归实现

Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:

  1. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  2. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  3. 若key值等于当前结点的值,则查找成功,返回true。
  4. 若遍历一圈cur走到nullptr了说明没有此结点,返回false
//Find非递归
bool Find(const K& key)
{
     
     
	Node* cur = _root;
	while (cur)
	{
     
     
		if (cur->_key < key)
		{
     
     
			cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
		}
		else if (cur->_key > key)
		{
     
     
			cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
		}
		else
		{
     
     
			return true;//若key值等于当前结点的值,则查找成功,返回true。
		}
	}
	return false;//遍历一圈没找到返回false
}

3.5.2.递归实现

递归的实现主要是转换成子问题来解决。针对于Find的递归实现,只需遵循如下规则即可:

  1. 若树为空树,则查找失败,返回nullptr。
  2. 若key值小于当前结点的值,则递归到该结点的左子树当中进行查找。
  3. 若key值大于当前结点的值,则递归到该结点的右子树当中进行查找。
  4. 若key值等于当前结点的值,则查找成功,返回对应结点的地址。
//递归版查找
bool FindR(const K& key)
{
     
     
	return _FindR(_root, key);
}
//查找的子树
bool _FindR(Node* root, const K& key)
{
     
     
	if (root == nullptr)
		return false;
	if (root->_key < key)
	{
     
     
		//如果比key小,转换到右子树去找
		return _FindR(root->_right, key);
	}
	else if (root->_key > key)
	{
     
     
		//如果比key大,转换到左子树去找
		return _FindR(root->_left, key);
	}
	else
	{
     
     
		//找到了
		return true;
	}
}

三、二叉搜索树的应用

1.K模型

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

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

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

其实我前面模拟实现的二叉搜索树就是一个K模型。

2.KV模型

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

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

我们可以针对K模型,在其内部实现进行稍稍修改即可达到KV模型的实现,代码链接直达:BSTree/BSTree/BSTree_K_V.h · wei/cplusplus - 码云 - 开源中国 (gitee.com)

接下来给出测试用例:

  • 1、英文单词与对应的中文单词:
namespace K_V
{
     
     
	void TestBSTree1()
	{
     
     
		// 输入单词,查找单词对应的中文翻译
		BSTree<string, string> dict;
		dict.InsertR("string", "字符串");
		dict.InsertR("tree", "树");
		dict.InsertR("left", "左边、剩余");
		dict.InsertR("right", "右边");
		dict.InsertR("sort", "排序");
		// 插入词库中所有单词
		string str;
		while (cin >> str)
		{
     
     
			BSTreeNode<string, string>* ret = dict.FindR(str);
			if (ret == nullptr)
			{
     
     
				cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
			}
			else
			{
     
     
				cout << str << "中文翻译:" << ret->_value << endl;
			}
		}
	}
}
int main()
{
     
     
	K_V::TestBSTree1();
}

image-20230328204025664

  • 2、单词与其出现次数统计
namespace K_V
{
     
     
	void TestBSTree2()
	{
     
     
		string arr[] = {
     
      "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
		//统计水果出现的次数
		BSTree<string, int> countTree;
		int count = 0;
		for (const auto& str : arr)
		{
     
     
			auto ret = countTree.FindR(str);
			if (ret == nullptr)
			{
     
     
				countTree.InsertR(str, 1);
			}
			else
			{
     
     
				ret->_value++;//修改value
			}
		}
		countTree.InOrder();
	}
}
int main()
{
     
     
	K_V::TestBSTree2();
}

image-20230328203719086


四、二叉搜索树性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

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

image-20230320220620724

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N / 2
  • 综上时间复杂度为O(N)。

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


猜你喜欢

转载自blog.csdn.net/m0_64224788/article/details/129880405