数据结构初阶 · 二叉搜索树

目录

前言:

二叉搜索树的实现

二叉搜索树的基本结构

中序遍历


前言:

在最初学习二叉树的时候,就提及到过单独用树来存储数据是既不如链表也不如顺序表的,二叉树的用处可以用来排序,比如堆排序,也可以用来搜索数据,这是二叉树的用处,用来排序可以实现堆,用来搜索数据可以实现二叉搜索树,即今天实现的一种结构。

那么什么是二叉搜索树呢?

即左孩子比根小,右孩子比根大,且所有的子树都满足这个特点,这就是二叉搜索树,那么是如何实现搜索数据的呢?

搜索数据就是判断大小,最多走高度次个语句就可以找到数据了。

那么找数据的时间复杂度是不是O(logn)呢?很显然不是,万一存在只有左子树或者只有右子树有节点的树呢?那样的话时间复杂度就是O(N)了,所以时间复杂度是O(logN ~ N)。

话不多说,现在开始实现。

二叉搜索树的实现

二叉搜索树的基本结构

template <class T>
struct BSTreeNode
{
	BSTreeNode<T>* _left;
	BSTreeNode<T>* _right;
	T _key;

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

template <class T>
class BSTree
{
public:
	typedef BSTreeNode<T> Node;

private:
	Node* _root = nullptr;

};

这是二叉搜索树的基本结构,每个节点都是一个结构体,好奇的人可能会问为什么值不是val而是key?这是因为二叉搜索树有两个模型,一个是key模型,一个是key-value模型,在key模型中,是不能修改数据的,因为一旦修改了数据整个树的结构就很容易被打乱,在key-value模型中,就可以修改数据,比如有一个数据集合,每个节点都有key和value,每存在一个key,value就++,所以key-value模型中能修改数据,但是修改的是value,即值出现的次数,总结就是能修改的数据就是对整棵树的结构没有影响的数据。

增的基本逻辑就是,如果比当前位置的值大,就走右子树,如果比当前位置的值小,就走左子树,如果该树是一个空树,那么这个值就充当根节点。当走到空了,我们就应该考虑连接的部分了,连接的时候,我们需要父节点,判断该值和父节点的大小,再使父节点的左右指针指向这个节点,既然需要父节点,我们这个时候就需要存储父节点的位置,每当走下个节点的时候,就存储一下父节点的位置,基本逻辑就这么多:

bool Insert(const T& val)
{
	if (_root == nullptr)
	{
		_root = new Node(val);
		return true;
	}
	Node* root = _root;
	Node* parent = nullptr;
	//判断部分
	while (root)
	{
		if (val > root->_key)
		{
			parent = root;
			root = root->_right;
		}
		else if (val < root->_key)
		{
			parent = root;
			root = root->_left;
		}
		else
		{
			return false;
		}
	}
	Node* newnode = new Node(val);
	//连接部分 开始判断大小关系
	if (parent->_key > val)
	{
		parent->_left = newnode;
	}
	else
	{
		parent->_right = newnode;
	}
	return true;
}

当然,为了方便,我们都写成了成员函数。

这里有个问题就是,如果存在两个相同的数据怎么办?

实际来说二叉搜索树是不允许存在相同的数据的,这样导致了数据冗余,就像字典里面,存在相同的两个单词吗?不会的是吧,所以我们就不考虑多种相同数据的情况,代码里面返回的就是false。

查就很简单了,查就是增的部分代码,遍历一遍,比较有没有这个值就行,遍历多简单,小就走左子树,大就走右子树,相等就返回true:

//查
bool FindKey(const  T& val)
{
	Node* root = _root;
	while (root)
	{
		if (val > root->_key)
		{
			root = root->_right;
		}
		else if (val < root->_left)
		{
			root = root->_left;
		}
		else
		{
			return true;
		}
	}
	return false;
}

中序遍历

数据加上了,也可以查数据了,我们现在想把数据打印出来看一下怎么办呢?这里推荐使用中序遍历,左子树根右子树这样的顺序打印,因为二叉搜索树的特性,这里打印出来就是升序,看着较为顺眼:

//中序遍历
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

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

感觉奇怪吧?为什么InOrder的参数不用Node* root呢?因为存在this指针,this指针是在参数的第一个位置,如果我们传参,传的是根,接受到的还是this指针,就冲突了,所以这里有几个办法,第一个是用一个get set函数,这个方法java比较喜欢使用,第二个方法就是设为私有,套一层函数使用吗,私有函数就没有this指针了。

到现在是不是都感觉二叉搜索树没啥?那是因为还没有到删除部分。删除部分才是二叉搜索树的核心。

给定一个二叉搜索树,删除可以分为以下几种情况,第一种情况是删除7 和 14,第二种情况是删除3 和 8。

第一种情况是属于可以直接删除的情况。

对于直接删除的情况,我们分为左右指针都为空,左指针为空,右指针为空的三种情况,实际上,我们可以只分为两种情况,第一种是左指针为空,第二种是右指针为空,比如7,删除7就是让6指向7的任意左右指针就可以了,删除14,我们需要让10的右指针指向13,有一个点就是为什么10指向的地方一定是比10大的?因为二叉树的特性,如果是9,就一定不会在10的下面。

我们可以总结以下,删除的时候,先判断是左为空还是右为空,然后判断子节点和父节点的位置,这样好让父节点指向下一个指针,连接的主要根据就是判断子节点和父节点相对位置。

如果两个都为空怎么办?我们已知一个节点不为空,另一个节点为不为空我们都指向它,总归是没错的。这点可以反证。当我们删除的是根节点的时候,只需要让根节点指向的内容是空就可以了,所以无论我们把删除根节点的位置放在左为空还是右为空都没问题。

到这里两个都为空的问题也就顺理成章的解决了,两个都为空,来就直接走左为空的场景,判断相对位置,父节点连接子节点的右节点,连接的是空指针,解决了就。

这部分的代码如下:

Node* parent = nullptr;
Node* cur = _root;
//先找到 找到该节点才能删除
while (cur)
{
	if (cur->_key < val)
	{
		parent = cur;
		cur = cur->_right;
	}
	else if (cur->_key > val)
	{
		parent = cur;
		cur = cur->_left;
	}
	//找到了 开始删除
	else
	{
		///第一种情况:左为空 -> 都为空
		if (cur->_left == nullptr)
		{
			//删除根节点的时候
			if (cur == _root)
			{
				cur = cur->_right;
			}
			else
			{
				if (cur == parent->_left)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;
				}
			}
			///第二种情况:右为空
			else if (cur->_right == nullptr)
			{
				if (cur == parent->_left)
				{
					parent->_left = cur->_left;
				}
				else
				{
					parent->_right = cur->_left;
				}
			}
		}
}

第二种情况是删除3 和 8 的情况,这种就要麻烦一点,删除的话得用替换法,因为我们没有办法之直接删除它,那么是怎么个替换法呢?我们从树里面找一个数据,满足大于该节点的左节点,小于该节点的右节点,就算是替换完成了。

那么从哪里找这种适配的数据呢?当然是从该节点的左右子树去找了,我们找右子树的最小值,或者是左子树的最大值都可以满足,右子树的最小值,即比右节点的值小,但是同时比左节点大,这就满足了,找到了该值之后,我们要做的是交换数据,交换了数据之后,我们应该怎么样删除右子树的最小值的节点呢?有人提议说用递归删除,比如删除3,用4进行替换,我们删除得先找到这个数据吧,关键问题是根本找不到这个数据,因为交换了数据之后树的结构算是被轻微破坏了,所以我们想要删除就让它的父节点指向空就可以了,此时也要判断一下相对位置即可,总体删除代码如下:

	bool EraseKey(const T& val)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//先找到 找到该节点才能删除
		while (cur)
		{
			if (cur->_key < val)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > val)
			{
				parent = cur;
				cur = cur->_left;
			}
			//找到了 开始删除
			else
			{
				///第一种情况:左为空 -> 都为空
				if (cur->_left == nullptr)
				{
					//删除根节点的时候
					if (cur == _root)
					{
						cur = cur->_right;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
				}
				///第二种情况:右为空
				else if (cur->_right == nullptr)
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
				///左右都不为空 -> 替换法
				else
				{
					Node* rightMinParent = cur;
					Node* rightMin = cur->_right;
					//找右子树的最小值
					while (rightMin->_left)
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}
					swap(rightMin->_key, cur->_key);


					if (rightMinParent->_left == rightMin)
						rightMinParent->_left = rightMin->_right;
					else
						rightMinParent->_right = rightMin->_right;
				}
				return true;
			}
		}
		return false;
	}

最后父节点也可以直接指向空的,但是为了代码的美观性,这样写也不是不行。


感谢阅读!

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值