C++二叉搜索树

💓博主CSDN主页:麻辣韭菜-CSDN博客💓

⏩专栏分类:C++知识分享

🚚代码仓库:C++高阶🚚

🌹关注我🫵带你学习更多C++知识
  🔝🔝

目录

一.二叉搜索树的定义

二. 二叉搜索树循环操作

1. 二叉搜索树的插入

2. 二叉搜索树的查找

3. 二叉搜索树的删除

三.二叉搜索树递归操作

1.二叉搜索树递归算法的插入实现

2.二叉搜索树递归算法的查找实现

3.二叉搜索树递归算法的删除实现

四.二叉搜索树拷贝构造(赋值重载)与析构函数

五.二叉搜索树应用场景

1. K模型

2. KV模型

3.二叉搜索树的性能分析


一.二叉搜索树的定义

观察上图,我们可以发现根节点7的左边节点是小于右边的节点,左子树的上所有的节点都小于根节点,右子树的所有节点都大于根节点。而且根的左右子树也都是搜索二叉树

二. 二叉搜索树循环操作

1. 二叉搜索树的插入

先定义出二叉搜索树结构 

//定义二叉树节点
template<class K>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;
	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
	
};
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
private:
	Node* _root = nullptr;

};

二叉搜索树插入实现方法↓

public:
    //二叉搜索树插入实现
	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 if (cur->_key > key) //大于要插入的数就在左子树
			{
				parent = cur;
				cur = cur->_left;
			}
			else  // 相等后面AVL 红黑在说。
			{
				return false;
			}

		}
		cur = new Node(key);
		//链接
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}

这里有个细节必须要说以下记录下父节点,cur出了作用域就销毁了,我们在链接过程中是找不到cur,就存在内存泄漏了野指针问题。先编写个中序遍历函数打印看看是不是对的

//中序遍历函数
void Inorder() 
	{
		_InOrder(_root);
		cout << endl;
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

我们在主函数中,调中序函数传参是个问题,我们在类的内部再实现一个调用中序遍历的函数。

打印结果没有问题,接着实现查找和删除

2. 二叉搜索树的查找

bool Find(const K& key)
	{
		if (_root == nullptr) //如果二叉树为空直接返回假
		{
			return false;
		}
		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; //循环结束没找到返回假
	}

3. 二叉搜索树的删除

删除比较复杂,先画图考虑各种左右子树情况

删除1 4 7 13 10 14 这6个数没有问题,直接无脑删除 父节点的左右指针指向nullptr就行。

问题的关键在于如何删除 8 3 6 这3个数?

替换法 比如 我要删除8 那就去左子树找到最大的那个节点 7。

把8修改成7 再删除7 这样就没有破坏二叉搜索树的结构。

bool Earse(const K& key)
	{
		if (_root == nullptr)
			return false; //树为空直接返回
		//遍历找到要删除的数
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			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;
				}
				//右为空
				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;
				}
				//两边都不为空 选择右子树小的节点或者左子树最大的节点。
				//以左子树最大节点为例
				else
				{
					Node* pmaxLeft = cur; //如果最大左子树的父亲节点设置为空,下面循环没有进去,父亲没有更新,下面条件判断就会出现空指针问题。
					Node* maxLeft = cur->_left;
					while (maxLeft->_right)
					{
						pmaxLeft = maxLeft;
						maxLeft = maxLeft->_right;
					}
					cur->_key = maxLeft->_key;
					if (pmaxLeft->_right == maxLeft) //这判断也是一样 如果上面循环没有进去 那么 8这个节点右子树就打乱结构了。
					{
						pmaxLeft->_right = maxLeft->_left;
					}
					else
					{
						pmaxLeft->_left = maxLeft->_left;
					}

					delete maxLeft;
					
				}
				return true;
			}
		}
		return false;
	}

三.二叉搜索树递归操作

1.二叉搜索树递归算法的插入实现

    bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
	bool _InsertR(Node* &root, const K& key)
	{
		if (root == nullptr)
			root = new Node(key);
		
		if (root->_key < key)
		{
			return _InsertR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
		
		
	}

用递归算法插入,这里有个小细节,我们在类内部实现是需要再套一层函数如上图代码所示

套的那层实现递归算法。如果我们要插入的值大于根的值那就往右子树递归,反之就往左子树递归。一直递归到空,在空这里new一个节点,关键的来了,如果我们传参的值不是引用,那么函数递归返回 栈帧销毁,新开的节点,在上一层递归函数父亲节点没有连接的。

我们传参时用引用就非常巧妙,引用本来就是它自己,父节点的右或左都是它,天然连接。

2.二叉搜索树递归算法的查找实现

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

递归查找就非常简单了,大于就右边找,反之左边找,找到返回真,递归到空还是没有返回假,或者本来就是空。

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->_left, key);
		}
		//大于根就右边找
		else if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		//相等
		else
		{	//删除
			
			Node* del = root; //先记录要删除的节点,
            //右为空直接删除			
            if (root->_right == nullptr)
				root = root->_left;
			//左为空直接删除
            else if (root->_left == nullptr)
				root = root->_right;
			
            //两边都不为空,以右子树为例 找最小的            
            else
			{
				Node* MinRight = root->_right;
				while (MinRight->right)
				{
						MinRight = MinRight->left; //找到右边最小的节点
				}
				swap(root->_key, MinRight->_key);//替换法:右边节点最小的值和根交换值
				return _EraseR(root->_right, key);//替换了之后我们要删除就是它,
                //我们再次递归是不是就变成了上面两种情况,左右各为空的情况。
			}
			delete del;
			return true;
		}
	}

递归算法删除的实现 我直接在代码讲解了。有那个地方不懂得可以私信我

四.二叉搜索树拷贝构造(赋值重载)与析构函数

    BSTree() = default; // 制定强制生成默认构造
	BSTree(const BSTree<K>& k)
	{
		_root = Copy(k._root);
	}
	//现代拷贝写法
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

	~BSTree()
	{
		Destroy(_root);
	}
    //构造
    Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* newRoot = new Node(root->_key);
		newRoot = Copy(root->_left);
		newRoot = Copy(root->_right);
		return newRoot;
	}
    //析构
	void Destroy(Node* &root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}

这3个默认成员函数这里不在多讲 ,不太理解的可以先看C++类和对象-CSDN博客。对于递归算法不太理解,自己可以下去多画一画递归展开图

五.二叉搜索树应用场景

1. K模型

K 模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到
的值
比如: 给一个单词 word ,判断该单词是否拼写正确 ,具体方式如下:
以词库中所有单词集合中的每个单词作为 key ,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2. KV模型

每一个关键码 key ,都有与之对应的值 Value ,即 <Key, Value> 的键值对
该种方式在现实生活中非常常见:
比如 英汉词典就是英文与中文的对应关系 ,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文 <word, chinese> 就构成一种键值对;
再比如 统计单词次数 ,统计成功后,给定单词就可快速找到其出现的次数, 单词与其出
现次数就是 <word, count> 就构成一种键值对

 先讲之前K模型改造成KV模型

namespace gx
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;

		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}

	};
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		
		
		bool Erase(const K& key)
		{
			if (_root == nullptr)
				return false; //树为空直接返回
			//遍历找到要删除的数
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				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;
					}
					//右为空
					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;
					}
					//两边都不为空 选择右子树小的节点或者左子树最大的节点。
					//以左子树最大节点为例
					else
					{
						Node* pmaxLeft = cur; //如果最大左子树的父亲节点设置为空,下面循环没有进去,父亲没有更新,下面条件判断就会出现空指针问题。
						Node* maxLeft = cur->_left;
						while (maxLeft->_right)
						{
							pmaxLeft = maxLeft;
							maxLeft = maxLeft->_right;
						}
						cur->_key = maxLeft->_key;
						if (pmaxLeft->_right == maxLeft) //这判断也是一样 如果上面循环没有进去 那么 8这个节点右子树就打乱结构了。
						{
							pmaxLeft->_right = maxLeft->_left;
						}
						else
						{
							pmaxLeft->_left = maxLeft->_left;
						}

						delete maxLeft;

					}
					return true;
				}
			}
			return false;
		}
		Node* Find(const K& key)
		{
			if (_root == nullptr) //如果二叉树为空直接返回假
			{
				return nullptr;
			}
			Node* cur = _root;
			while (cur)//遍历左右子树
			{
				if (cur->_key < key) // 根小于要查找数右子树找
				{
					cur = cur->_right;
				}
				else if (cur->_key > key) // 根大于要查找数左子树找
				{
					cur = cur->_left;
				}
				else    //等于就是找到了 返回真
				{
					return cur;
				}
			}
			return nullptr; //循环结束没找到返回假
		}
		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key,value);
				return true;
			}
			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
				{
					return false;
				}

			}
			cur = new Node(key,value);
			//链接
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
		protected:
			void _InOrder(Node* root)
			{
				if (root == nullptr)
					return;

				_InOrder(root->_left);
				cout << root->_key << ":" << root->_value << endl;
				_InOrder(root->_right);
			}
	private:
		Node* _root = nullptr;

	};
}

在主函数写一个测试函数,看看结果 

3.二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜树:
最优情况下,二叉搜索树为完全二叉树 ( 或者接近完全二叉树 ) ,其平均比较次数为: $log_2 N$
最差情况下,二叉搜索树退化为单支树 ( 或者类似单支 ) ,其平均比较次数为: $\frac{N}{2}$
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?下篇讲解 AVL 树和红黑树就可以上场了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值