数据结构篇八:二叉搜索树

本文围绕二叉搜索树展开,介绍了其概念、操作,包括查找、插入和删除等。详细阐述了插入、查找和删除的迭代与递归实现方式,还说明了两种结构K模型和KV模型。对二叉搜索树的性能进行分析,指出最优和最差情况,并提及后续可通过AVL树和红黑树改进。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

  前面我们已经学习过了二叉树,二叉搜索树是在二叉树的基础上增添了一些规则,使其能完成快速查找的功能,同时它也帮助我们更好的理解后续将要学习的map和set。

1. 二叉搜索树

1.2 二叉搜索树的概念

  二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
  • 它的左右子树也分别为二叉搜索树。

2.2 二叉搜索树操作

在这里插入图片描述

  1. 二叉搜索树的查找
    a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找
    b、最多查找高度次,走到到空,还没找到,这个值不存在。
  2. 二叉搜索树的插入
    插入的具体过程如下:
    a. 树为空,则直接新增节点,赋值给root指针。
    b. 树不空,按二叉搜索树性质查找插入位置,插入新节点。
    在这里插入图片描述
  3. 二叉搜索树的删除
    首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
    a. 要删除的结点无孩子结点
    b. 要删除的结点只有左孩子结点
    c. 要删除的结点只有右孩子结点
    d. 要删除的结点有左、右孩子结点

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
  情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
  情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
  情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题–替换法删除。
在这里插入图片描述  此部分会在实现小节进行详细讲解。

2. 二叉搜索树的实现

2.1 插入

2.1.1 迭代插入

  插入十分简单,新插入值比当前节点的值小就进入它的左子树,比它大就进入它的右子树,相等就返回false,说明这个值已经存在,无需插入,(注意:二叉搜索树一般是不会存储重复值的) 遇到空节点就该进行插入了。但是在进行链接时需要注意,我们的结构并不是三叉链结构,因此对于一个节点是无法找到它的父亲节点的,因此在比较时还需要保存父亲节点,方便进行链接操作。

bool insert(const K& key)
	{
   
   
		if (_root == nullptr)
		{
   
   
			_root = new Node(key);
		}
		
		//保存父亲节点
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
   
   
			//确保每一次循环结束parent就是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;
	}

2.1.2 递归插入

  递归需要传入根节点,但是在类外面我们无法传入根节点,因为它的类的私有成员变量,因此我们可以套一层,在类里面我们是可以访问根节点的。递归插入更加简单,只需要在递归到空的时候直接开一个新结点给root就可以了,因为我们是引用传值,不需要保存父亲节点,直接就可以链接上了。

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

bool _InsertR(Node*& root, const K& key)
{
   
   
	if (root == nullptr)
	{
   
   
		root = new Node(key);
		return true;
	}

	if (root->_key > key)
	{
   
   
		_InsertR(root->_left, key);
	}

	if (root->_key < key)
	{
   
   
		_InsertR(root->_right, key);
	}

	return false;
}

2.2 查找

2.2.1 迭代查找

  查找也比较简单,被查找节点的值比当前节点的值小就进入它的左子树,比它大就进入它的右子树,相等就返回true或者返回该节点的指针。

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;
	}

2.2.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;

	if (_FindR(root->_left, key))
		return true;

	if (_FindR(root->_right, key))
		return true;

	return false;
}

2.3 删除

2.3.1 迭代删除

  删除的情况有很多,我们分开来看。

2.3.1.1 情况一

在这里插入图片描述
  这种情况下直接删除就好了,因为没有孩子节点,并不会有什么影响。

2.3.1.2 情况二

在这里插入图片描述  删除该节点,并将它的孩子节点链接到它的父亲节点。如果被删除节点是父亲的右孩子,就将被删除节点的孩子链接到父亲的右子树上;同理,如果被删除节点是父亲的左孩子,就将被删除节点的孩子链接到父亲的左子树上。原因很好理解,父亲节点的右子树都是比它大的,左子树都是比它小的,大的值自然要链接到右边,小的值自然要链接到左边。
在这里插入图片描述

2.3.1.3 情况三

在这里插入图片描述
  我们发现如果删除该节点就会打乱它的左右子树,打破二叉搜索树的规则。自然我们可以选择将所有值(除了被删除节点)重新构造一个新的二叉搜索树出来,但是这样太麻烦了。我们可以用替换法来解决这个问题。
  替换法:我们可以找出一个节点,在不破坏二叉搜索树规则的前提下来代替被删除节点,我们通常可以找被删除节点的左子树的最右节点(左子树中最大的节点)或者右子树的最左节点(右子树中最大节点)来替换它,我采用的是找右子树的最左节点来解决问题,我们来看:
在这里插入图片描述
  我们此时就发现继续删除节点3就变成了我们前面所说的情况一,直接删除就可以了。

	bool Earse(const K& key)
	{
   
   
		Node* parent = nullptr;
		Node* cur = _root;
		while (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 && cur->_right == nullptr)
				{
   
   
					cur = nullptr;
				}
				//左为空或者右为空
				else if (cur->_right == nullptr || cur->_left == nullptr)
				{
   
   
					if (cur->_left == nullptr)
					{
   
   
						if (cur == _root)
						{
   
   
							_root = _root->_right;
						}
						else if (cur == parent->_left)
						{
   
   
							parent->_left = cur->_right;
						}
						else
						{
   
   
							parent->_right = cur->_right;
						}
					}
					else
					{
   
   
						if (cur == _root)
						{
   
   
							_root = _root->_left;
						}
						else if (cur == parent->_left)
						{
   
   
							parent->_left = cur->_left;
						}
						else
						{
   
   
							parent->_right = cur->_left;
						}
					}
				}
				//左右都不为空
				else
				{
   
   
					//替换法
					//找到右子树的最小节点

					Node* parent = cur;
					Node* subLeft = cur->_right;
					while (subLeft->_left)
					{
   
   
						parent = subLeft;
						subLeft = subLeft->_left;
					}

					swap(cur->_key, subLeft->_key);

					if (parent->_right == subLeft)
						parent->_right = subLeft->_right;
					else
						parent->_left = subLeft->_right;
				}
				return true;
			}
		}
		return 
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不如小布.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值