【C++】二叉搜索树

前言

我们在数据结构中,学习了基本的二叉树的性质,完全二叉树的性质,树和森林的转换,还有哈夫曼树。这些都是较为基础的树,而今天我们来学习一种在存储数据上很有特点的树 — 二叉搜索树/二叉排序树
那么话不多说,马上开始今天的学习

在这里插入图片描述

一. 什么是二叉搜索树

在这里插入图片描述
我们上面说了,二叉搜索树是一种数据存储特殊的树,仔细观察

任何一个节点,他的左子树的所有节点都比他小,右子树的所有节点都比他大。
这就是二叉搜索树

而我们如果中序遍历输出这棵二叉搜索树,我们可以发现
1->3->4->6->7->8->10->13->14
刚好是升序

二. 二叉搜索树的构建

了解二叉搜索树
二叉搜索树就是一个数据存储特殊的二叉树,结构上没有特殊部分,所以其结构体和普通二叉树并没有区别

//类模板,用于存储不同数据
template<class K>
struct BinarySearchTree
{
	BinarySearchTree<K>*_left;//左子树
	BinarySearchTree<K>*_right;//右子树
	K _key;//节点值
	
	//构造函数
	BinarySearchTree(const K&key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};

template<class K>
class BSTree
{
	typedef BinarySearchTree<K> Node;
private:
	Node*_root = nullptr;//根节点
};

接下来我们来插入数据。
因为二叉搜索树的数据存储特性,所以我们当前不允许有节点值相同,同时因为其性质,新插入的节点需要先找到其应该在的位置,然后再构建节点链接,就可以了

我们拿上面的二叉搜索树为例子
构建的数组是这样一组数据{ 8, 3, 1, 10, 6, 4, 7, 14, 13 }

代码如下:

	bool Insert(const K&key)
	{
		//头为空时单独处理
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		//循环找插入的位置
		Node*cur = _root;
		//记录父节点,实现链接
		Node*parent = nullptr;
		while (cur)
		{
			parent = cur;
			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else
			{
				//相等则返回假
				return false;
			}
		}

		//找到了要插入的位置
		cur = new Node(key);
		//链接
		if (key > parent->_key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

插入节点的情况分两种

  1. 最开始,头结点为空,我们单独处理
  2. 头结点不为空,我们从头结点开始进行比较,如果当前节点值比插入值大,则往应该放到该节点的左子树,反之放在右子树,直到走到空为止。注意,因为最后还需要链接父子节点,所以我们需要存储父节点
    并且我们无法保证该节点应该放在父节点的左子树还是右子树,所以我们需要进行比较,得知应该链接在父节点的左或者右

我们再编写一个中序遍历,输出一下这棵二叉搜索树。

	//中序遍历
	//因为二叉搜索树的特点,中序打印出来就是升序

	//实现封装
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	//因为要递归,所以要单独编写
	//注意此处不可以加缺省值_root,因为缺省值需要是常量
	void _InOrder(Node*root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

因为我们编写在类内,实际使用时不会传参,但递归需要传参,所以我们进行一层封装。

在这里插入图片描述
这样就成功构建了一棵二叉搜索树。

三. 二叉搜索树的查找

查找其实较为的简单,因为二叉搜索树的特性,我们只要一直比较就好,当前节点的值比查找的值大,则往左边走,比查找的值小,则往右边走。构建的前半部分也是就是查找
代码如下:

	//查找
	bool Find(const K&key)
	{
		Node*cur = _root;
		//循环查找
		while (cur)
		{
			//比当前值小,则往左走
			if (key<cur->_key)
			{
				cur = cur->_left;
			}//大则往右走
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}//不然就是相等,相等就是找到了
			else
			{
				return true;
			}
		}
		
		//循环没返回说明没查到
		return false;
	}

四. 二叉搜索树的删除

删除节点的情况较为复杂,读者可以边看边画图理解。
在这里插入图片描述
还是这课树,我们可以先大致把删除节点分为三种情况

  1. 叶子结点,比如:1,4,7,13
  2. 度为1的节点,比如:10,14
  3. 度为2的节点,比如:6,3,8

叶子节点

叶子结点的删除很简单,只要像查找那样,循环找到节点,然后删除即可。

度为1的节点

度为1的节点,首先也是先找到节点,要删除该节点,我们就需要链接其子树,但程序还需要知道是左子树需要链接,还是右子树需要链接;也不知道是要链接在删除节点父节点的左还是右。
拿10为例子,首先我们找到10,发现其左为空,所以我们需要链接他的右,也就是14,同时我们还需要记录父亲节点,也就是8,知道10是8的右节点,所以我们需要将14链接在8的右

度为2的节点

度为2的节点删除比较难想,其实也是使用替代法,但是并不是让其左右节点来替代,而是用其左子树的最右节点/右子树的最左节点,其实也就是数值和删除节点最接近的节点,因为左子树的最右节点其实是左子树中最大的右子树的最左节点其实是右子树中最小的,所以这两个节点最接近删除节点
比如我们使用右子树的最小节点来替代,我们以删除8为例子
首先,找到节点8,然后找他的右子树的最小节点,也就是10,我们将10赋给8,也就是覆盖了原先的8,然后现在就转变成我们要删除原先的10了,又因为10是右子树的最左节点,所以其最多只会有右节点,不会有左节点,所以我们就又将问题转换成删除度为1的节点,将该节点的父和其右子树链接就行。注意:这里既可能链接在父节点的左,也可能是右,所以也需要判断。

小结&特殊情况
但是,删除叶子节点其实可以和删除度为1的节点有相同的处理,将其空节点当成子节点链接就好
还会有一个特殊情况
在这里插入图片描述
我们在查找的过程需要记录父亲节点,但是如果是这样一棵树,然后我们要删除10,那么父亲节点就是空,因为没有进入循环,那么此时的删除是会崩溃的。处理方法之一就是,换根
我们直接将根换成3,这样就既保证了二叉搜索树的结构,又成功删除了节点。

具体代码如下

bool Erase(const K&key)
	{
		//分成两类
		//左或者右为空(包括叶子结点)
		//左右孩子都有

		//首先先找节点
		Node*cur = _root;
		//记录父亲节点
		Node*parent = nullptr;

		while (cur)
		{
			//parent = cur;
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				//找到了
				//分两种情况
				//左为空
				if (cur->_left == nullptr)
				{
					//还有可能删到根节点的一边为空(有点像歪脖子树)
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						//要判断父节点链接左还右
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;

					return true;

				}  // 右为空
				else if (cur->_right == nullptr)
				{
					//还有可能删到根节点的一边为空(有点像歪脖子树)
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						//要判断父节点链接左还右
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;

					return true;
				}
				else
				{
					//左右子树都不为空
					//找保姆
					//左子树的最大节点 or 右子树的最小节点  二者都可以
					//  最右节点             最左节点
					Node*pMinRight = cur;//右子树的最小节点的父节点
					Node*MinRight = cur->_right;//右子树的最小节点
					//找右子树的最左节点
					while (MinRight->_left)
					{
						pMinRight = MinRight;
						MinRight = MinRight->_left;
					}

					//直接赋值
					cur->_key = MinRight->_key;

					//判断父节点要链接左还是右
					if (pMinRight->_left == MinRight)
					{
						pMinRight->_left = MinRight->_right;
					}
					else
					{
						pMinRight->_right = MinRight->_right;
					}

					//删除MinRight,因为完成交换了
					delete MinRight;
					MinRight = nullptr;

					return true;
				}
			}
		}

		return false;
	}

因为是树结构,所以我们也可以使用递归完成以上的增删查的操作

五. 递归

1. 递归查找

因为二叉搜索树的特殊结构,所以其实我们不用遍历所有的节点,只要根据比较结果走相应的路径就好

因为是在类内写递归,所以我们同样需要封装一层

代码如下:

//查找
	bool FindR(const K&key)
	{
		return _FindR(_root, key);
	}

	bool _FindR(Node*root, const K&key)
	{
		if (root == NULL)
		{
			return false;
		}
		//比较当前节点,相等则返回真
		if (root->_key == key)
		{
			return true;
		}
		//不相等继续往一边走
		if (key < root->_key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return _FindR(root->_right, key);
		}

	}

2. 递归插入

插入的基本思路也一样,但是我们在递归到空时构建新节点。
链接有三种方式:

  1. 多传一个参数,记录父节点
  2. 递归到空节点的上一层,比如if(root->_left==NULL) 开始构建节点
  3. 传引用

前两个方法和循环写法没什么区别,我们展示一下第三种

代码如下:

//插入
	bool InsertR(const K&key)
	{
		return _InsertR(_root, key);
	}

	//使用引用,当前的递归可以影响上一层
	bool _InsertR(Node*&root, const K&key)
	{
		if (root == NULL)
		{
			root = new Node(key);
			return true;
		}

		//因为传参是引用,root相当于父节点的左或右
		if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else if(key<root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			//相同则返回假
			return false;
		}
	}

我们在传参时,参数是指针的引用,这样我们跳转到下一层递归,下一层递归的root就是上一层root的左节点或者右节点。

3. 递归删除

递归删除的基本思路和删除一样,也是分为2种情况,叶子节点或者度为1的节点,度为2的节点。
删除第一种情况很简单,我们使用引用,所以直接赋值就好。
删除第二种情况也可以像循环那样,但是我们还可以做个应用。
先看代码

	//删除
	bool EraseR(const K&key)
	{
		return _EraseR(_root, key);
	}

	bool _EraseR(Node* &root, const K&key)
	{
		if (root == NULL)
		{
			//没找到删除的节点
			return false;
		}

		//递归
		if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _EraseR(root->_left, key);
		}
		else
		{
			//找到了
			
			//保存一下要删除的节点
			Node*del = root;

			if (root->_left == NULL)
			{
				//左为空,则链接右
				root = root->_right;
			}
			else if (root->_right == NULL)
			{
				//右为空,则链接左
				root = root->_left;
			}
			else
			{
				//还是替代法,找左子树的最大或者右子树的最小
				//此处举例左子树的最大

				Node*LMax = root->_left;
				while (LMax->_right)
				{
					LMax = LMax->_right;
				}
				
				//覆盖,然后从删除节点的左子树重新删除
				root->_key = LMax->_key;
				return _EraseR(root->_left, root->_key);

				//递归回来删除节点
				delete LMax;
			}


			return true;
		}
	}

删除度为2的节点,我们可以将替换的值覆盖后,转为在删除节点的左子树中,删除替换的节点。

六. 拷贝构造/析构/赋值重载

拷贝构造

二叉搜索树的深拷贝其实同STL的容器一样,需要一个节点一个节点的拷贝,我们使用前序构建,后续链接的方式拷贝。
代码如下:

	//拷贝构造--深拷贝
	BSTree(const BSTree<K>&t)
	{
		_root = Copy(t._root);
	}

	//深拷贝
	//前序构建,后续链接
	Node* Copy(Node*root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node*newRoot = new Node(root->_key);
		//链接
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);

		return newRoot;
	}

析构
析构我们需要后续遍历销毁节点。

	//析构
	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}
	
	//销毁二叉搜索树
	void Destroy(Node*root)
	{
		if (root == NULL)
		{
			return;
		}
		
		//先删除左右节点,再删除当前节点
		Destroy(root->_left);
		Destroy(root->_right);

		delete root;
		root = nullptr;
	}

赋值重载
我们采用现代写法,套用拷贝构造

	//赋值重载
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

因为参数是形参,会发生拷贝构造,我们再用swap交换一下,这样就可以获得新的二叉搜索树。

七. 完整代码

完整代码如下:

#pragma once

//二叉搜索树
//每个左节点都比根节点小,每个右节点都比根节点大


//类模板,用于存储不同数据
template<class K>
struct BinarySearchTree
{
	BinarySearchTree<K>*_left;//左子树
	BinarySearchTree<K>*_right;//右子树
	K _key;//节点值

	//构造函数
	BinarySearchTree(const K&key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	~BinarySearchTree()
	{
		_left = nullptr;
		_right = nullptr;
	}
};


template<class K>
class BSTree
{
	typedef BinarySearchTree<K> Node;
public:
	//BSTree()=default;//制定强制生成默认构造

	BSTree()
		:_root(nullptr)
	{}

	//拷贝构造--深拷贝
	BSTree(const BSTree<K>&t)
	{
		_root = Copy(t._root);
	}

	//赋值重载
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}


	//析构
	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}

	//插入
	bool Insert(const K&key)
	{
		//头为空时单独处理
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		//循环找插入的位置
		Node*cur = _root;
		//记录父节点,实现链接
		Node*parent = nullptr;
		while (cur)
		{
			parent = cur;
			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else
			{
				//相等则返回假
				return false;
			}
		}

		//找到了要插入的位置
		cur = new Node(key);
		//链接
		if (key > parent->_key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

	//中序遍历
	//因为二叉搜索树的特点,中序打印出来就是升序

	//实现封装
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	//查找
	bool Find(const K&key)
	{
		Node*cur = _root;
		//循环查找
		while (cur)
		{
			//比当前值小,则往左走
			if (key < cur->_key)
			{
				cur = cur->_left;
			}//大则往右走
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}//不然就是相等,相等就是找到了
			else
			{
				return true;
			}
		}

		//循环没返回说明没查到
		return false;
	}

	//删除
	//删除最好画图
	//要考虑极端情况
	bool Erase(const K&key)
	{
		//分成两类
		//左或者右为空(包括叶子结点)
		//左右孩子都有

		//首先先找节点
		Node*cur = _root;
		//记录父亲节点
		Node*parent = nullptr;

		while (cur)
		{
			//parent = cur;
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				//找到了

				//分两种情况

				//左为空
				if (cur->_left == nullptr)
				{
					//还有可能删到根节点的一边为空(有点像歪脖子树)
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						//要判断父节点链接左还右
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}

					delete cur;
					cur = nullptr;

					return true;

				}  // 右为空
				else if (cur->_right == nullptr)
				{
					//还有可能删到根节点的一边为空(有点像歪脖子树)
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						//要判断父节点链接左还右
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}

					delete cur;
					cur = nullptr;

					return true;
				}
				else
				{
					//左右子树都不为空
					//找保姆
					//左子树的最大节点 or 右子树的最小节点  二者都可以
					//  最右节点             最左节点

					Node*pMinRight = cur;//右子树的最小节点的父节点
					Node*MinRight = cur->_right;//右子树的最小节点

					while (MinRight->_left)
					{
						pMinRight = MinRight;
						MinRight = MinRight->_left;
					}

					//直接赋值
					cur->_key = MinRight->_key;

					//判断父节点要链接左还是右
					if (pMinRight->_left == MinRight)
					{
						pMinRight->_left = MinRight->_right;
					}
					else
					{
						pMinRight->_right = MinRight->_right;
					}

					//删除MinRight,因为完成交换了
					delete MinRight;
					MinRight = nullptr;

					return true;
				}
			}
		}

		return false;
	}

	//递归写法
	//查找
	bool FindR(const K&key)
	{
		return _FindR(_root, key);
	}

	//插入
	bool InsertR(const K&key)
	{
		return _InsertR(_root, key);
	}


	//删除
	bool EraseR(const K&key)
	{
		return _EraseR(_root, key);
	}

protected:
	//深拷贝
	//前序构建,后续链接
	Node* Copy(Node*root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node*newRoot = new Node(root->_key);
		//链接
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);

		return newRoot;
	}

	//销毁二叉搜索树
	void Destroy(Node*root)
	{
		if (root == NULL)
		{
			return;
		}

		//先删除左右节点,再删除当前节点
		Destroy(root->_left);
		Destroy(root->_right);

		delete root;
		root = nullptr;
	}


	//删除
	bool _EraseR(Node* &root, const K&key)
	{
		if (root == NULL)
		{
			//没找到删除的节点
			return false;
		}

		//递归
		if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _EraseR(root->_left, key);
		}
		else
		{
			//找到了

			//保存一下要删除的节点
			Node*del = root;

			if (root->_left == NULL)
			{
				//左为空,则链接右
				root = root->_right;
			}
			else if (root->_right == NULL)
			{
				//右为空,则链接左
				root = root->_left;
			}
			else
			{
				//还是替代法,找左子树的最大或者右子树的最小
				//此处举例左子树的最大

				Node*LMax = root->_left;
				while (LMax->_right)
				{
					LMax = LMax->_right;
				}

				//覆盖,然后从删除节点的左子树重新删除
				root->_key = LMax->_key;
				return _EraseR(root->_left, root->_key);

				//递归回来删除节点
				delete LMax;
			}


			return true;
		}
	}


	//使用引用,当前的递归可以影响上一层
	bool _InsertR(Node*&root, const K&key)
	{
		if (root == NULL)
		{
			root = new Node(key);
			return true;
		}

		//因为传参是引用,root相当于父节点的左或右
		if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			//相同则返回假
			return false;
		}
	}


	bool _FindR(Node*root, const K&key)
	{
		if (root == NULL)
		{
			return false;
		}

		if (root->_key == key)
		{
			return true;
		}

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

	//因为要递归,所以要单独编写
	//注意此处不可以加缺省值_root,因为缺省值需要是常量
	void _InOrder(Node*root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

private:
	Node*_root = nullptr;//根节点
};

结束语

本篇知识记录较杂,请多谅解。本着记笔记分享的目的,望佬指点。感谢你的阅读

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值