二叉搜索树相关

定义:

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。 它的左右子树也分别为二叉搜索树

对二叉搜索树的一些操作

基本结构

首先,我们先定义出一些结构
第一个结构是树里面的内容,包含左指针右指针,以及key和value。
接下来的一个类,用来表示这是一个二叉搜索树

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:
		Node* _root = nullptr;
};

Insert操作

Insert的思路是通过key去查找,看在树里面有没有这个节点,如果有就插入失败(因为二叉搜索树中不允许有重复的值出现,否则就失去了意义)。 如果没有那就去找一个合适的位置插入。
找合适的位置:①如果根为空,直接创建新节点当根节点插入 ②根不为空,就去子树上找。 这里我们要注意为了方便控制我们的循环条件,我们大循环是以cur为循环条件去找的,所以我们需要有parent来记录cur的父亲位置,这样后面才方便插入。(为什么不直接改循环条件? 因为这里判断不仅仅是一个节点,它既需要判断左节点又要判断右节点,所以我们选择以parent的形式去记录)

bool Insert(const K& key, const V& value)
{
	// 1.根为空,直接插入一个新节点
	if (_root == nullptr)
	{
		_root = new Node(key, value);
		return true;
	}
	// 2.根不为空,则去找到合适位置插入
	Node* parent = _root;
	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已经是空指针了,我们应该需要的是cur的parent位置,所以上面需要有parent来记录
	cur = new Node(key, value);   // 直接重新赋值cur就可以了,不用再加一个新的变量进来
	if (key > parent->_key)
		parent->_right = cur;
	if (key < parent->_key)
		parent->_left = cur;
	return true;
}

Find操作

Find操作非常简单,找根,左孩子右孩子,如果遍历完了依然没有找到,那就返回空指针。

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

Erase操作

Erase操作相对就会比较的麻烦了。
Erase的思路:首先,我们需要去判断这个节点是否存在,不存在返回false。存在才会有下面的删除操作(不存在的情况放在最外面了)。
删除节点分为三个情况:①删除的节点只有左孩子
②删除节点只有右孩子(没有孩子包含在这两种情况)
③既有左孩子又有右孩子
那么针对三种情况,我们有不同的要求,建议通过图形来进行理解:
①、②种是类似的。在这里插入图片描述
我们进行判断又分三种情况,第一种:我们要删除的节点就是根节点,那么就直接对根进行处理(我们说了删除节点无左右孩子也包含在①、②的情况里,这里就体现出来了,这里也可以处理)。 第二种:我们要删除的节点cur是parent的左孩子 第三种: 我们要删除的节点cur是parent的右孩子。 第二种和第三种情况是不一样的,这里结合图片就可以看出来。

难处理的是第③种情况
如果说我们删除的节点既有左孩子又有右孩子,那么在处理的时候需要用到交换法。 交换的对象是cur和cur左孩子中的最大值或右孩子中的最小值。
所以第一步是找到最大的左孩子或最小的右孩子(这里我们以找到最小右孩子为例)
找到之后,还要分情况讨论: 一种是这个最小的右孩子有左节点也有右节点的情况
还一种是这个最小的右孩子只有右节点的情况(注意:这里不可能出现这个节点还有左节点的情况,因为在此之前我们找到的这个RightMin是右子树的最小节点,就不可能出现它还有比他小的左节点的情况)
第二种可以看到,我们的RightMin还有一个右节点的情况,这个情况就得特殊处理了,如果RightMinParent->left = RightMin。我们RightMinParent的左节点就得指向RightMin的右结点(同样,RightMin不会有左节点)
这里我们怎么样更好的理解记忆呢?
我们可以看第一种情况是RightMinParent和RightMin二者成一条线。 第二种情况是不成一条线,RightMin为RightMinParent左孩子的情况。
在这里插入图片描述

bool Erase(const K& key)
{
	// 删除分为3个情况
	// 1.删除节点只有左孩子 2.删除节点只有右孩子(没有孩子包含在这两种情况)  3.既有左孩子又有右孩子
	Node* parent = _root;
	Node* cur = _root;
	while (cur)
	{
	    // 先找节点,找到了才有后面的删除操作,否则要返回false
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else   //此时进行删除
		{
			if (cur->_left == nullptr)  //无左孩子
			{
				if (cur == _root)   // 只有一个根节点
					_root = cur->_right;
				else if(cur == parent->_left)   // 这个节点为parent的左孩子的情况
				{
					parent->_left = cur->_right;
				}
				else if (cur == parent->_right) // 这个节点为parent的右孩子的情况
				{
					parent->_right = cur->_right;
				}
				delete cur;
			}
			else if (cur->_right == nullptr)  // 无右孩子
			{
				if (cur == _root)
					_root = cur->_left;  // 直接让左孩子作为根就可以了
				else if (cur == parent->_left)
				{
					parent->_left = cur->_left;
				}
				else if (cur == parent->_right)
				{
					parent->_right = cur->_left;
				}
				delete cur;
			}
			else  // 此时为左右孩子都有的情况
			{
				Node* RightMinParent = cur;
				Node* RightMin = cur->_right;
				while (RightMin->_left)
				{
					RightMinParent = RightMin;
					RightMin = RightMin->_left;
				}

				swap(RightMin->_key, cur->_key);
				swap(RightMin->_value, cur->_value);
				
				if (RightMinParent->_right == RightMin)   // 特殊情况得特殊处理
					RightMinParent->_right = RightMin->_right;
				else
					RightMinParent->_left= RightMin->_right;  //此时,RightMin
				//只可能有右孩子,因为它是最小的右子树节点(不能有比它更小的了)
				delete RightMin;
			}
		}
		return true;
	}
	return false;
}

InOrder遍历二叉树操作

这里我们需要注意两个点: 第一个是如何遍历,也就是我们调用采用递归的方法。
第二个是为什么我们这里要封装一层,而不是直接用,直接传值root并接收。 因为root是一个私有变量,我们在类外是无法访问到的,此时我们要执行遍历操作又必须得从根开始。想访问到,就需要通过函数进入到内部,才能访问到这个私有变量。

	void InOrder()  // 为什么这里要封装一层而不是直接传入root,因为root是私有成员,
	//我们在外面传参时是访问不到这个私有成员变量的。我们只有通过函数进入到类内,
	//才能访问到root,并传参给包了一层的InOrder函数
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << ":" << root->_value << endl;
		_InOrder(root->_right);
	}

模拟字典

下面的代码可以用来模拟字典操作

BSTree<string, string> dict;
dict.Insert("insert", "插入");
dict.Insert("erase", "删除");
dict.Insert("left", "左边");
dict.Insert("string", "字符串");
dict.Erase("insert");
dict.Erase("string");
string str;
while (cin >> str)
{
	auto ret = dict.Find(str);
	if (ret)
	{
		cout << str << ":" << ret->_value << endl;
	}
	else
	{
		cout << "单词拼写错误" << endl;
	}
}

模拟统计次数

下面的这个代码,可以用来统计次数

	string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
	// 统计水果出现的次
	BSTree<string, int> countTree;
	for (auto str : strs)
	{
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);   // 不存在就插入
		}
		else
		{
			ret->_value++;  //存在就让value++
		}
	}
	countTree.InOrder();
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值