二叉搜索树(KV模型,二叉搜索树删除节点)

1. 二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
在这里插入图片描述
每个子树均满足这个特征。

他可以用来搜索,在我们以前怎么搜索呢,之前的数据结构不仅能存储数据,也能查找数据。最优搜索便是二分查找,但是二分查找前提必须有序,最快的一个排序也是n*logn,并且二分查找必须要在Vector中,头插,中间插,头删,中间删的挪动数据的复杂度也很高。他是一种华而不实的查找方式。而二叉搜索树是一个非常适合用于查找的结构,复杂度为(O(logN)-O(N)之间),最坏是找高度次,而且他天然的一个特性,中序遍历为有序。

2. 二叉搜索树的实现

2.1 插入

插入,大于就去右子树比较,小于就去左子树在比较。需要保证搜索树的性质。
怎么保证呢,我们维护两个节点,parent和cur,在每次走之前,parent保存这次的位置,当key大于当前节点,cur往右走,当key小于当前节点,cur往右走。不允许相等,假如相等直接返回false,cur一直走,当节点为空的时候,再拿key和parent节点的值进行比较,如果小于parent对应的值,则连接在左边,否则连接在右边。

插入的时间复杂度,最坏是O(N),注意别和,完全二叉树和满二叉树搞混了,因为他可能是单支结构。
在这里插入图片描述
所以他不在极端情况下是比链表顺序表要优一些,但是极端情况也只是相同O(N)。所以基于搜索二叉树上又加了一层平衡,比较均匀高度就是可控的,平衡搜索二叉树接近O(logN),看做O(logN)

2.2 查找

明白了二叉搜索树的性质之后,查找也是容易了许多。

当前树不为空

只要给定值大于当前节点的值,往右走,小于当前节点的值往左走,等于就返回这个节点。

当前树为空
返回空值

时间复杂度
最坏与链表相同O(N),假如是优化后的平衡搜索二叉树近似于O(logN)

2.3 遍历

这里我们采用中序,因为它的性质给他带来一种天然的属性,中序遍历的时候是有序的。

左—根---右

这里有个编码的小tips,假如成员函数是递归函数,你要用到私有的成员变量。
在这里插入图片描述
但是你在main函数中,无法调用因为你访问不到,私有的root节点。在这里插入图片描述

这时我们可以写一个函数获取root节点,但是这样不好看。我们将这个递归函数做再一次的封装。
在这里插入图片描述
这样就好看多了。
在这里插入图片描述
打印出来是有序的。
在这里插入图片描述

2.4 删除

在这里插入图片描述
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点(剩余节点)

2.5 被删除节点无孩子节点

在这里插入图片描述

2.6 被删除节点只有右孩子(左边为空)

在这里插入图片描述
假如要删头,就是一个右单枝二叉树
在这里插入图片描述

假如被删除节点是父亲的右节点,将被删除节点的右孩子连接父亲的右节点。
在这里插入图片描述
假如被删除节点是父亲的左节点,将被删除节点的右孩子连接至父亲的左节点
在这里插入图片描述

2.7 被删除节点只有左孩子(右边为空)

和左边为空的逻辑,一模一样。相对的需要注意左单枝二叉树

在这里插入图片描述

2.8 归纳

叶子节点就可以看做,左右为空的任意情况之一,在其中一并判断。
在这里插入图片描述
无论将它看做左为空,还是右为空,逻辑都是可以处理。

2.9 被删除节点,左右都不为空

首先这个节点删除不能影响它的性质,直接删除掉,后续的节点怎么连起来呢,很难处理。
所以我们思路得变成,在树中找一个合适的节点,将它覆盖掉。同时又可以保证它的性质不变。
经过研究,我们可以取,被删除节点的,左子树最大值或者右子树最小值。将要删除的节点覆盖。同时可以保证它的性质。

怎么找出左子树的最大值呢?

由于其子树也满足二叉搜索树的性质,所以就是左子树的最右节点。

为什么左子树最大值可以呢?

因为覆盖删除节点的值无非满足两个条件,第一,小于右孩子,第二,大于左孩子。

从左子树挑选,不会出现大于右孩子的(有就不是二叉搜索树了)肯定是小于右孩子的,满足条件1。
但是还要大于左孩子,就需要挑选左子树的最大值即最右节点,才能保证替换后的值大于左子树中的所有值。找到了最右节点,不管是不是叶子节点,他一定是一个右为空的节点(假如右边还有,那他就不是最右了)。

怎么找出右子树的最小值?

由于其子树也满足二叉搜索树的性质,所以就是右子树的最左节点。

为什么右子树的最小值可以呢?
两个条件,第一,小于右孩子,第二,大于左孩子。
从右子树选,一定大于左孩子,而且选的是最小值,所以一定小于右孩子。
同时选定的一定是一个左为空的节点。

示例1

删除6,以找右子树最小值(右子树的最左)为例。
在这里插入图片描述
当subMin->left为空停止

把subMin->_key赋值给cur->_key。

由于要删除,所以需要一个smParent,在他每次循环下去,判断其左是否为空时,保存他的上一个
这种情况下,smParent初始值为null没有问题,因为循环下去就会被重新赋值。
在这里插入图片描述

删除掉subMin,subMin左边一定为空,右边不一定,由于subMin是smParent的左,所以需要smParent的左连接subMin的右。

示例2

还有隐藏的一个问题,父亲节点不能声明为nullptr,在这种情况下

删除7,找7的右子树最小值。

在这里插入图片描述

在这里插入图片描述
由于一开始,subMin->left就等于nullptr,所以没有循环进入赋值。后面if语句里还对smParent进行了操作,空指针异常,所以smParent要声明为cur。

3 K模型与KV模型

搜索树,真正的作用是排序。它分为两种模型。
K搜索模型对应STL中的Set,他只有一个值
K-value搜索模型对应STL的map,它存储两个值。

3.1 K模型

K搜索模型,他用来查找“在不在”的问题,例如门禁,在刷卡期间,门禁机器识别你的学号,他就去查找存储结构中你的信息,从而判断。
而且还被用来做排序和去重,当中序的时候天然有序,重复的数据直接不插入

3.2 K模型搜索树实现代码

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

//K,键值
template<class K>
struct BSTreeNode
{
	K _key;
	struct BSTreeNode<K>* _left;
	struct BSTreeNode<K>* _right;

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

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
    //插入
	bool Insert(const K& val)
	{
		if (_root == nullptr)
		{
			_root = new Node(val);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (val > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if(val < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		//走到这,说明cur为空,parent为他的上一个节点。
		//确定了他的位置,开始和父亲比较,看放左边还是右边
		Node* newNode = new Node(val);
		if (val > parent->_key)
		{
			parent->_right = newNode;
		}
		else//不存在等于情况,等于的话在前面肯定返回false了
		{
			parent->_left = newNode;
		}

		return true;
	}
	//查找(K不允许修改)
	const Node* Find(const K& key)
	{
		Node* cur = root;
		while (cur)
		{

			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key == cur->_key)
			{
				return cur;
			}
			else
			{
				cur = cur->_left;
			}
		}
		return nullptr;
	}


	//删除
	bool Erase(const K& val)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (val > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (val<cur->_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)
					{
						cur->_left = _root;
					}
					else
					{
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				//两边都有节点
				//右子树最小值(最左)为例
				else
				{
					Node* cur = _root;
					Node* subMin = cur->_right;
					Node* smParent = cur;
					while (subMin->_left)
					{
						smParent = subMin;
						subMin = subMin->_left;
					}
					//覆盖
					cur->_key = subMin->_key;
					//子树的最左节点,父亲根据subMin在哪一边,连接他的右
					if (subMin == smParent->_left)
					{
						//subMin可以看做左为空
						//连上他的右
						smParent->_left = subMin->_right;
					}
					else
					{
						smParent->_right = subMin->_right;
					}
					delete subMin;
				}
				return true;
			}
		}
		return false;
		
	}
	
	//用来递归的子结构
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

			_Inorder(root->_left);
		    cout << root->_key<<" ";
			_Inorder(root->_right);
	}
    //封装起来让外面可以调用
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
private:
	Node* _root = nullptr;
};



#include"BSTree.hpp"


#include"BSTree.hpp"


int main()
{
	BSTree<int> Tree;
	Tree.Insert(5);
	Tree.Insert(4);
	Tree.Insert(1);
	Tree.Insert(8);
	Tree.Insert(6);
	Tree.Inorder();
	
	Tree.Erase(4);
	Tree.Inorder();
	Tree.Erase(5);
	Tree.Inorder();
	Tree.Erase(8);

	Tree.Inorder();
	return 0;
}

在这里插入图片描述

3.3 KV模型

K-value,字典模型,输入英文查找中文。
当你插入一个英文(K)的时候,对应的中文(val)也被插入。

相比于K模型,代码逻辑不变,还是以K为依据。将Find中的const去掉,因为Value是可以改变的。然后K值不变,声明的时候加上const。

他依旧可以,1. 查找在不在,2. 排序+去重。
在这里插入图片描述
3. 最关键的是他具有字典的特征:
在这里插入图片描述
4.统计出现次数

在这里插入图片描述

3.4 KV模型搜索树实现代码

#include<iostream>

using namespace std;
#pragma once

//K,键值
template<class K,class V>
struct BSTreeNode
{
	const K _key;
	V _val;
	struct BSTreeNode<K,V>* _left;
	struct BSTreeNode<K,V>* _right;

	BSTreeNode(const K& k,const V& v)
		:_key(k)
		, _val(v)
		,_left(nullptr)
		,_right(nullptr)
	{}
};

template<class K,class V>
class BSTree
{
	typedef BSTreeNode<K,V> Node;
public:
	bool Insert(const K& key,const V& val)
	{
		if (_root == nullptr)
		{
			_root = new Node(key,val);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if(key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		//走到这,说明cur为空,parent为他的上一个节点。
		//确定了他的位置,开始和父亲比较,看放左边还是右边
		Node* newNode = new Node(key,val);
		if (key > parent->_key)
		{
			parent->_right = newNode;
		}
		else//不存在等于情况,等于的话在前面肯定返回false了
		{
			parent->_left = newNode;
		}

		return true;
	}
	//不允许改
	 Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{

			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key == cur->_key)
			{
				return cur;
			}
			else
			{
				cur = cur->_left;
			}
		}
		return nullptr;
	}


	//用来递归的子结构
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

			_Inorder(root->_left);
		    cout << root->_key<<" : "<<root->_val;
			cout << endl;
			_Inorder(root->_right);
	}
    //封装起来让外面可以调用
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
private:
	Node* _root = nullptr;
};

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

int main()
{

	
	/*BSTree<string, string> dict;
	dict.Insert("apple", "苹果");
	dict.Insert("orange", "橘子");
	dict.Insert("people", "人");
	dict.Insert("student", "学生");
	dict.Insert("milk", "牛奶");
	dict.Insert("paper", "卫生纸");
	dict.Insert("paper", "啦啦啦");
	dict.Insert("Big apple", "苹果");
	dict.Inorder();*/
	//string str;
	//while (cin >> str)
	//{
	//	BSTreeNode<string, string>* ret = dict.Find(str);
	//	if (ret)
	//	{
	//		cout << ret->_val << endl;
	//	}
	//	else
	//	{
	//		cout << "字典中不存在"<< endl;
	//	}
	//	
	//}
	string strArry[] = { "苹果", "香蕉", "西瓜", "西瓜", "香蕉" };
	//前面存名字,后面存次数
	BSTree<string, int> countTree;
	for (auto& str : strArry)
	{
		auto ret = countTree.Find(str);
		if (ret == nullptr)
		{
			//没有就插入,第一次插入str和1
		  	countTree.Insert(str, 1);
		}
		else
		{
			//第二次往上,就++次数
			ret->_val++;
		}
	}
	countTree.Inorder();

	return 0;
}


  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

楠c

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

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

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

打赏作者

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

抵扣说明:

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

余额充值