二叉搜索树(BSTree)的介绍与模拟实现

目录

基础概念

其他性质

为什么要存在二叉搜索树呢?

模拟实现BinarySearchTree类(二叉搜索树)

基础框架

成员函数Insert(非递归版本)

成员函数Find(非递归版本)

成员函数Erase(非递归版本)

成员函数Find(递归版本)

成员函数Insert(递归版本)

成员函数Erase(递归版本)

Insert、Erase、Find这三个成员函数都有非递归和递归的版本,哪个版本更好呢?

析构函数

拷贝构造

赋值运算符重载operator=

BSTree的整体代码(可复制)

二叉搜索树的缺陷(时间复杂度)

二叉搜索树的应用


基础概念

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

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

其他性质

1.为什么上面说二叉搜索树也被称为二叉排序树呢?因为对二叉搜索树进行中序遍历时,数据一定是升序,没有其他可能。

2.同时因为二叉搜索树在插入新节点时,如果新节点的值和二叉搜索树中的任意节点的值相等,则新节点会插入失败,此时Insert函数会什么也不做,直接return false,所以严格意义上说,往二叉搜索树中不断插入元素时,二叉搜索树不仅支持排升序,还能支持去重。

3.二叉搜索树中值最小的节点一定是:从根节点一直向左遍历,如果谁的左子树为nullptr,则谁就是值最小的节点,我们叫它最左节点。

4.二叉搜索树中值最大的节点一定是:从根节点一直向右遍历,如果谁的右子树为nullptr,则谁就是值最大的节点,我们叫它最右节点。

5.BSTree在key模型下只支持增Insert、删Erase、查Find,不支持直接改,只能通过【先删再增】的方式间接修改,只有在【key,value】模型下支持直接改。这也很好理解,毕竟如果可以修改key,搜索树的性质就不能被保证了,比如不能保证【若它的左子树不为空,则左子树上所有节点的值都小于根节点的值】等等性质。

6.同一堆元素,如果插入顺序不一样,则BSTree的结构也会不一样,这是BSTree的一个缺陷。

为什么要存在二叉搜索树呢?

以前学习的数据结构比如顺序表和链表,它们都是基础的数据结构,这些结构存储数据的效率还行,但在这些结构中查找数据时效率会特别低,因为只能遍历,即暴力查找。有人可能会说二分查找啊,在N个数据中找一个值的时间复杂度为O(logN),效率这么高。的确,但你忘记了一点,二分查找是有前提的,那就是有序,所以在查找前还需要手动对数据进行排序,排序的消耗是很大的,所以综合来说,二分查找的效率也不高。为此二叉搜索树和哈希表就诞生了,它们是专门用于查找数据的场景的数据结构。

模拟实现BinarySearchTree类(二叉搜索树)

为什么我们叫二叉搜索树而不叫搜索二叉树呢?因为搜索二叉树英文的缩写叫SBTree,有点骂人的意思,所以我们叫二叉搜索树,英文的缩写是BSTree。

因为BSTree求高度或者求节点个数的这些成员函数的实现方法和二叉树一致,所以这里不再编写。

基础框架

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:

private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

我们给BSTreeNode的模板参数名叫K,这是因为二叉搜索树比较节点大小时,一般称呼节点的值为关键值或者关键码,即key,模板参数又是key成员的类型,所以干脆叫K。

因为BSTree中只有一个内置类型的成员,所以给了缺省值nullptr后就不必显示写默认构造函数了。

成员函数Insert(非递归版本)

当插入指定节点时,节点应该插在哪里呢?

有2种情况。

第1种:当二叉搜索树是一颗空树的时候,直接指定该节点为整棵树的根节点即可。

第2种:当二叉搜索树不是一颗空树时,则需要进行判断,如果正在被插入的节点的key值(上文讲过关键值)比当前遍历到的节点的key值大,则往当前节点的右子树继续遍历,如果比当前遍历到的节点的key值小,则往当前节点的左子树继续遍历,如果和当前遍历到的节点的key值相等,因为搜索二叉树中不允许存在相同的key值,所以插入失败,return false退出Insert函数,如果没有遇到key值相等的情况,并在遍历过程中遇到了空位置,我们就将指定节点插入到这个空位置即可,如下两图示例。

从第2种情况我们可以得出一点:二叉搜索树在插入新节点时,如果新节点的值和二叉搜索树中的任意节点的值相等,则新节点会插入失败,此时Insert函数会什么也不做,直接return false,所以严格意义上说,往二叉搜索树中不断插入元素时,二叉搜索树不仅支持排升序,还能支持去重。这也呼应了上文讲解二叉搜索树的性质的部分。

示例1

 示例2

代码如下。

给了一个Node*parent,这是因为被插入节点找到插入位置(也就是nullptr位置)后,我们需要被插入节点的父亲节点将被插入节点连接起来,因为Node类不是一个三叉链(忘记了去看二叉树那一章),所以如果不给一个parent,我们就不知道被插入节点的父亲节点是谁。我们设置了parent记录cur的父亲节点是谁后,每当cur向左或者向右走前,先让parent=cur,这样parent就永远是cur的父亲节点了。

返回值为bool类型,如果插入成功则返回true,反之返回false。

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		if (key > parent->_key)
			parent->_right = new Node(key);
		else
			parent->_left = new Node(key);

		return true;

	}

private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

开始测试Insert,咱们先编写一个中序遍历,如果是升序,则说明Insert没有问题。

Inorder中序遍历代码如下。

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:

	void Inorder(Node*root)
	{
		if (root->_left != nullptr)
			Inorder(root->_left);

		cout << root->_key<<' ';

		if (root->_right != nullptr)
			Inorder(root->_right);
	}
	//写法2
	/*void Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		Inorder(root->_left);
		cout << root->_key << ' ';
		Inorder(root->_right);
	}*/

private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

中序遍历整颗搜索二叉树还存在一个问题,那就是在BSTree类外通过BSTree类的对象调用Inorder函数时,拿不到私有的_root作为Inorder的实参。

有人可能会说,在类内给Inorder的形参一个缺省值_root不就行了,想法很美好,现实很残酷,因为只有全局变量或者常量才可以作为缺省值,this->_root不符合做缺省值的条件。

那该怎么办呢?

有人会说在类内部写一个GetRoot接口,让它return _root,这样我们在类外调用该接口就能获得_root的值了,在类外调用中序遍历时就有实参了。这种方法是可行的,但不是最优解,因为我们设置_root为私有就是不希望暴露他,现在你在类外调用GetRoot获得私有成员_root的值就违背了初衷。除此之外,编写GetRoot对程序员也有成本,该程序员必须得知道该接口的返回值类型一定只能为两种,如果不是引用类型,则只能为const Node*,如果是引用类型,则只能为const Node*const &。为什么呢?如果GetRoot的返回值类型为Node*,那别人有可能通过该接口修改根节点上的key值;如果返回值类型为Node*&,则别人有可能通过该接口修改_root的指向,导致整棵树都找不到了,内存泄漏,同时还有可能通过该接口修改根节点上的key值;除这些之外,还可能会产生其他意想不到的问题,所以GetRoot的返回值不能胡乱设计,所以编写时对程序员是有成本的。所以因为这两个缺点(违背初衷和编写成本),GetRoot不是最优解。

也有人会说,那我将上面的test1()函数设置成BSTree类的友元函数,不就可以访问_root了嘛?这种方法也是可行的,但也不是最优解,甚至比GetRoot还要差,因为本种方法不仅包含了GetRoot的两个缺点,还有一个额外的缺点,就是太挫了,不优雅,不好看。

接下来看看最优解是怎么做的。

我们如果完全不暴露_root,那么意味着在BSTree类外是绝对访问不了_root的,但现在我们又需要在BSTree类外访问_root(因为在BSTree类外通过BSTree类的对象调用成员函数Inorder时需要_root的值作为参数),这不就矛盾了吗?的确,所以我们换一种思路,既然在BSTree类外是绝对访问不了_root的,那我们就不在BSTree类外访问_root,转而在BSTree类内访问_root,即让在BSTree类外调用的成员函数Inorder不需要参数,然后增加一个_Inorder函数,让Inorder的实现就是对_Inorder的一层封装,然后让_Inorder函数在BSTree类内访问_root,如下图所示。

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	void Inorder()
	{
		_Inorder(_root);
	}
private:
	void _Inorder(Node* root)
	{
        if(root==nullptr)//必须有这两行代码,有一种特殊情况,一开始root就是nullptr,则下面解引用会报错。
            return;

		if (root->_left != nullptr)
			_Inorder(root->_left);

		cout << root->_key << ' ';

		if (root->_right != nullptr)
			_Inorder(root->_right);
	}
	//写法2
	/*void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_key << ' ';
		_Inorder(root->_right);
	}*/

	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

做完上面的内容后,接下来就可以开始测试了。

如下图所示,将数组中的所有元素插进搜索二叉树后,因为对二叉树进行中序遍历是升序,所以咱们编写的Insert接口是正确的。

测试代码如下。

#include"BSTree.h"

void test1()
{
	int a[] = { 3,2,1,5,4,6,7,9,8,10 };
	BSTree<int>b;
	for (int& e : a)
	{
		b.Insert(e);
	}
	b.Inorder();
}

void main()
{
	test1();
}

成员函数Find(非递归版本)

示例1

示例2

示例3

Find接口的逻辑基本复用了Insert的逻辑。

观察上面3个示例的规律可以发现,如果不存在正在被寻找的节点,则寻找时一定会遍历到NULL位置结束;如果搜索二叉树中存在正在被寻找的节点,则一定在遍历到NULL位置之前就能找到目标节点。

代码逻辑如下。

Find返回值类型为bool,找到了返回true,反之返回false。

如果正在被寻找的节点的key值(上文讲过关键值)比当前遍历到的节点的key值大,则往当前节点的右子树遍历继续寻找,如果比当前遍历到的节点的key值小,则往当前节点的左子树遍历继续寻找,如果和当前遍历到的节点的key值相等,则找到了,如果没有遇到key值相等的情况,并且此时已经遍历到了空位置,则说明搜索二叉树中不存在正在被寻找的节点。

代码如下。可以发现的确和Insert的代码差不多。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:   
    bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (key > cur->_key)
				cur = cur->_right;
			else if (key < cur->_key)
				cur = cur->_left;
			else
				return true;
		}
		//走到这里cur==nullptr,说明已经遍历到空位置了还没有找到目标节点,说明目标节点不存在,返回false即可
		return false;
	}

Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

成员函数Erase(非递归版本)

对于二叉搜索树的删除,首先查找元素是否在二叉搜索树中,如果不存在,则无需删除,直接return false,表示无法删除;如果存在目标节点,则要删除的目标结点可能分下面四种情况:

a.要删除的结点无孩子结点

b.要删除的结点只有左孩子结点

c.要删除的结点只有右孩子结点

d.要删除的结点有左、右孩子结点

上面看起来有4种情况,按理来说会有4种应对方案,但实际上,情况a可以看作是情况b,情况a也可以看作是情况c,所以最终只会有应对b、c、d这3种情况的3个方案,为什么呢?

拿下图的节点7举例为什么情况a可以看作是情况b。

假如要删除7,7是一个叶子节点,没有左或者右孩子节点,符合情况a,所以删除7非常简单,直接让7的父亲节点6的指针成员_right指向nullptr,然后delete删除节点7即可。那么这为什么可以看作是情况b呢?情况b是:要删除的节点只有左孩子节点。那么我们假设节点7只有左孩子节点,但我们都知道7是没有孩子节点的,它是叶子节点,所以这里我们只是假设。当7只有左孩子节点时,7被删除后,我们肯定是要将7的左子树继续挂在整颗树上的,所以7的父亲节点6的指针成员_right肯定要指向7的左孩子节点(注意这里不能用_left指向7的左孩子节点,因为7的左孩子虽然比7小,但比6大)。综上所述,如果7只有左孩子节点,则删除7要让父亲节点6的_right指向7的_left;如果7没有孩子节点,则删除7要让父亲节点6的_right指向nullptr,重点来了,因为此时7没有孩子节点,7的_left就是nullptr,所以删除7要让父亲节点6的_right指向nullptr就等价于让父亲节点6的_right指向7的_left。回看当7只有左孩子时的情况,此时删除7也是要让父亲节点6的_right指向7的_left。这就说明了当7没有孩子节点时,删除7的解决方案是可以套用【当7只有左孩子节点时的删除7的方案】,情况a下删除节点可以使用情况b下删除节点的方案,不就证明了情况a可以看作是情况b吗?没错,所以情况a可以看作是情况b。

情况a可以看作是情况c的原因与a能看作是b的原因完全一致,不再赘述。

综上所述,因为a能看作是b,a也能看作是c,所以我们不必对情况a做单独处理,所以最终只有b、c、d三种情况,应对处理方案也只有三种。

3种解决方案如下:

情况b的解决方案:删除目标节点前首先要找到它,找到后删除目标结点且使被删除节点的父亲结点指向被删除节点的左孩子结点——直接删除法。这里只说了让父亲节点指向被删除节点的左孩子节点,但父亲节点有_left指针和_right指针,那让父亲节点的哪个指针指向被删除节点的左孩子节点呢?这得看被删除节点是它的父亲节点的左孩子还是右孩子,如果是左孩子,则是父亲节点的_left去指向;如果是右孩子,则是父亲节点的_right去指向。

(普通)示例1如下图所示,现在要删除3。

首先找到3,3只有左孩子节点,因为被删除的节点3是父亲节点8的左孩子,所以以3的左孩子节点1作为根节点的子树中的每个节点肯定都比被删除节点的父亲节点8小,因此直接将8的_left指向3的左孩子节点1,然后delete节点3即可。

(普通)示例2如下图所示,现在要删除7。

首先找到7,7没有左孩子也没有右孩子,属于情况a,但上面说过可以将情况a视作情况b,是可以假设7只有左孩子节点的,所以删除7时可以套用情况b的解决方案。7只有左孩子节点时,因为被删除的节点7是父亲节点6的右孩子,所以以7的左孩子节点作为根节点的子树中的每个节点肯定都比被删除节点的父亲节点6大,因此直接将6的_right指向7的左孩子节点,然后delete节点7即可。

(特殊)示例3如下图所示,现在要删除8。

首先找到8,8只有左孩子节点,按理说删除8只要让8的父亲节点指向8的左孩子节点,然后delete节点8即可,但现在的问题是8没有父亲节点,Node*parent=nullptr,套用普通示例的方法会因为parent->left或者parent->right解引用空指针导致程序崩溃,所以在该种情况下,直接让_root变成节点3,即更换整颗BSTree的根节点,然后delete节点8即可。

分割线——————————

情况c的解决方案:删除目标节点前首先要找到它,找到后删除目标结点且使被删除节点的双亲结点指向被删除结点的右孩子结点——直接删除法。这里只说了让父亲节点指向被删除节点的右孩子结点,但父亲节点有_left指针和_right指针,那让父亲节点的哪个指针指向被删除节点的右孩子节点呢?这得看被删除节点是它的父亲节点的左孩子还是右孩子,如果是左孩子,则是父亲节点的_left去指向;如果是右孩子,则是父亲节点的_right去指向。

(普通)示例1如下图所示,现在要删除10。

首先找到10,10只有右孩子节点,因为被删除的节点10是父亲节点8的右孩子,所以以10的右孩子节点14作为根节点的子树中的每个节点肯定都比被删除节点的父亲节点8大,因此直接将8的_right指向10的右孩子节点14,然后delete节点10即可。

(普通)示例2如下图所示,现在要删除4。

首先找到4,4没有左孩子也没有右孩子,属于情况a,但上面说过可以将情况a视作情况c,是可以假设4只有右孩子节点的,所以删除4时可以套用情况c的解决方案。4只有右孩子节点时,因为被删除的节点4是父亲节点6的左孩子,所以以4的右孩子节点作为根节点的子树中的每个节点肯定都比被删除节点的父亲节点6小,因此直接将6的_left指向4的右孩子节点,然后delete节点4即可。

(特殊)示例3如下图所示,现在要删除8。

首先找到8,8只有右孩子节点,按理说删除8只需让8的父亲节点指向8的右孩子节点,然后直接delete节点8即可。但现在的问题是8没有父亲节点,Node*parent=nullptr,套用普通示例的方法会因为parent->left或者parent->right解引用空指针导致程序崩溃,所以在该种情况下,直接让_root变成节点10,即更换整颗BSTree的根节点,然后delete节点8即可。

分割线——————————

情况d的解决方案

有两种。

第一种:(可结合下图思考)删除目标节点A前首先要找到它,找到A后,在需要被删除的节点A的右子树中寻找中序遍历下的第一个结点B,即关键码最小的节点B,然后将需要被删除的节点A的值替换成节点B的值,最后删除下图中右半部分打红叉的、关键码最小的节点B,B被删除后,就相当于删除了目标节点A(因为BSTree中不再存在节点A的关键码),此时删除任务也就完成了。——替换删除法。那么如何删除节点B呢?

因为节点B虽然绝对不存在左孩子(毕竟如果B有左孩子,那么节点B就不是关键码最小的节点),但节点B是可能存在右孩子的,所以我们delete掉节点B前,要考虑将节点B的右子树挂到整颗BSTree上,所以要让节点B的父亲节点指向节点B的_right,注意即使节点B没有右孩子,B的父亲节点也要指向nullptr,否则就产生悬空指针了(因为我们马上会delete节点B,B的父亲节点不应再指向B),而因为此时B的_right刚好就是nullptr,所以这里让节点B的父亲节点指向节点B的_right可谓是一举两得,即不管节点B有无右孩子,让节点B的父亲节点指向节点B的_right都是正确的)。现在还有最后一个问题:节点B的父亲节点有_left和_right两个指针成员,那让哪个指针成员指向节点B的_right呢?答案:【如果节点B是它父亲节点的左孩子,则让父亲节点的_left指向节点B的_right;如果节点B是它父亲节点的右孩子,则让父亲节点的_right指向节点B的_right。】最后delete下图中右半部分打红叉的、关键码最小的结点B。这就是删除节点B的全部流程。

第二种:(可结合下图思考)删除目标节点A前首先要找到它,找到A后,在需要被删除的节点A的左子树中寻找中序遍历下的最后一个结点B,即关键码最大的节点B,然后将需要被删除的节点A的值替换成节点B的值,最后删除下图中右半部分打红叉的、关键码最大的节点B,B被删除后,就相当于删除了目标节点A(因为BSTree中不再存在节点A的关键码),此时删除任务也就完成了。——替换法删除法。那么如何删除节点B呢?

因为节点B虽然绝对不存在右孩子(毕竟如果B有右孩子,那么节点B就不是关键码最大的节点),但节点B是可能存在左孩子的,所以我们delete掉节点B前,要考虑将节点B的左子树挂到整颗BSTree上,所以要让节点B的父亲节点指向节点B的_left,注意即使节点B没有左孩子,即B的_left是nullptr,因为B的父亲节点也要指向nullptr,否则就产生悬空指针了(因为我们马上会delete节点B,B的父亲节点不应再指向B),而因为此时B的_left刚好就是nullptr,所以这里让节点B的父亲节点指向节点B的_left可谓是一举两得,即不管节点B有无左孩子,让节点B的父亲节点指向节点B的_left都是正确的。现在还有最后一个问题:节点B的父亲节点有_left和_right两个指针成员,那让哪个指针成员指向节点B的_left呢?答案:【如果节点B是它父亲节点的左孩子,则让父亲节点的_left指向节点B的_left;如果节点B是它父亲节点的右孩子,则让父亲节点的_right指向节点B的_left。】最后delete下图中右半部分打红叉的、关键码最大的结点B,这就是删除节点B的全部流程。

阶段性总结:

观察上面的两种替换删除法。可以发现替换删除法对比【情况b和情况c的直接删除法】是有区别的,区别在于:直接删除法是真的将被删除节点A的物理空间给delete释放掉了,而替换删除法没有将被删除节点A的物理空间给delete释放掉,只是把节点A的关键码变成了节点B的关键码,然后把节点B的物理空间给delete释放了,这相当于【伪delete】了节点A,因为BSTree中只是不存在节点A的关键码,但实际上节点A的物理空间还存在。笔者认为这也是为什么这种方法叫【替换删除法】,因为实际没有delete掉节点A,而是delete了节点B,通过delete节点B删除节点A,是谓【替换删除法】。

问题:对于上面的两种替换删除法,删除目标节点A时,第一种是在需要被删除的节点A的右子树中寻找中序遍历下的第一个结点B,即关键码最小的节点B,然后通过删除节点B来达到删除节点A的效果;第二种是在需要被删除的节点A的左子树中寻找中序遍历下的最后一个结点B,即关键码最大的节点B,然后通过删除节点B来达到删除节点A的效果。那你知道节点B为什么非得是这两个位置吗?

答案:(可结合下图思考)拿第一种替换删除法举例。如果需要被删除的节点既有左孩子,又有右孩子,那直接删除该节点会比较麻烦;但如果某个节点只有右孩子或者只有左孩子,那删除这个节点就非常容易了,这一点从上文中的情况b的解决方案和情况c的解决方案就可以看出来(情况b就是要删除的节点只有左孩子节点,情况c就是要删除的节点只有右孩子节点)。现在节点A就是一个既有左孩子,又有右孩子的节点,所以直接删除节点A会比较麻烦,然后要知道的是我们是可以通过删除一个只有右孩子的替死鬼节点B来达到删除节点A的效果的,这时因为替死鬼节点B只有右孩子,所以删除B会很容易,这就等价于通过这样的方法,会让删除节点A变得很容易。所以现在的问题就是如何找到一个只有右孩子的替死鬼节点。恰好,在要删除的节点A的右子树中中序遍历下的第一个结点就是一个只有右孩子的节点(因为如果该节点存在左孩子,那该节点就不是节点A的右子树中中序遍历下的第一个结点),所以节点B非得是这两个位置的原因之一就是:替换删除法需要一个只有左孩子或者只有右孩子的节点,而刚好这两个位置的节点必定是一个只有左孩子或者只有右孩子的节点。节点B非得是这两个位置的原因之二就是:替换删除法不光只是需要一个只有左孩子或者只有右孩子的节点,它还要求该节点的值必须符合搜索二叉树的要求,不能破坏搜索二叉树的性质,比如下图的节点5,虽然也是一个只有右孩子的节点,但如果5被填进节点A,那显然会破坏搜索二叉树的性质。

前面都是理论,接下来上实际例子,通过这样对情况d的删除的过程进行更深一步的理解。

咱们这里做一个规定:下文中所有的示例统一用第一种方案,即:

(可结合下图思考)删除目标节点A前首先要找到它,找到A后,在需要被删除的节点A的右子树中寻找中序遍历下的第一个结点B,即关键码最小的节点B,然后将需要被删除的节点A的值替换成节点B的值,最后删除节点B,B被删除后,就相当于删除了目标节点A(因为BSTree中不再存在节点A的关键码),此时删除任务也就完成了。——替换删除法。那么如何删除节点B呢?

因为节点B虽然绝对不存在左孩子(毕竟如果B有左孩子,那么节点B就不是关键码最小的节点),但节点B是可能存在右孩子的,所以我们delete掉节点B前,要考虑将节点B的右子树挂到整颗BSTree上,所以要让节点B的父亲节点指向节点B的_right,注意即使节点B没有右孩子,B的父亲节点也要指向nullptr,否则就产生悬空指针了(因为我们马上会delete节点B,B的父亲节点不应再指向B),而因为此时B的_right刚好就是nullptr,所以这里让节点B的父亲节点指向节点B的_right可谓是一举两得,即不管节点B有无右孩子,让节点B的父亲节点指向节点B的_right都是正确的)。现在还有最后一个问题:节点B的父亲节点有_left和_right两个指针成员,那让哪个指针成员指向节点B的_right呢?答案:【如果节点B是它父亲节点的左孩子,则让父亲节点的_left指向节点B的_right;如果节点B是它父亲节点的右孩子,则让父亲节点的_right指向节点B的_right。】最后delete下图中右半部分打红叉的、关键码最小的结点B。这就是删除节点B的全部流程。

示例1如下图所示,现在要删除3。

假设指向【被删除节点的右子树中关键码最小的节点(即中序遍历下的第一个结点)】的指针叫Rmin,指向【该节点的父亲节点】的指针叫RminParent。

首先找到需要删除的目标节点3,然后找到Rmin节点4和Rmin节点4的父亲节点RminParent节点6。【为什么要找Rmin的父亲节点6呢?因为Rmin节点虽然绝对不存在左孩子(毕竟如果Rmin有左孩子,那么Rmin节点就不是关键码最小的节点),但Rmin节点是可能存在右孩子的,我们delete掉Rmin节点前,要考虑将Rmin的右子树挂到整颗BSTree上。】

然后将Rmin节点4的值填到需要被删除的目标节点3中,最后删除Rmin节点即可完成删除目标节点3这个任务。该怎么删除Rmin节点呢?【因为Rmin是RminParent的左孩子,所以让RminParent节点6的_left指向Rmin节点4的_right,最后delete掉下图右半部分打红叉的Rmin节点4即可。】走到这里就成功删除了目标节点3。

示例2如下图所示,现在要删除8。

假设指向【被删除节点的右子树中关键码最小的节点(即中序遍历下的第一个结点)】的指针叫Rmin,指向【该节点的父亲节点】的指针叫RminParent。

首先找到需要删除的目标节点8,然后找到Rmin节点10和Rmin节点10的父亲节点RminParent节点8。【为什么要找Rmin的父亲节点8呢?因为Rmin节点10虽然绝对不存在左孩子(毕竟如果Rmin有左孩子,那么Rmin节点就不是关键码最小的节点),但Rmin节点是可能存在右孩子的,我们delete掉Rmin节点前,要考虑将Rmin的右子树挂到整颗BSTree上。】

然后将Rmin节点10的值填到需要被删除的目标节点8中,最后删除Rmin节点即可完成删除目标节点8这个任务。该怎么删除Rmin节点呢?【因为Rmin是RminParent的右孩子,所以让RminParent节点10的_right指向Rmin节点10的_right,最后delete掉下图右半部分打红叉的Rmin节点10即可。】走到这里就成功删除了目标节点8。

再次阶段性总结:走到这里,应该能够发现Erase的难点在于如何找到Rmin节点和RminParent,Rmin靠一个中序遍历即可搞定,最主要是RminParent,拿下图举例子,假如需要被删除的节点是8,则首先要找到被删除的节点8,假设cur指向节点8,则cur->_right,即节点10就是被删除节点的右子树的根节点,Rmin正是从该根节点10开始不断的向左移动,当Rmin不能再向左移动时,Rmin就是关键码最小的节点,每次Rmin=Rmin->_left前,都要RminParent=Rmin,但如下图,Rmin在第一次向左移动前就是关键码最小的节点,此时就会造成RminParent没有值,可能是个野指针或者是nullptr,之后通过RminParent连接Rmin的右子树时,就会解引用nullptr或者野指针导致程序崩溃,所以一开始最好让RminParent=cur,而不要RminParent=nullptr。

我们为什么要将情况a看作情况b或者情况c呢?或者说为什么要将情况a合并到情况b或者情况c里去?因为情况的分类越少,我们编写代码就越简洁。

分割线——————————

Erase的代码如下。

前面也说过,删除目标节点前,首先得找到目标节点,如果没找到,则说明树中不存在该节点,删除也会失败,直接return false,如果找到了,再执行删除的逻辑。为了使代码的模块更清晰,这里先编写【寻找被删除节点】的代码。

	bool Erase(const K& key)
	{
		//删除key之前,得先找到关键码为key的节点,和该节点的父亲节点,开始寻找
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if(key==cur->_key)
			{
				//找到了需要删除的目标节点cur与cur的父亲节点parent,开始删除,分情况b、c、d,每种情况都有对应的解决方案。	
			}
		}
		//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
		return false;
	}

然后编写找到【需要被删除的节点】后,该怎么删除目标节点的代码。

	        else if(key==cur->_key)//找到了需要删除的目标节点cur与cur的父亲节点parent,开始删除,分情况a、b、b,有对应的解决方案。
			{
				//情况b,被删除的目标节点cur只有左孩子,情况a可以看作情况b,所以情况a也会走这个if分支,不会走情况c的if分支,因为情况b的代码在情况c前面
				if (cur->_right==nullptr)
				{
					//对应文中例举的特殊示例3的代码,删除的目标节点cur是整颗BSTree的根节点,则直接更换新的根节点,然后将旧的根节点删除即可。
					if (cur == _root)
					{
						_root = cur->_left;
						delete cur;
						cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
					}
					//对应文中普通示例1和2的代码,删除的目标节点不是BSTree的根节点
					else
					{
						//被删除节点是它的父亲节点的左孩子
						if (cur = parent->_left)
						{
							parent->_left = cur->_left;
							delete cur;
						}
						//被删除节点是它的父亲节点的右孩子
						else if (cur = parent->_right)
						{
							parent->_right = cur->_left;
							delete cur;
						}

					}
				}
				//情况c,被删除的目标节点cur只有右孩子
				else if (cur->_left == nullptr)
				{
					//对应文中例举的特殊示例3的代码,删除的目标节点cur是整颗BSTree的根节点,则直接更换新的根节点,然后将旧的根节点删除即可。
					if (cur == _root)
					{
						_root = cur->_right;
						delete cur;
						cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
					}
					//对应文中普通示例1和2的代码,删除的目标节点不是BSTree的根节点
					else
					{
						//被删除节点是它的父亲节点的左孩子
						if (cur = parent->_left)
						{
							parent->_left = cur->_right;
							delete cur;
						}
						//被删除节点是它的父亲节点的右孩子
						else if (cur = parent->_right)
						{
							parent->_right = cur->_right;
							delete cur;
						}
					}
				}
				//情况d,被删除的目标节点既有左孩子,又有右孩子。文中也说过,应对情况d的解决方案有两种,咱们按第一种走:删除目标节点A时,找被删除节点A的右子树中关键码最小的节点B,
				else if (cur->_left != nullptr && cur->_right != nullptr)
				{
					Node* Rmin = cur->_right;
					Node* RminParent = cur;
					
					//先确定Rmin和RminParent的位置
					while (Rmin->_left!= nullptr)
					{
						RminParent = Rmin;
						Rmin = Rmin->_left;
					}
					//然后将需要被删除节点的关键码变成Rmin的关键码
					cur->_key = Rmin->_key;

					//最后删除Rmin节点即可删除【需要被删除目标的节点】,该怎么删除文中已经说过,不再赘述。
					if (Rmin == RminParent->_left)
						RminParent->_left = Rmin->_right;
					else if (Rmin == RminParent->_right)
						RminParent->_right = Rmin->_right;

					delete Rmin;
				}

Erase的整体代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
    bool Erase(const K& key)
	{
		//删除key之前,得先找到关键码为key的节点,和该节点的父亲节点,开始寻找
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if(key==cur->_key)//找到了需要删除的目标节点cur与cur的父亲节点parent,开始删除,分情况a、b、b,有对应的解决方案。
			{
				//情况b,被删除的目标节点cur只有左孩子,情况a可以看作情况b,所以情况a也会走这个if分支,不会走情况c的if分支,因为情况b的代码在情况c前面
				if (cur->_right==nullptr)
				{
					//对应文中例举的特殊示例3的代码,删除的目标节点cur是整颗BSTree的根节点,则直接更换新的根节点,然后将旧的根节点删除即可。
					if (cur == _root)
					{
						_root = cur->_left;
						delete cur;
						cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
					}
					//对应文中普通示例1和2的代码,删除的目标节点不是BSTree的根节点
					else
					{
						//被删除节点是它的父亲节点的左孩子
						if (cur = parent->_left)
						{
							parent->_left = cur->_left;
							delete cur;
						}
						//被删除节点是它的父亲节点的右孩子
						else if (cur = parent->_right)
						{
							parent->_right = cur->_left;
							delete cur;
						}

					}
				}
				//情况c,被删除的目标节点cur只有右孩子
				else if (cur->_left == nullptr)
				{
					//对应文中例举的特殊示例3的代码,删除的目标节点cur是整颗BSTree的根节点,则直接更换新的根节点,然后将旧的根节点删除即可。
					if (cur == _root)
					{
						_root = cur->_right;
						delete cur;
						cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
					}
					//对应文中普通示例1和2的代码,删除的目标节点不是BSTree的根节点
					else
					{
						//被删除节点是它的父亲节点的左孩子
						if (cur = parent->_left)
						{
							parent->_left = cur->_right;
							delete cur;
						}
						//被删除节点是它的父亲节点的右孩子
						else if (cur = parent->_right)
						{
							parent->_right = cur->_right;
							delete cur;
						}
					}
				}
				//情况d,被删除的目标节点既有左孩子,又有右孩子。文中也说过,应对情况d的解决方案有两种,咱们按第一种走:删除目标节点A时,找被删除节点A的右子树中关键码最小的节点B,
				else if (cur->_left != nullptr && cur->_right != nullptr)
				{
					Node* Rmin = cur->_right;
					Node* RminParent = cur;
					
					//先确定Rmin和RminParent的位置
					while (Rmin->_left!= nullptr)
					{
						RminParent = Rmin;
						Rmin = Rmin->_left;
					}
					//然后将需要被删除节点的关键码变成Rmin的关键码
					cur->_key = Rmin->_key;

					//最后删除Rmin节点即可删除【需要被删除目标的节点】,该怎么删除文中已经说过,不再赘述。
					if (Rmin == RminParent->_left)
						RminParent->_left = Rmin->_right;
					else if (Rmin == RminParent->_right)
						RminParent->_right = Rmin->_right;

					delete Rmin;
				}

				return true;
			}
		}
		//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
		return false;
	}


private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

开始测试,如下图所示。可以发现符合预期,有几次中序遍历的结果相同是因为要删除的元素在BSTree中不存在导致删除失败;有几次中序遍历的结果是空,这是因为数组a中有几个重复的元素,而BSTree天生支持去重,导致BSTree中的元素个数和数组a中的元素个数不同,但咱们【删除元素后中序遍历打印的次数】和【删除的元素是什么】的依据都是数组a,此时就造成了BSTree中的元素已经全部删除完毕了,BSTree是个空树,当然中序遍历时什么都打印不出来,只不过【删除后中序遍历打印】的次数没有到达循环终止条件。

测试代码如下。

#include"BSTree.h"

void test1()
{
	int a[] = { 3,2,1,5,5,5,3,2,1,4,6,7,9,8,10,10};
	BSTree<int>b;
	cout << "开始插入——————"<<endl;
	for(int&e:a)
	{
		b.Insert(e);
	}
	b.Inorder();

	cout << "开始删除———————" << endl;
	for (int& e : a)
	{
		b.Erase(e);
		cout << "删除" << e << "以后,搜索树的中序遍历为:";
		b.Inorder();
	}
}

void main()
{
	test1();
}

成员函数Find(递归版本)

因为Find的递归版本的参数和非递归版本的参数一致,构成不了函数重载,所以我们下面干脆把递归版本的Find函数叫FindR,R表示recurrence(递归)的意思。

FindR在一个搜索树中查找一个指定值时,肯定要确定在哪颗搜索树,所以肯定要访问private的_root,和前面的Inorder函数同理,为了完全不暴露private的_root,我们最好不要通过【编写一个在public中的GetRoot函数,让GetRoot去return _root】这样的方法在类外调用FindR时能够拿到私有的_root,而是最好像下面这样写。

代码如下。这样在类外调用FindR函数时,只需传需要查找元素的关键码即可,不必关心_root的问题。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	bool FindR(const K& key)
	{
		return _FindR(key, _root);
	}
private:
	bool _FindR(const K& key, Node* root)
	{
		if (root == nullptr)
			return false;
		if (key > root->_key)
			return _FindR(key, root->_right);
		else if (key < root->_key)
			return _FindR(key, root->_left);
		else
			return true;
	}
private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

成员函数Insert(递归版本)

因为递归版本的Insert参数和非递归版本的Insert参数一致,构成不了函数重载,所以我们下面干脆把递归版本的Insert函数叫InsertR,R表示recurrence(递归)的意思。

因为递归版本的InsertR也需要访问_root,为了在类外完全不暴露_root,写法如下所示。

写法一:因为我们找到插入位置,new出需要插入的节点后,还需要让该节点的父亲节点指向新插入的节点,所以给了一个rootParent表示root节点的父亲节点,root节点表示在找到目标位置时,目前遍历到了哪个节点。最开始root遍历到整棵树的根节点时,此时root没有父亲节点,所以一开始给rootParent传nullptr。

代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
    bool InsertR(const K&key)
	{
		return _InsertR(key, _root,nullptr);
	}
private:
	bool _InsertR(const K& key, Node* root,Node*rootParent)
	{
		//如果BSTree为空树,则直接new一个节点,然后更新_root节点即可。
		if (root == _root && root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		//如果root遍历到空位置,说明我就要在这里完成新节点的插入,new创建节点后,判断新节点是它父亲的左孩子还是右孩子,然后完成连接即可。
		else if (root != _root && root == nullptr)
		{
			root = new Node(key);
			if (key > rootParent->_key)
			{
				rootParent->_right = root;
				return true;
			}
			else if (key < rootParent->_key)
			{
				rootParent->_left = root;
				return true;
			}
		}
		//如果root在遍历的过程中,发现BSTree中有节点的key和我需要插入节点的key相同,说明无法插入,需要被插入的节点重复了
		else if (key == root->_key)  
			return false;  
		
		
		//如果需要插入节点的关键码比当前遍历到的节点的关键码大,则继续向右边遍历
		if (key > root->_key) 
			return _InsertR(key, root->_right, root); 
		//如果需要插入节点的关键码比当前遍历到的节点的关键码小,则继续向左边遍历
		else if(key<root->_key) 
			return _InsertR(key, root->_left, root); 
	}
private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

写法二:还有一种比较巧妙的方法,不需要形参rootParent,而是把参数root的类型从Node*改成Node* &,这样的话,插入新节点后就无需考虑新节点的父亲节点连接新节点的问题。

先看代码,然后说明为什么插入新节点后无需考虑新节点的父亲节点连接新节点的问题

代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	bool InsertR(const K&key)
	{
		return _InsertR(key, _root);
	}
private:
	bool _InsertR(const K& key, Node* &root)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		else if (key == root->_key)
			return false;


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

private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

插入新节点后,为什么无需考虑新节点的父亲节点连接新节点的问题呢?看下面递归展开图,在root遍历的途中,这个引用没有起到作用,但在找到空位置完成插入新节点时,引用就起作用了,因为root是引用,所以当前函数中的root等价于上一层递归函数中的root-> left,所以当root的值变化后,root-> left也会跟着变,所以不必担心root的父亲节点和root的连接问题。  

说一下,当前层函数的root不管引用的是上一层的root->_right还是root->_left,当前层函数的root引用的都不是一个临时变量,也不是一个右值,不能认为root引用的是上一层递归函数的root->_right或者root->_left这两个变量的值,而要清楚root引用的是root->_right或者root->_left这个变量本身(左值)。如何证明root引用的是变量本身(左值),而不是变量的右值呢?因为因为左值引用(&)只能引用左值,不能引用右值,注意临时变量虽然具有常性,但因为能取它的地址,所以它也是左值。本层root引用上一层root->_right的具体过程:因为root->_right等价于 *(root)._right,所以->本质也是解引用,所以root->_right这个表达式实际上会先通过root这个地址解引用得到一个在堆上开辟空间的匿名Node对象、然后通过该对象拿到对象的指针成员_right,成员_right是一个变量,而不仅仅表示一个右值,所以它(root->_right)既可以表示一个new出来的成员变量_right本身(左值),又可以表示一个右值,但因为左值引用(&)只能引用左值,所以当前层函数的root引用的是上一层的root指针指向的节点的成员指针变量本身,而不是引用一个右值。

并且当整颗树为空时,代码这样写也没问题,看递归展开图,如下图所示,此时root是_root的别名,当root=new Node(8)后,也就是root修改后,_root会跟着变。

注意,写法1和写法2因为形参的个数不同,所以还构成了函数重载。

成员函数Erase(递归版本)

因为递归版本的Erase参数和非递归版本的Erase参数一致,构成不了函数重载,所以我们下面干脆把递归版本的Erase函数叫EraseR,R表示recurrence(递归)的意思。

因为递归版本的EraseR也需要访问_root,为了在类外完全不暴露_root,写法如下所示。

写法1:因为需要被删除的目标节点root被删除时,root可能会有孩子节点,所以需要root的父亲节点rootParent接管root的孩子节点,又因为我们模拟实现BSTree时,没有将BSTree设计成三叉链,只通过root找不到root的父亲节点,所以给_EraseR函数增加了参数Node*rootParent表示root的父亲节点。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	bool EraseR(const K& key)
	{
		return _EraseR(key, _root,nullptr);
	}

private:
	//非引用版本的_EraseR,需要加参数rootParent
	bool _EraseR(const K& key, Node* root, Node* rootParent)
	{
		//如果root在BSTree中遍历时,root等于了nullptr,则表示BSTree中没有需要被删除的目标节点,此时直接return false表示删除失败
		if (root == nullptr)
			return false;
		
		if (key > root->_key)
			return _EraseR(key, root->_right, root);
		else if (key < root->_key)
			return _EraseR(key, root->_left, root);
		//找到了目标节点root,开始删除它。文中说过,目标节点在被删除时分3种情况,所以咱们也有3种应对方案
		else
		{
			//情况b,目标节点只有左孩子,文中说过:情况a也可视为情况b或者情况c,因为情况b的代码在c上面,所以情况a直接走这个if分支
			if (root->_right == nullptr)
			{
				//如果目标节点是根节点,直接更换根节点,然后删除目标节点
				if (root == _root)
				{
					_root = root->_left;
					delete root;
				}
				else
				{
					//如果需要被删除的目标节点是它父亲的右孩子,则让父亲节点的_right指针成员接管目标节点的左子树
					if (root->_key > rootParent->_key)
						rootParent->_right = root->_left;

					//如果需要被删除的目标节点是它父亲的左孩子,则让父亲节点的_left指针成员接管目标节点的左子树
					if (root->_key < rootParent->_key)
						rootParent->_left = root->_left;
					//最后释放目标节点的空间
					delete root;
				}
			}
			//情况c,目标节点只有右孩子
			else if (root->_left == nullptr)
			{
				//如果目标节点是根节点,直接更换根节点,然后删除目标节点
				if (root == _root)
				{
					_root = root->_right;
					delete root;
				}
				else
				{
					//如果需要被删除的目标节点是它父亲的右孩子,则让父亲节点的_right指针成员接管目标节点的右子树
					if (root->_key > rootParent->_key)
					{
						rootParent->_right = root->_right;
					}
					//如果需要被删除的目标节点是它父亲的左孩子,则让父亲节点的_left指针成员接管目标节点的右子树
					else
					{
						rootParent->_left = root->_right;
					}
					//最后释放目标节点的空间
					delete root;
				}
			}
			//情况d,目标节点左右孩子都存在,咱们使用文中的第一种方案,找目标节点的右子树中最小的节点
			else
			{
				Node* Rmin = root->_right;
				Node* RminParent = root;
				//找Rmin节点和RminParent节点的位置,Rmin表示right min(目标节点的右子树中最小的节点)
				while (Rmin->_left != nullptr)
				{
					RminParent = Rmin;
					Rmin = Rmin->_left;
				}
				//将目标节点root的关键码替换成Rmin节点的关键码
				root->_key = Rmin->_key;

				//最后删除Rmin节点即可在情况d下删除目标节点root,如何删除Rmin节点在文中已经说明过了

				//如果Rmin是它父亲节点的右孩子,则让父亲节点的_rigjt接管Rmin的右子树(右子树可能为空,也可能不为空)
				if (Rmin== RminParent->_right)
					RminParent->_right = Rmin->_right;
				//如果Rmin是它父亲节点的左孩子,则让父亲节点的_left接管Rmin的右子树(右子树可能为空,也可能不为空)
				else
					RminParent->_left = Rmin->_right;

				delete Rmin;
			}

			//不管是哪种情况,删除完目标节点后,return true表示删除成功
			return true;
		}
	}
private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

写法2:因为需要被删除的目标节点root被删除时,root可能会有孩子节点,所以需要root的父亲节点接管root的孩子节点,又因为我们模拟实现BSTree时,没有将BSTree设计成三叉链,只通过root找不到root的父亲节点,所以在方法1中,我们给了_EraseR函数增加了形参rootParent表示root的父亲节点。现在我们换一种巧妙的写法,不需要增加该形参,只需把形参Node*root改成Node* &root,这样我们也能找到root的父亲节点。

先看代码,然后画一个递归展开图进行讲解:为什么只需把形参Node*root改成Node* &root就能找到root的父亲节点。

代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	bool EraseR(const K& key)
	{
		return _EraseR(key, _root);
	}

private:
	//引用版本的_EraseR,不需要加参数rootParent
	bool _EraseR(const K& key, Node*& root)
	{
		//如果root在遍历整颗BSTree的过程中,root等于nullptr,则证明需要被删除的目标节点在BSTree中不存在,此时直接return false表示删除失败即可
		if (root == nullptr)
			return false;

		//先找到需要删除的目标节点
		if (key > root->_key)
			return _EraseR(key, root->_right);
		else if (key < root->_key)
			return _EraseR(key, root->_left);
		//找到了,开始删除
		else
		{
			//情况b,目标节点只有左孩子,文中说过:情况a也可视为情况b或者情况c,因为情况b的代码在c上面,所以情况a直接走这个if分支
			if (root->_right == nullptr)
			{
				Node* temp = root;
				root = root->_left;
				delete temp;
			}
			//情况c,目标节点只有右孩子
			else if (root->_left == nullptr)
			{
				Node* temp = root;
				root = root->_right;
				delete temp;
			}
			//情况d,目标节点左右孩子都存在,咱们使用文中的第一种方案,找目标节点的右子树中最小的节点
			else
			{
				Node* Rmin=root->_right;
				Node* RminParent = root;
				//找Rmin节点的位置,Rmin表示right min(目标节点的右子树中最小的节点)
				while (Rmin->_left != nullptr)
				{
					RminParent = Rmin;
					Rmin = Rmin->_left;
				}
				//将需要删除的目标节点的关键码替换成Rmin节点的关键码
				root->_key = Rmin->_key;
				//最后删除Rmin节点即可删除【需要被删除的目标节点】
				if (Rmin == RminParent->_left)
					RminParent->_left = Rmin->_right;
				else if (Rmin == RminParent->_right)
					RminParent->_right = Rmin->_right;

				delete Rmin;
			}
			//不管是哪种情况,删除完目标节点后,return true表示删除成功
			return true;
		}
	}

private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

说一下,当前层函数的root不管引用的是上一层的root->_right还是root->_left,当前层函数的root引用的都不是一个临时变量,也不是一个右值,不能认为root引用的是上一层递归函数的root->_right或者root->_left这两个变量的值,而要清楚root引用的是root->_right或者root->_left这个变量本身(左值)。如何证明root引用的是变量本身(左值),而不是变量的右值呢?因为因为左值引用(&)只能引用左值,不能引用右值,注意临时变量虽然具有常性,但因为能取它的地址,所以它也是左值。本层root引用上一层root->_right的具体过程:因为root->_right等价于 *(root)._right,所以->本质也是解引用,所以root->_right这个表达式实际上会先通过root这个地址解引用得到一个在堆上开辟空间的匿名Node对象、然后通过该对象拿到对象的指针成员_right,成员_right是一个变量,而不仅仅表示一个右值,所以它(root->_right)既可以表示一个new出来的成员变量_right本身(左值),又可以表示一个右值,但因为左值引用(&)只能引用左值,所以当前层函数的root引用的是上一层的root指针指向的节点的成员指针变量本身,而不是引用一个右值。

递归展开图如下所示。

示例1

示例2

注意,写法1和写法2因为形参的个数不同,所以还构成了函数重载。

Insert、Erase、Find这三个成员函数都有非递归和递归的版本,哪个版本更好呢?

编写代码时,如果某种应用情景在思想上极度偏向于递归,那比较推荐递归;

如果并没有在思想上偏向于递归,递归也行,迭代(即循环)也行,那最好还是写成迭代版本,因为递归次数如果比较多,很容易爆栈(栈溢出),而且如果某种应用情景在思想上并没有很偏向于递归,那递归编写的难度也会高于迭代。

析构函数

BSTree销毁时,需要将new出的Node节点的空间全部释放,对于树形结构来说,遍历整棵树时使用递归的思路更具优势,但问题在于:虽然析构函数能通过this指针显示递归调用自己,如下图所示,但有一个致命问题,析构函数除了隐藏的this指针这个形参外,不能再有其他形参,导致无法给下一层递归函数传参,也就没法控制遍历整棵BSTree。

那该怎么办呢?答案:既然不能让析构函数去递归,那我就让析构调用Destroy,然后让Destroy去递归。

代码如下。

可以看到Destory是走了后序遍历,这里必须是后序遍历,因为删除当前节点前,要考虑当前节点可能还存在左子树或者右子树,如果先delete当前节点,那么当前节点的指针成员_right和_left就会变成一个随机值,就找不到当前节点的左子树或者右子树了,所以当前节点必须是在左右子树删除完毕后才能删除的。

这里说一下,有人可能会对上一段的措辞产生误解,上一段中说:“在先delete当前节点再delete左右子树的情景下,当前节点被删除后,当前节点的指针成员_right和_left就会变成一个随机值。”这是不是说明Node节点对象被delete后,当对象本身都不存在时,对象的成员_right和_left还依然存在?答案:并不是,对象本身等价于它所有成员的集合,或者说对象本身就是【它所有成员的集合】的别名,所以当Node对象被delete删除后,_right和_left这两个指针成员变量肯定也被删除了,所占的空间也被释放了,但通过delete root删除了Node对象后,root这个指针变量记录的地址依然是Node对象的首地址,因为此时root是一个悬空指针,所以我们不能再解引用root,否则程序崩溃。但即使我们不能解引用root,也不能否认在Node节点没被删除时,它的数据就存储在内存地址为root的这块内存上,这块内存的地址区间为【Node节点的首地址(即root),首地址+sizeof(Node)个字节】。既然在Node节点没被删除时,它的数据就存储在这块内存区间上,而因为Node节点的_right成员变量是Node节点的数据的一部分,所以在Node节点没被删除时,Node节点的_right成员变量的值时当然也在这块内存地址区间内的内存上,只不过通过delete root删除了Node对象后,Node对象的_right成员变量曾经所占用的内存上的值就变成了一个随机值。所以说,上一段中说:“当前节点的指针成员_right和_left就会变成一个随机值。”只是想说明:Node节点没被删除时,它的指针成员肯定占据了某块内存,当Node节点被删除后,Node节点的成员所占据的内存就不属于它们了,这块内存上的数据就会变成一个随机值。而并不是想说:当对象本身都不存在时,对象的成员_right和_left还依然存在。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	~BSTree()
	{
		Destory(_root);
	}
private:
	void Destory(Node*root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
	} 


private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

拷贝构造

BSTree的拷贝构造也需要咱们手动编写,因为默认生成的拷贝构造只会将下面代码中的_root拷贝给另一个BSTree对象,这会导致两个BSTree对象管理同一堆节点,这就会产生问题,什么问题呢?两个BSTree对象在销毁前都会先调用析构函数delete释放自己管理的所有节点,然后再销毁自己(即BSTree对象),先调用析构的BSTree对象在析构结束后,所管理的所有的节点的空间都会被释放,这些空间此时就会归还给系统,这就导致后析构的BSTree对象在调用析构时,会因为要调Destroy函数而去解引用悬空指针、会因为要释放空间而去delete悬空指针、这些操作都会导致程序崩溃,因为悬空指针指向的空间已经不属于用户了。

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:

private:
	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

怎么编写呢?

BSTree对象A拷贝对象B时,需要将B的Node节点全部深拷贝一份(深拷贝即A先new出Node节点的空间,再拷贝B的Node节点的值),对于树形结构来说,如果想要A能深拷贝出B的所有节点,则必须遍历B的整棵树,遍历树时使用递归的思路更具优势,但问题在于:所有的构造函数(包括拷贝构造)都与其他成员函数不同,无法通过类对象或者this指针显示调用它们,所以也就没法显示递归调用拷贝构造,也就没法控制遍历整棵BSTree。

为什么所有的构造函数都无法被显示调用呢?举个例子,如下图所示,在f函数中不能通过this->A(1)调用构造函数,这是因为A(1)这种调用构造函数的格式和创建一个匿名对象的格式一样,编译器如果看到A(1),它会认为你是在创建匿名的A类对象,而不是在调用构造函数。

那该怎么办呢?答案:既然不能让拷贝函数去递归,那我就让拷贝构造调用Copy函数,然后让Copy去递归,框架如下代码所示。这里说一下,在类模板A中,类模板A的模板参数列表是可以省略的,但注意在类模板A中,类模板B的模板参数列表就不能省略了,所以下图编写BSTree的拷贝构造时,模板参数列表<K>可以省略也可以不省略。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	BSTree<K>(const BSTree<K>& bs)
	{
		Copy(bs._root);
	}
private:
	void Copy(Node* root)
	{
        //未开始
	}

	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

因为前面编写了Insert函数,所以编写BSTree的拷贝构造时我们就可以复用它。现在BSTree对象A拷贝对象B时的思路就是:遍历B对象,每遍历到B对象的一个关键码,就直接让A对象调用Insert函数插入新节点。但有一点需要注意,那就是遍历B对象时,必须是前序遍历,因为对于二叉搜索树来说,同样的值,如果插入的顺序不一样,会导致树的结构也不一样,如下图演示。

Copy函数的第一种写法:即让Copy复用Insert函数。所以BSTree拷贝构造的第一种写法如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
    bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		if (key > parent->_key)
			parent->_right = new Node(key);
		else
			parent->_left = new Node(key);

		return true;
	}   

	BSTree<K>(const BSTree<K>& bs)
	{
		Copy(bs._root);//注意使用不同写法的Copy时,本行代码也有所不同,写法1没有返回值,不需要参数接收
	}
private:
    //写法1
	void Copy(Node* root)
	{
		if (root == nullptr)
			return;
		Insert(root->_key); 

		Copy(root->_left);
		Copy(root->_right);
	}

	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

Copy函数还有第二种写法,即可以选择不复用Insert函数。所以拷贝构造的第二种写法如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
	BSTree<K>(const BSTree<K>& bs)
	{
		_root=Copy(bs._root);//注意使用不同写法的Copy时,本行代码也有所不同,写法2有返回值,需要参数_root接收
	}
private:
    //写法2
	void Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* cur = new Node(root->_key);
		cur->_left = Copy(root->_left);
		cur->_right = Copy(root->_right);
		return cur;
	}

	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

注意编写完拷贝构造后,因为只要编写了任意一个构造函数,编译器都不会自动生成默认构造函数了,所以此时在类外像:BSTree<int>b;这样定义对象就会报错,因为没有默认构造函数可以调用了,所以此时必须手动编写默认构造。这里说一下,学了C++11后,我们还有另一种解决方法,即使我编写了其他的构造函数,我也可以强制让编译器自动生成默认构造,格式:BSTree()=default;

赋值运算符重载operator=

传统写法:因为编写上面接口时,Destory和Copy函数已经实现了,所以A=B时,咱们可以直接先调用Destory函数销毁A对象管理的所有节点,然后调用Copy函数让A对象深拷贝B对象管理的所有节点,这样就完成了A=B。

代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:
    //赋值运算符函数的传统写法
	BSTree<K>& operator=(const BSTree<K>& bs)
	{
		Destory(_root);
		_root = Copy(bs._root);//注意使用不同写法的Copy时,本行代码也有所不同。写法1有返回值,需要参数_root接收;如果用写法2,因为Copy函数没有返回值,所以无需参数_root接收
		return *this;
	}
private:
	//写法1
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* cur = new Node(root->_key);
		cur->_left = Copy(root->_left);
		cur->_right = Copy(root->_right);
		return cur;
	}
	//写法2
	/*void Copy(Node* root)
	{
		if (root == nullptr)
			return;
		Insert(root->_key); 

		Copy(root->_left);
		Copy(root->_right);
	}*/

    void Destory(Node*root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
	} 


	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

现代写法:假设A=B,在实现了拷贝构造后,我们operator=现代写法的思想就是创建一个临时对象temp,通过B对象拷贝构造temp对象,然后让A对象和temp对象交换_root成员的值,这样一来A对象就完成了对B对象的深拷贝,不仅如此,因为temp对象是operator=函数内创建的临时变量,所以temp对象出了函数作用域后会自动销毁,temp对象在销毁前会调用析构函数把【A和temp交换前A所管理的所有老节点】给delete释放掉,所以整体来说,这样写既能完成A深拷贝B,还能释放A的垃圾资源。

代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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

template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;

public:
	赋值运算符函数的现代写法1
	//BSTree<K>& operator=(const BSTree<K>& bs)
	//{
	//	BSTree<K>temp(bs);
	//	swap(_root, temp._root);
	//	return *this;
	//}
	
	//更精简的赋值运算符函数的现代写法2
	BSTree<K>& operator=(BSTree<K> bs)//注意这里不能加const,否则swap就无法修改bs的成员
	{
		swap(_root, bs._root);
		return *this;
	}

private:

	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

BSTree的整体代码(可复制)

文件BSTree.h如下。

#pragma once
#include<iostream>
using namespace std;

template<class K>
class BSTreeNode//BinarySearchTreeNode的缩写
{
public:
	BSTreeNode(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}

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


template<class K>
class BSTree//BinarySearchTree的缩写
{
	typedef BSTreeNode<K> Node;
public:

	BSTree<K>()
		:_root(nullptr)
	{}

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;
		Node* parent=nullptr;
		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		if (key > parent->_key)
			parent->_right = new Node(key);
		else
			parent->_left = new Node(key);

		return true;

	}

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (key > cur->_key)
				cur = cur->_right;
			else if (key < cur->_key)
				cur = cur->_left;
			else
				return true;
		}
		//走到这里cur==nullptr,说明已经遍历到空位置了还没有找到目标节点,说明目标节点不存在,返回false即可
		return false;
	}


	bool Erase(const K& key)
	{
		//删除key之前,得先找到关键码为key的节点,和该节点的父亲节点,开始寻找
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if(key==cur->_key)//找到了需要删除的目标节点cur与cur的父亲节点parent,开始删除,分情况a、b、b,有对应的解决方案。
			{
				//情况b,被删除的目标节点cur只有左孩子,情况a可以看作情况b,所以情况a也会走这个if分支,不会走情况c的if分支,因为情况b的代码在情况c前面
				if (cur->_right==nullptr)
				{
					//对应文中例举的特殊示例3的情景,当删除的目标节点cur是整颗BSTree的根节点时,直接更换新的根节点,然后将旧的根节点删除即可。
					if (cur == _root)
					{
						_root = cur->_left;
						delete cur;
						cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
					}
					//对应文中普通示例1和2的情景,删除的目标节点不是BSTree的根节点
					else
					{
						//被删除节点是它的父亲节点的左孩子
						if (cur = parent->_left)
						{
							parent->_left = cur->_left;
							delete cur;
						}
						//被删除节点是它的父亲节点的右孩子
						else if (cur = parent->_right)
						{
							parent->_right = cur->_left;
							delete cur;
						}

					}
				}
				//情况c,被删除的目标节点cur只有右孩子
				else if (cur->_left == nullptr)
				{
					//对应文中例举的特殊示例3的情景,当删除的目标节点cur是整颗BSTree的根节点时,则直接更换新的根节点,然后将旧的根节点删除即可。
					if (cur == _root)
					{
						_root = cur->_right;
						delete cur;
						cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
					}
					//对应文中普通示例1和2的情景,删除的目标节点不是BSTree的根节点
					else
					{
						//被删除节点是它的父亲节点的左孩子
						if (cur = parent->_left)
						{
							parent->_left = cur->_right;
							delete cur;
						}
						//被删除节点是它的父亲节点的右孩子
						else if (cur = parent->_right)
						{
							parent->_right = cur->_right;
							delete cur;
						}
					}
				}
				//情况d,被删除的目标节点既有左孩子,又有右孩子。文中也说过,应对情况d的解决方案有两种,咱们按第一种走:删除目标节点A时,找被删除节点A的右子树中关键码最小的节点B,
				else if (cur->_left != nullptr && cur->_right != nullptr)
				{
					Node* Rmin = cur->_right;
					Node* RminParent = cur;
					
					//先确定Rmin和RminParent的位置
					while (Rmin->_left!= nullptr)
					{
						RminParent = Rmin;
						Rmin = Rmin->_left;
					}
					//然后将需要被删除节点的关键码变成Rmin的关键码
					cur->_key = Rmin->_key;

					//最后删除Rmin节点即可删除【需要被删除目标的节点】,该怎么删除文中已经说过,不再赘述。
					if (Rmin == RminParent->_left)
						RminParent->_left = Rmin->_right;
					else if (Rmin == RminParent->_right)
						RminParent->_right = Rmin->_right;

					delete Rmin;
				}

				return true;
			}
		}
		//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
		return false;
	}


public:
	bool EraseR(const K& key)
	{
		return _EraseR(key, _root,nullptr);
	}

private:
	//非引用版本的_EraseR,需要加参数rootParent
	bool _EraseR(const K& key, Node* root, Node* rootParent)
	{
		//如果root在BSTree中遍历时,root等于了nullptr,则表示BSTree中没有需要被删除的目标节点,此时直接return false表示删除失败
		if (root == nullptr)
			return false;
		
		if (key > root->_key)
			return _EraseR(key, root->_right, root);
		else if (key < root->_key)
			return _EraseR(key, root->_left, root);
		//找到了目标节点root,开始删除它。文中说过,目标节点在被删除时分3种情况,所以咱们也有3种应对方案
		else
		{
			//情况b,目标节点只有左孩子,文中说过:情况a也可视为情况b或者情况c,因为情况b的代码在c上面,所以情况a直接走这个if分支
			if (root->_right == nullptr)
			{
				//如果目标节点是根节点,直接更换根节点,然后删除目标节点
				if (root == _root)
				{
					_root = root->_left;
					delete root;
				}
				else
				{
					//如果需要被删除的目标节点是它父亲的右孩子,则让父亲节点的_right指针成员接管目标节点的左子树
					if (root->_key > rootParent->_key)
						rootParent->_right = root->_left;

					//如果需要被删除的目标节点是它父亲的左孩子,则让父亲节点的_left指针成员接管目标节点的左子树
					if (root->_key < rootParent->_key)
						rootParent->_left = root->_left;
					//最后释放目标节点的空间
					delete root;
				}
			}
			//情况c,目标节点只有右孩子
			else if (root->_left == nullptr)
			{
				//如果目标节点是根节点,直接更换根节点,然后删除目标节点
				if (root == _root)
				{
					_root = root->_right;
					delete root;
				}
				else
				{
					//如果需要被删除的目标节点是它父亲的右孩子,则让父亲节点的_right指针成员接管目标节点的右子树
					if (root->_key > rootParent->_key)
					{
						rootParent->_right = root->_right;
					}
					//如果需要被删除的目标节点是它父亲的左孩子,则让父亲节点的_left指针成员接管目标节点的右子树
					else
					{
						rootParent->_left = root->_right;
					}
					//最后释放目标节点的空间
					delete root;
				}
			}
			//情况d,目标节点左右孩子都存在,咱们使用文中的第一种方案,找目标节点的右子树中最小的节点
			else
			{
				Node* Rmin = root->_right;
				Node* RminParent = root;
				//找Rmin节点和RminParent节点的位置,Rmin表示right min(目标节点的右子树中最小的节点)
				while (Rmin->_left != nullptr)
				{
					RminParent = Rmin;
					Rmin = Rmin->_left;
				}
				//将目标节点root的关键码替换成Rmin节点的关键码
				root->_key = Rmin->_key;

				//最后删除Rmin节点即可在情况d下删除目标节点root,如何删除Rmin节点在文中已经说明过了

				//如果Rmin是它父亲节点的右孩子,则让父亲节点的_rigjt接管Rmin的右子树(右子树可能为空,也可能不为空)
				if (Rmin== RminParent->_right)
					RminParent->_right = Rmin->_right;
				//如果Rmin是它父亲节点的左孩子,则让父亲节点的_left接管Rmin的右子树(右子树可能为空,也可能不为空)
				else
					RminParent->_left = Rmin->_right;

				delete Rmin;
			}

			//不管是哪种情况,删除完目标节点后,return true表示删除成功
			return true;
		}
	}


	//引用版本的_EraseR,不需要加参数rootParent
	//bool _EraseR(const K& key, Node*& root)
	//{
	//	//如果root在遍历整颗BSTree的过程中,root等于nullptr,则证明需要被删除的目标节点在BSTree中不存在,此时直接return false表示删除失败即可
	//	if (root == nullptr)
	//		return false;

	//	//先找到需要删除的目标节点
	//	if (key > root->_key)
	//		return _EraseR(key, root->_right);
	//	else if (key < root->_key)
	//		return _EraseR(key, root->_left);
	//	//找到了,开始删除
	//	else
	//	{
	//		//情况b,目标节点只有左孩子,文中说过:情况a也可视为情况b或者情况c,因为情况b的代码在c上面,所以情况a直接走这个if分支
	//		if (root->_right == nullptr)
	//		{
	//			Node* temp = root;
	//			root = root->_left;
	//			delete temp;
	//		}
	//		//情况c,目标节点只有右孩子
	//		else if (root->_left == nullptr)
	//		{
	//			Node* temp = root;
	//			root = root->_right;
	//			delete temp;
	//		}
	//		//情况d,目标节点左右孩子都存在,咱们使用文中的第一种方案,找目标节点的右子树中最小的节点
	//		else
	//		{
	//			Node* Rmin=root->_right;
	//			Node* RminParent = root;
	//			//找Rmin节点的位置,Rmin表示right min(目标节点的右子树中最小的节点)
	//			while (Rmin->_left != nullptr)
	//			{
	//				RminParent = Rmin;
	//				Rmin = Rmin->_left;
	//			}
	//			//将需要删除的目标节点的关键码替换成Rmin节点的关键码
	//			root->_key = Rmin->_key;
	//			//最后删除Rmin节点即可删除【需要被删除的目标节点】
	//			if (Rmin == RminParent->_left)
	//				RminParent->_left = Rmin->_right;
	//			else if (Rmin == RminParent->_right)
	//				RminParent->_right = Rmin->_right;

	//			delete Rmin;
	//		}
	//		//不管是哪种情况,删除完目标节点后,return true表示删除成功
	//		return true;
	//	}
	//}

public:
	bool InsertR(const K&key)
	{
		return _InsertR(key, _root,nullptr);
	}
private:
	引用版本的_InsertR,不需要加参数rootParent
	//bool _InsertR(const K& key, Node* &root)
	//{
	//	if (root == nullptr)
	//	{
	//		root = new Node(key);
	//		return true;
	//	}
	//	else if (key == root->_key)
	//		return false;


	//	if (key > root->_key)
	//		return _InsertR(key, root->_right);
	//	else if (key < root->_key)
	//		return _InsertR(key, root->_left);
	//}

	//非引用版本的_InsertR,需要加参数rootParent
	bool _InsertR(const K& key, Node* root,Node*rootParent)
	{
		//如果BSTree为空树,直接new一个节点,然后更新_root节点即可。
		if (root == _root && root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		//如果root遍历到空位置,说明我就要在这里完成新节点的插入,new创建节点后,判断新节点是它父亲的左孩子还是右孩子,然后完成连接即可。
		else if (root != _root && root == nullptr)
		{
			root = new Node(key);
			if (key > rootParent->_key)
			{
				rootParent->_right = root;
				return true;
			}
			else if (key < rootParent->_key)
			{
				rootParent->_left = root;
				return true;
			}
		}
		//如果root在遍历的过程中,发现BSTree中有节点的key和我需要插入节点的key相同,说明无法插入,需要被插入的节点重复了
		else if (key == root->_key)  
			return false;  
		
		//如果需要插入节点的关键码比当前遍历到的节点的关键码大,则继续向右边遍历
		if (key > root->_key) 
			return _InsertR(key, root->_right, root); 
		//如果需要插入节点的关键码比当前遍历到的节点的关键码小,则继续向左边遍历
		else if(key<root->_key) 
			return _InsertR(key, root->_left, root); 
	}
public:
	bool FindR(const K& key)
	{
		return _FindR(key, _root);
	}
private:
	bool _FindR(const K& key, Node* root)
	{
		if (root == nullptr)
			return false;
		if (key > root->_key)
			return _FindR(key, root->_right);
		else if (key < root->_key)
			return _FindR(key, root->_left);
		else
			return true;
	}
public:
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
private:
	//写法1
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		if (root->_left != nullptr)
			_Inorder(root->_left);

		cout << root->_key << ' ';

		if (root->_right != nullptr)
			_Inorder(root->_right);
	}
	//写法2
	/*void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_key << ' ';
		_Inorder(root->_right);
	}*/

public:
	~BSTree<K>()
	{
		Destory(_root);
	}
private:
	void Destory(Node*root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
	} 
public:
	BSTree<K>(const BSTree<K>& bs)
	{
		_root=Copy(bs._root);//注意使用不同写法的Copy时,本行代码也有所不同。写法1有返回值,需要参数_root接收;如果用写法2,因为Copy函数没有返回值,所以无需参数_root接收
	}
private:
	//写法1
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* cur = new Node(root->_key);
		cur->_left = Copy(root->_left);
		cur->_right = Copy(root->_right);
		return cur;
	}
	//写法2
	/*void Copy(Node* root)
	{
		if (root == nullptr)
			return;
		Insert(root->_key); 

		Copy(root->_left);
		Copy(root->_right);
	}*/
public:
	赋值运算符函数的传统写法
	//BSTree<K>& operator=(const BSTree<K>& bs)
	//{
	//	Destory(_root);
	//	_root = Copy(bs._root);//注意使用不同写法的Copy时,本行代码也有所不同。写法1有返回值,需要参数_root接收;如果用写法2,因为Copy函数没有返回值,所以无需参数_root接收
	//	return *this;
	//}

	赋值运算符函数的现代写法1
	//BSTree<K>& operator=(const BSTree<K>& bs)
	//{
	//	BSTree<K>temp(bs);
	//	swap(_root, temp._root);
	//	return *this;
	//}
	
	//更精简的赋值运算符函数的现代写法2
	BSTree<K>& operator=(BSTree<K> bs)//注意这里不能加const
	{
		swap(_root, bs._root);
		return *this;
	}

private:

	Node* _root=nullptr;//给缺省值后就可以不用显示写默认构造了
};

文件test.cpp如下

#include"BSTree.h"

void test1()
{
	int a[] = { 3,2,1,5,5,5,3,2,1,4,6,7,9,8,10,10 };
	//int a[] = { 3,2,1,5,4,6,7,9,8,10};
	BSTree<int>b;
	cout << "开始插入——————"<<endl;
	for(int&e:a)
	{
		b.InsertR(e);
	}
	b.Inorder();

	cout << "开始删除———————" << endl;
	for (int& e : a)
	{
		b.EraseR(e);
		cout << "删除" << e << "以后,搜索树的中序遍历为:";
		b.Inorder();
	}
}

void test2()
{
	int a[] = { 3,2,1,5,5,5,3,2,1,4,6,7,9,8,10,10 };
	BSTree<int>b;
	for (int& e : a)
	{
		b.InsertR(e);
	}
	b.Inorder();
	cout << endl;
	cout << (b.FindR(3))<<endl;
	cout << (b.FindR(100)) << endl;
	cout << (b.FindR(10)) << endl;
	cout << (b.FindR(9))<<endl;

	
}

void test3()
{
	int a[] = { 3,2,1,5,5,5,3,2,1,4,6,7,9,8,10,10 };
	BSTree<int>x;
	for (auto e : a)
	{
		x.InsertR(e);
	}
	x.Inorder();
	BSTree<int>y;
	y = x;
	y.Inorder();
}



void main()
{
	test3();
	
}

二叉搜索树的缺陷(时间复杂度)

问题:对于一棵二叉搜索树,我们要查找某个节点的时间复杂度是多少呢?有人肯定抢答说是O(logN),毕竟在找目标节点时,每次能排除一半的错误选项。那是不是O(logN)呢?

答案:不是O(logN),只有在BSTree的结构比较理想的情况下去BSTree上找某个节点时,它的时间复杂度才会是O(logN),什么叫做比较理想的情况下呢?意思是:这棵BSTree上的节点分布比较均匀,近似一棵完全二叉树。假如情况不够理想,那时间复杂度会是什么样的呢?在最坏的情况下的时间复杂度是O(N),N表示节点总数。如下图左半部分所示就是一种最坏的情况,当插入节点的关键码有序或者接近有序时,二叉树就会退化成近似于是单枝树或者就是单枝树,而单枝树这样的结构和链表就没区别了;如下图右半部分所示,即使不是单枝树,但当BSTree的节点分布不够均匀时,即不近似于一个完全二叉树时,它的时间复杂度可能会略微优化,但本质上和单枝树还是一个量级。

接下来公布正确答案:在BSTree中查找一个节点的时间复杂度为O(h),h表示树的高度。同时,因为删除节点和插入节点首先也要查找目标节点,观察Insert和Erase的代码可以发现,在Insert或者Erase函数中,真正实现插入和删除节点逻辑的过程所用的时间复杂度相比于查找的过程所用的时间复杂度是九牛一毛,所以在BSTree中插入或者删除一个节点的时间复杂度也为O(h)。

BSTree的缺陷:在上文中讲解为什么需要存在二叉搜索树的部分也说过,BSTree这样的结构存在的意义就是查找的效率比vector或者list这样的基础数据结构要高,但现在我们知道了在极端情况下,BSTree也会退化成近似于链表的单枝树,所以BSTree的查找效率比链表高就成了一个空谈,这就是BSTree的缺陷。

为了解决BSTree的缺陷,我们就会对BSTree做一个旋转操作让它保持平衡,这也是我们以后会讲解的平衡二叉搜索树,平衡二叉搜索树有两种,一种叫AVL树,一种叫红黑树。对于平衡树来说,查找的时间复杂度就真的是O(logN)了,假如有1000个节点,则找某个节点真的只需要10次(2^10=1000),在100w个节点中找某个节点则只需20次,在10亿个节点中找某个节点则只需30次,而对于非平衡的BSTree来说,其效率就只能看运气了。平衡搜索二叉树本质还是搜索树,和BSTree唯一的区别:因为在同样数量的节点下平衡树的高度更低,所以查找效率更高,其他的性质都是一样的。

二叉搜索树的应用

K模型:

K模型表示只有key作为关键码(即目标节点的值),在搜索树的Node节点结构中只需要存储key即可。上文中模拟实现的BSTree就是一个K模型的二叉搜索树,以后咱们会学习的set则是一个K模型的平衡二叉搜索树。

K模型主要的应用场景是判断某种东西是否存在,比如门禁系统,车栏杆系统,又或者是这样的场景:输入一个单词str,判断该单词是否拼写正确。乍一看很麻烦,实际上不难,具体做法是:以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

 代码如下。

#include"BSTree.h"
#include<string>

void test5()
{
	BSTree<string>bs;
	bs.Insert("hello");
	bs.Insert("world");
	bs.Insert("apple");
	bs.Insert("orange");
	cout << "——开始检查是否拼写正确,请输入英语——" << endl;
	string str;
	while (cin >> str)
	{
		bool isTrue = bs.Find(str);
		if (isTrue == true)
			cout << "您输入的英文:" << str << "存在" << endl;
		else
			cout << "您输入的英文:" << str << "不存在" << endl;
	}
}


void main()
{
	test5();
}

有人可能会说:K模型的BSTree的确能判断某种东西存不存在、能检查单词是否拼写正确,但具备这些功能的容器又不是只有你K模型的BSTree,我vector或者list还有其他容器一样行,你搞这么多花里胡哨的吓唬谁呢?

的确,vector和list一样行,但问题在于BSTree是搜索树,它查找的效率是比vector和list都高的,这就是它的价值所在。

有一道题用K模型的BSTree解决非常容易,如下图,寻找相交链表的第一个相交节点。只需要将L1的节点的地址全部插进BSTree中,然后在BSTree中调用Find函数找L2的节点,哪个节点最先被找到,则哪个节点就是相交链表的第一个相交节点。

【K,V】模型:

每一个关键码key,都有与之对应的值value,即<key, value>的键值对。以后咱们会学习的map就是一个【K,V】模型的平衡二叉搜索树。

咱们讲解【K,V】模型的应用场景前,首先把【K,V】模型的二叉搜索树的代码给整出来,它的代码只需在K模型的BSTree上稍作修改即可:只需增加一个模板参数V,然后修改Insert、Find、_Inorder接口,大部分接口比如Erase接口都不用修改,整体代码如下。咱们把【K,V】模型的二叉搜索树写在命名空间KV里,把它和K模型的BSTree隔离开。

namespace KV
{
	template<class K,class V>
	class BSTreeNode//BinarySearchTreeNode的缩写
	{
	public:
		BSTreeNode(const K& key,const V&value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			,_value(value)
		{}

		BSTreeNode<K,V>* _left;
		BSTreeNode<K,V>* _right;
		K _key;
		V _value;
	};


	template<class K,class V>
	class BSTree//BinarySearchTree的缩写
	{
		typedef BSTreeNode<K,V> Node;
	public:

		BSTree<K,V>()
			:_root(nullptr)
		{}

		bool Insert(const K& key,const V& val)
		{
			if (_root == nullptr)
			{
				_root = new Node(key,val);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur != nullptr)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			if (key > parent->_key)
				parent->_right = new Node(key,val);
			else
				parent->_left = new Node(key,val);

			return true;

		}
		//KV模型下,我们期望如果找到了目标节点,就返回该节点的地址,如果没找到,就返回nullptr
		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur != nullptr)
			{
				if (key > cur->_key)
					cur = cur->_right;
				else if (key < cur->_key)
					cur = cur->_left;
				else
					return cur;
			}
			//走到这里cur==nullptr,说明已经遍历到空位置了还没有找到目标节点,说明目标节点不存在,返回nullptr即可
			return nullptr;
		}

		bool Erase(const K& key)
		{
			//删除key之前,得先找到关键码为key的节点,和该节点的父亲节点,开始寻找
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur != nullptr)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key == cur->_key)//找到了需要删除的目标节点cur与cur的父亲节点parent,开始删除,分情况a、b、b,有对应的解决方案。
				{
					//情况b,被删除的目标节点cur只有左孩子,情况a可以看作情况b,所以情况a也会走这个if分支,不会走情况c的if分支,因为情况b的代码在情况c前面
					if (cur->_right == nullptr)
					{
						//对应文中例举的特殊示例3的情景,当删除的目标节点cur是整颗BSTree的根节点时,直接更换新的根节点,然后将旧的根节点删除即可。
						if (cur == _root)
						{
							_root = cur->_left;
							delete cur;
							cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
						}
						//对应文中普通示例1和2的情景,删除的目标节点不是BSTree的根节点
						else
						{
							//被删除节点是它的父亲节点的左孩子
							if (cur = parent->_left)
							{
								parent->_left = cur->_left;
								delete cur;
							}
							//被删除节点是它的父亲节点的右孩子
							else if (cur = parent->_right)
							{
								parent->_right = cur->_left;
								delete cur;
							}

						}
					}
					//情况c,被删除的目标节点cur只有右孩子
					else if (cur->_left == nullptr)
					{
						//对应文中例举的特殊示例3的情景,当删除的目标节点cur是整颗BSTree的根节点时,则直接更换新的根节点,然后将旧的根节点删除即可。
						if (cur == _root)
						{
							_root = cur->_right;
							delete cur;
							cur = nullptr; //这行代码可有可无,因为cur只是Erase函数的一个局部指针变量,出了函数作用域就不存在了,只要不在Erase函数内部再次使用,就不必担心悬空指针的问题。但这里为了保持一个好习惯,我们还是写了这行代码
						}
						//对应文中普通示例1和2的情景,删除的目标节点不是BSTree的根节点
						else
						{
							//被删除节点是它的父亲节点的左孩子
							if (cur = parent->_left)
							{
								parent->_left = cur->_right;
								delete cur;
							}
							//被删除节点是它的父亲节点的右孩子
							else if (cur = parent->_right)
							{
								parent->_right = cur->_right;
								delete cur;
							}
						}
					}
					//情况d,被删除的目标节点既有左孩子,又有右孩子。文中也说过,应对情况d的解决方案有两种,咱们按第一种走:删除目标节点A时,找被删除节点A的右子树中关键码最小的节点B,
					else if (cur->_left != nullptr && cur->_right != nullptr)
					{
						Node* Rmin = cur->_right;
						Node* RminParent = cur;

						//先确定Rmin和RminParent的位置
						while (Rmin->_left != nullptr)
						{
							RminParent = Rmin;
							Rmin = Rmin->_left;
						}
						//然后将需要被删除节点的关键码变成Rmin的关键码
						cur->_key = Rmin->_key;

						//最后删除Rmin节点即可删除【需要被删除目标的节点】,该怎么删除文中已经说过,不再赘述。
						if (Rmin == RminParent->_left)
							RminParent->_left = Rmin->_right;
						else if (Rmin == RminParent->_right)
							RminParent->_right = Rmin->_right;

						delete Rmin;
					}

					return true;
				}
			}
			//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
			return false;
		}

	public:
		void Inorder()
		{
			_Inorder(_root);
			cout << endl;
		}
	private:
		//写法1
		void _Inorder(Node* root)
		{
			if (root == nullptr)
				return;

			if (root->_left != nullptr)
				_Inorder(root->_left);

			cout << "key为:"<<root->_key<<"value为:"<< root->_value <<endl;//KV模型下就把key和value都打印出来

			if (root->_right != nullptr)
				_Inorder(root->_right);
		}
	
	public:
		~BSTree()
		{
			Destory(_root);
		}
	private:
		void Destory(Node* root)
		{
			if (root == nullptr)
				return;
			Destory(root->_left);
			Destory(root->_right);
			delete root;
		}
	public:
		BSTree(const BSTree& bs)
		{
			_root = Copy(bs._root);
		}
	private:
		//写法1
		Node* Copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;
			Node* cur = new Node(root->_key,root->_val);
			cur->_left = Copy(root->_left);
			cur->_right = Copy(root->_right);
			return cur;
		}
	public:
		

		//更精简的赋值运算符函数的现代写法2
		BSTree& operator=(BSTree bs)//注意这里不能加const
		{
			swap(_root, bs._root);
			return *this;
		}

	private:

		Node* _root = nullptr;//给缺省值后就可以不用显示写默认构造了
	};


}

有了【K,V】模型的BSTree的代码后,接下来说说【K,V】模型的BSTree有哪些应用场景。

【K,V】模型的应用场景有很多。

比如火车站票检时,虽然你没有拿票,但只要刷身份证一样能检测,这种场景下身份证就作为key,火车票就作为value,它只要知道了身份证,就可以通过它找到火车票了;

又比如翻译语言,把英语翻译成中文,乍一看也很麻烦,实际上不难,具体方式如下:以词库中所有单词集合中的每个单词作为key,它的中文翻译作为value,每个节点中都要有key和value成员,以这样的节点构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则翻译,不存在则不翻译,如下图所示。

下图演示代码的黄框处显示water不是一个英文,但我们知道water是水,那是不是表示程序有问题呢?答案:程序没问题,这里显示water不是一个英文的原因是词库并没有录入water单词,即没有往二叉搜索树中插入key为water、value为水的Node节点。

代码如下。

#include"BSTree.h"
#include<string>

void test4()
{
	KV::BSTree<string, string>bs;
	bs.Insert("hello", "你好");
	bs.Insert("world", "世界");
	bs.Insert("apple", "苹果");
	bs.Insert("orange", "橘子");
	cout << "——开始翻译,请输入英语——" << endl;
	string str;
	while (cin >> str)
	{
		//对于KV模型的BSTree,它的成员函数Find,如果找到了目标节点则返回节点的地址,没找到则返回nullptr
		KV::BSTreeNode<string, string>* p = bs.Find(str);
		if (p != nullptr)
			cout << "您输入的英文为:" << str << ",翻译成中文为:" << p->_value<<endl;
		else
			cout << "您输入的字符串:" << str << "不是一个英文"<<endl;
	}
}


void main()
{
	test4();
}

上面while(cin>>str)这行代码涉及到IO流的一个知识点,cin>>str会调用operator>>函数,该函数的返回值是cin,cin是一个类对象,该对象能够隐式调用operatorbool成员函数,该函数能返回一个bool值true,所以这里循环的条件是一直是true,所以死循环。

通过【K,V】模型的BSTree还可以做到统计某种东西的次数,演示如下图。有人可能会对下图结果产生误解,不是说好的BSTree是一棵排序树,天生支持排升序吗?怎么value值从上到下是4、2、4呢?答案:你这是搞混了,BSTree是按key去排序的,value不参与比较大小,即value压根不影响排序结果。

代码如下。

#include"BSTree.h"
#include<string>

void test6()
{
	KV::BSTree<string, int>bs;
	string array[] = { "橘子","苹果","橘子","香蕉","苹果","苹果","橘子","苹果","橘子","香蕉" };
	for (string& str : array)
	{
		//Find函数在KV模型下,如果找到了则返回节点的地址,没找到则返回nullptr
		KV::BSTreeNode<string,int>* p=bs.Find(str);
		//没找到该节点就插入这个节点,并计数为1
		if (p == nullptr)
			bs.Insert(str, 1);
		//如果找到了重复节点,则让计数+1即可
		else
			(p->_value)++;
	}
	//最后中序遍历一遍KV模型的BSTree打印出统计结果
	bs.Inorder();
}

void main()
{
	test6();
}

有人可能会说:【K,V】模型的BSTree的确能翻译语言,能统计次数,但具备这些功能的容器又不是只有你【K,V】模型的BSTree,我vector或者list还有其他容器一样行,你搞这么多花里胡哨的吓唬谁呢?

的确,vector和list一样行,但问题在于BSTree是搜索树,它查找的效率是比vector和list都高的,这就是它的价值所在。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个 C++ 实现的二排序类(BSTree): ```cpp #include <iostream> using namespace std; struct Node { int data; Node* left; Node* right; }; class BSTree { public: BSTree(); // 构造函数 ~BSTree(); // 析构函数 void insert(int value); // 插入结点 Node* build(int arr[], int n); // 构造二排序 void printPath(int value); // 输出查找路径 bool isBST(); // 判断是否二排序 private: Node* root; // 根结点 void insert(Node*& p, int value); void destroy(Node* p); void printPath(Node* p, int value); bool isBST(Node* p, int minVal, int maxVal); }; BSTree::BSTree() : root(nullptr) {} BSTree::~BSTree() { destroy(root); } void BSTree::insert(int value) { insert(root, value); } Node* BSTree::build(int arr[], int n) { for (int i = 0; i < n; i++) { insert(arr[i]); } return root; } void BSTree::printPath(int value) { printPath(root, value); } bool BSTree::isBST() { return isBST(root, INT_MIN, INT_MAX); } void BSTree::insert(Node*& p, int value) { if (p == nullptr) { p = new Node; p->data = value; p->left = p->right = nullptr; } else if (value < p->data) { insert(p->left, value); } else if (value > p->data) { insert(p->right, value); } } void BSTree::destroy(Node* p) { if (p != nullptr) { destroy(p->left); destroy(p->right); delete p; } } void BSTree::printPath(Node* p, int value) { if (p == nullptr) { return; } else { cout << p->data << " "; if (value == p->data) { cout << endl; return; } else if (value < p->data) { printPath(p->left, value); } else { printPath(p->right, value); } } } bool BSTree::isBST(Node* p, int minVal, int maxVal) { if (p == nullptr) { return true; } else if (p->data < minVal || p->data > maxVal) { return false; } else { return isBST(p->left, minVal, p->data - 1) && isBST(p->right, p->data + 1, maxVal); } } ``` 其中,`Node` 结构体表示二排序的结点,包含一个整数和两个指向左右子的指针。`BSTree` 类实现了插入结点、构造二排序、输出查找路径、判断是否二排序这 4 个成员函数。具体实现如下: - `insert` 函数用于插入一个新结点,采用递归实现。 - `build` 函数用于构造二排序,接收一个整数数组和数组长度作为参数,依次将数组中的元素插入到二排序中。 - `printPath` 函数用于输出查找路径,接收一个整数作为参数,从根结点开始遍历二排序,输出查找路径上的所有结点值。 - `isBST` 函数用于判断是否二排序,采用递归实现。对于每个结点,判断其值是否在给定的最小值和最大值范围内,如果是,则递归判断左右子是否也是二排序。 需要注意的是,在 `insert` 函数和 `destroy` 函数中,左子和右子的指针都是指向指针的指针,这样才能在递归中改变它们的值。在 `printPath` 函数和 `isBST` 函数中,则直接使用指针即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值