【C++、数据结构】二叉搜索树 模拟实现

📖 前言

  • 从本章起,我们开始深入学习二叉树,学习其更高端的应用,然后将学习STL中比较重要的两个容器set和map。
  • 学习二叉搜索树也是为以后学习和实现set和map做铺垫。

1. 二叉搜索树

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

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

在这里插入图片描述

也就是说一棵二叉搜索树的任一个根节点,它的左子树所有节点的值都是小于根节点的值的,它的右子树所有结点的值都是大于根节点的值的。

二叉搜索树查找的时间复杂度:

  • 根据二叉搜索树的性质
  • 我们大多数人认为其搜索的一个值的速度是为树的高度次
  • 树的高度次的话,很多人就会认为是log2N次
  • 但是事实并不是,正确得查找时间复杂度是〇(N)

只有当是满二叉树或者是完全二叉树时间复杂度才是〇(logN)!!

解释:

当出现单边树的情况时,就是〇(N)的情况。
在这里插入图片描述

此时树的高速就是结点的个数,同时如果数据量过大,而且是递归查找的话,很有可能会有爆栈的风险!!
在以后我们会学习平衡二叉树,就是为了解决上述情况的问题。


2. 二叉搜索树的模拟实现

2.1 结点的声明

template<class K>

struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;

	K _key;

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

2.2 基本的几个成员函数

template<class K>

class BSTree
{                
	typedef BSTreeNode<K> Node;
private:
	//没有参数是不能递归的
	void DestroyTree(Node* root)
	{
		if (root == nullptr)
			return;

		DestroyTree(root->_left);
		DestroyTree(root->_right);
		delete root;
	}

	Node* CopyTree(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copyNode = new Node(root->_key);
		copyNode->_left = CopyTree(root->_left);
		copyNode->_right = CopyTree(root->_right);
		
		return copyNode;
	}
public:
	//强制编译器自己生成构造函数 -- C++11
	BSTree() = default;

	/*BSTree()
		:_root(nullptr)
	{}*/

	//前序遍历递归拷贝
	BSTree(const BSTree<K>& t)
	{
		_root = CopyTree(t._root);
	}

	//t1 = t2; -- 任何赋值重载都可以用现代写法
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

	~BSTree()
	{
		DestroyTree(_root);
		_root = nullptr;
	}

构造函数:

  • 这里我们可以采用传统的方法
  • 直接初始化成员变量
  • 也可以用C++11的语法default
  • 强制编译器自己生成构造函数

拷贝构造:

  • 这里我们用了递归的方式进行拷贝
  • 采用根 - 左 - 右 的前序遍历的递归方式对整个二叉树拷贝
  • 最后将跟结点返回

析构函数:

  • 析构函数我们这里也是采用递归的方式进行一个一个结点析构
  • 同样的我们再嵌套一个子函数
  • 也是采用类似前序遍历的方法将整个二叉树释放掉

采用递归方式的缺点就是如果数的结点个数足够多的时候,就会有爆栈的风险!!


非递归版本

(1)查找:

在二叉搜索树中找某个值:

bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}

		return false;
	}

根据二叉搜索树的性质,查找规则很简单:

  • 从根节点开始找起
  • 要找的值如果比根节点的值大,则在根节点的右子树中找
  • 要找的值如果比根节点的值小,则在根节点的左子树中找
  • 再在子树中重复上述操作,最终找到要找的值

所以再没有平衡二叉搜索树的情况下,查找的时间复杂度为〇(N)

(2)插入:
bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				parent = cur;
				cur = cur->_left;
			}
		}

		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

根据搜索二叉树的特性,插入规则如下:

  • 上述过程也是一个查找的过程
  • 根据要插入值的大小,定位其在树中合适的位置
  • 找到合适位置之后,直接插入即可
(3)删除:(重点)

二叉搜索树的删除,是一件非常麻烦的事情

  • 要删除结点,就要理清楚父子节点的链接关系(一不留神就把关系理乱了)
  • 要求删过之后的二叉树还是一棵搜索二叉树(相当困难,普通直接删除做不到)

分析问题:

  • (1)当没有孩子或者只有一个孩子时
  • 可以直接删除,孩子托管给父亲(托孤)

在这里插入图片描述
以删除14这个结点为例:

  • 该结点比10这个结点(父结点)大,在其右子树
  • 那么该右子树的所有的值都比10这个结点大
  • 所以要链接在10这个结点的右边

以删除7这个结点为例:

  • 该结点比6这个结点(父结点)大,在其右子树
  • 因为7这个结点没有孩子
  • 直接删除,将父节点(6结点)的右指向空
  • (2)当有两个孩子时
  • 没办法给父亲,父亲养不了,要找个人替代我养孩子

核心步骤:

  • 要找到 【左子树的最大值节点,或者右子树的最小值节点】
  • 找到之后,将要删除的结点和找到的结点的值进行交换(这里我们暂时用的是值交换)
  • 再将被交换过之后的值的结点删除
  • 一般被交换的结点都是末尾的叶子结点(按照上述的没有孩子的结点删除方式删除)

代码如下:

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			//找到了就分三种情况

			//该结点有一个孩子 -- 左为空 or 右为空(托孤)
			//该结点有两个孩子 -- 替换法
			
			//第一种情况:该结点有一个孩子且该结点的左为空
			if (cur->_left == nullptr)
			{
				//当删除的是根节点的时候
				//if(parent == 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 == parent)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}

				delete cur; 
			}
			//两个孩子都不为空(替换法删除)
			else 
			{
				//我们这里统一找右树最左结点(最小)
				//右子树的最小结点替代

				//minParent一开始不能给空,因为右子树的跟一开始就可能是minRight
				//Node* minParent = nullptr; -- 循环直接不能进去

				Node* minParent = cur;

				//从右子树的根开始
				Node* minRight = cur->_right;
				
				//找最左结点(最小)
				while (minRight->_left)
				{
					minParent = minRight;   
					minRight = minRight->_left;
				}

				//交换
				swap(minRight->_key, cur->_key);
				//**return Erase(key); -- 这是错的,因为这里已经不符合搜索树的规则了
				//递归过程中找不到想要想要删除的数(交换到后头的数)
				
				//直接赋值
				//cur->_key = minRight->_key;     
				                                                 
				//删除

				//找到最小结点,此结点一定是该结点父亲结点的左孩子
				//此结点一定没有左孩子(一定是左为空),有可能有右孩子,也可能没有右孩子
				//此时只需要将父亲的左指向该结点的右孩子即可
				//删除完成

				if (minParent->_left == minRight)
				{
					minParent->_left = minRight->_right;
				}
				else if (minParent->_right == minRight)
				{
					minParent->_right = minRight->_right;
				}

				delete  minRight;
			}

			return true;
		}
	}

	return false;
}

代码解释,如下图:

  • 第一种情况:该结点有一个孩子且该结点的左为空

在这里插入图片描述

  • 第二种情况:该结点有一个孩子且该结点的右为空

在这里插入图片描述

  • 第三种情况:两个孩子都不为空(替换法删除)

在这里插入图片描述
左子树的最大值节点,或者右子树的最小值节点

  • 根据二叉搜索树的特性
  • 任何一个结点的左子树所有结点的值都比根小
  • 任何一个结点的右子树所有结点的值都比根大

找要删除结点的左子树的最大值节点:

  • 那么找左子树的最右边结点
  • 那么该结点一定比根结点的右子树中所有的值都小
  • 但是该结点在根结点的左子树中是最大的
  • 让其和根结点的值交换
  • 将被交换的结点删除后,整棵树仍保持是一棵二叉搜索树

同理,找右子树的最小值节点也是一样的道理


递归版本

递归版本理解起来就相对与非递归版本更好理解了,直接看代码

(1)查找:
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)
	{
		return _FindR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _FindR(root->_left, key);
	}
	else
	{
		return true;
	}
}

逐层递归查找即可…

(2)插入:(重点)
bool InsertR(const K& key)
{
	return _InsertR(_root, key);
}

bool _InsertR(Node*& root, const K& key)
{
	//没有父指针,胜似父指针
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}

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

该如何链接上树呢?

  • 可以在递归的参数中多一个父亲结点,每次递归都更新一下Parent,然后再带到下一层递归
  • 显然这样在学过C++之后就麻烦了

用了一个指针的引用就解决了问题

  • 因为root的值此时是空,但是root同时是这个结点里的_left这个指针的别名
  • 相当于当前结点的父节点的左指针的别名
  • 意味着此时再去给root赋值就是去给该结点父亲结点的_left赋值
  • 那么此时就链接起来了
(3)删除:
bool EraseR(const K& key)
{
	return _EraseR(_root, key);
}

bool _EraseR(Node*& root, const K& key)
{
	//递归是用来找要删除的结点
	if (root == nullptr)
	{
		return false;
	}

	if (root->_key < key)
	{
		return _EraseR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _EraseR(root->_left, key);
	}
	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;
			while (minRight->_left)
			{
				minRight = minRight->_left;
			}

			swap(root->_key, minRight->_key);

			return _EraseR(root->_right, key);
			//转换成在root->_right(右子树)中去删除key
			//这里删除这个key一定会走左为空的场景(找最小)
		}

		delete del;
		return true;
	}
}

相等时就开始删除了(递归只是用来查找要删除的数的位置)

  • root是要删除结点的左结点 / 右结点的别名

分三种情况删除:

  1. 要删除的结点左为空
  2. 要删除的结点右为空
  3. 要删除的结点左右都为空(替换法)

总的来说递归版本比非递归版本更容易理解,删除过程参考非递归删除过程……(有异曲同工之妙)

  • 21
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yy_上上谦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值