【数据结构】二叉搜索树剖析(附源码)_二叉查找树的分析

template
struct BSTreeNode
{
public:
BSTreeNode(const K& val = K())
:_left(nullptr)
, _right(nullptr)
, _val(val)
{}

BSTreeNode<K>\* _left;
BSTreeNode<K>\* _right;
K _val;

};


![在这里插入图片描述](https://img-blog.csdnimg.cn/18f49f865cc44e6cbb25551a0920ac35.png)


  
  



### 1、二叉排序树的插入




---


我们这里实现的是**不允许键值冗余**的版本,插入节点需要注意:


* 插入节点一定是插入值为NULL节点的位置
* 插入节点后记得**父节点要与插入节点**连接
* 插入的节点比当前节点大往右子树走,反之往左子树走,相等返回false(插入失败)


**非递归版本:**



bool Insert(const K& val)
{
//找NULL节点插入
if (!_root)
{
_root = new Node(val);
return true;
}
Node* newnode = new Node(val);
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_val == val)
{
//实现的不允许键值冗余
return false;
}
else
{
parent = cur;
cur = cur->_left;
}
}
//插入节点与父节点相连
if (parent->_val > val)
{
parent->_left = newnode;
}
else
{
parent->_right = newnode;
}
return true;
}


**递归版本:**  
 外层的InsertR是给外层调用的,由于递归要有头结点,所以我们设置子函数\_InsertR来完成递归。


以插入节点{63,55}为栗子,叙述一下传引用的妙用:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/043515cca040411685facb0c9b52b8a2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)


* 参数传引用`Node*& root`是一大妙用,上面栗子,我们拿插入63与55为例,当插入63的时候,由于root是\_root 的引用,那么相当于`_root = new Node(val)`。而插入55的时候,这个时候不需要再让父节点的指针链接,是因为`_InsertR(root->_left, val);`root是上一层root->\_left的引用,相当于站在上一层来看,`root->_left = new Node(val)`,这个用法是很不错的。当然若是理解不了,可以再传多一个参数来记录父指针。
* 迭代版本不使用引用是因为使用引用头结点会改变头结点的位置,而递归当中不会改变头结点的位置。



bool \_InsertR(Node\*& root, const K& val)
{
	//传引用的妙用
	if (root == nullptr)
	{
		root = new Node(val);
		return true;
	}

	if (root->_val < val)
	{
		return \_InsertR(root->_right, val);
	}
	else if (root->_val > val)
	{
		return \_InsertR(root->_left, val);
	}
	else
	{
		return false;
	}
}
bool InsertR(const K& val)
{
	//子问题:往子树上插入
	return \_InsertR(_root, val);
}



---


  
  



### 2、二叉搜索树的遍历


我们通常遍历二叉树使用中序遍历,因为遍历出来的结果恰好是升序的,我们这里可以用一个Inorder函数来验证我们插入节点是否有没有编写正确。  
 这里我们用递归遍历,编写起来简单。  
 **递归版本:**



void \_Inorder(Node\* root)
{
	if (root == nullptr)
		return;
	\_Inorder(root->_left);
	cout << root->_val << " ";
	\_Inorder(root->_right);
}
void Inorder()
{
	\_Inorder(_root);
	cout << endl;
}

  
  



### 3、二叉树的查找




---


由二叉搜索树的本身性质,我们要查找一个树的时候最多走O(logN)高度,当我们走到NULL节点前还没有找到该节点,就表示查找失败。


* 查找遍历树的逻辑与插入类似,比较简单,不做叙述。  
 **非递归版本:**



Node* Find(const K& val)
{
Node* cur = _root;
while (cur)
{
if (cur->_val < val)
{
cur = cur->_right;
}
else if (cur->_val > val)
{
cur = cur->_left;
}
else
{
return cur;//查找成功
}
}
return nullptr;//查找失败
}


**递归版本:**


* 这里root没必要传引用,要传也可以。



Node\* FindR(Node\* root,const K& val)
{
	if (root == nullptr)
	{
		return false;
	}
	if (val < root->_val)
	{
		FindR(root->_left, val);
	}
	else if (val > root->_val)
	{
		FindR(root->_right, val);
	}
	else
	{
		return root;
	}
}
Node\* FindR(const K& val)
{
	return FindR(_root, val);
}

  
  



### 4、二叉排序树的删除(难)




---


二叉排序树的删除分为以下几种情况:


* 情况1:删除的节点有左右孩子,1.1、这种情况我们通常采用**伪删除**的方式,我们可以在该节点的右子树找最小节点或者该节点的左子树找最大节点来代替,再删除这个替换上来的节点。1.2、以用右子树的最小孩子来替换为例,注意该情况还分为右孩子的最左孩子不为NULL(右子树的最小节点),和右孩子的最左孩子为NULL的两种情况。
* 情况2:删除的节点只有左孩子/右孩子,让父亲指向该节点的左孩子/右孩子。
* 情况3:删除的节点没有孩子,父亲的原先指向该节点的指针置为NULL。
* 情况4:删除的节点是情况2或情况3+删除的是头结点。


![在这里插入图片描述](https://img-blog.csdnimg.cn/5110326a954944be9e2810a968a05e42.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)  
 用一个例子将上面几种情况都解释一下,假设现有一颗{63,55,90,42,58,60,70,10,42,67,83}的树。


**下面图中红色圈起来为要删除节点,绿色圈起来为替换节点**


**情况一** :我们以删除63为例:  
 **1.** 要找到替换的节点,我们就要找到90这颗子树的最小节点,再递归下去就是找到70这颗子树的最小节点…也就是最终的67这个节点,然后我们可以覆盖式的修改63。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/b3b80718b0c6407682836354f206fefe.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)  
 **2.** 然后我们把问题就可以缩小成为,删除底下的67节点,这个节点一定具备**只有右孩子/或没有孩子节点**,也就是情况2和情况3,而这两种情况都可以合成一种解决方案:  
 让被删除节点67(最底下的67)的**父节点(70)的左孩子指针**指向**67的右孩子**。


![在这里插入图片描述](https://img-blog.csdnimg.cn/f3f099cd6e6547fcbc3ddaa38d60ca58.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)


**情况一 第二种情况**:  
 删除55的情况:  
 这也是左右孩子都有的情况,但和前一个例子不同,它的替换节点就是它的右孩子,这个时候要删除,对于新的要删除58节点,它的删除方式就是让**父节点58的右孩子指针指向它的右孩子**。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/74fa9960013747fe8c20fa0188db89e8.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)


**情况二**:  
 删除90的情况:  
 我们可以用父节点的右孩子指针指向70即可,注意:倘若删除节点在父节点的什么位置,就让父节点对应的左右指针指向删除节点的非空节点。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/471bb77ec47a47caa8109c52aa34b9cd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)


**情况三:**  
 删除83的情况:虽然把他单独分类成一种情况,但是它是可以用情况二的逻辑去处理的,让70判断83在左还是右子树,然后在用指针指向83的左孩子或者右孩子都可以。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/10e83d5d88ce4e90be3bdb9e7eaaaab1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)  
 **情况四**:  
 以{1,2,3,4}为例子,当我们删除1的时候:  
 倘若按照情况二的方式处理,那么我们会有一个parent为NULL,并且链接2的情况,所以实际上我们要在情况二的条件判断加多一个parent指针是否为NULL的情况。  
 解决方案:实际上让删除节点的左或者右孩子成为新的头结点就可以了。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/dec283f1b92949d2847f3d692382d5e0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)  
 **非递归版本:**  
 代码对各种情况进行了标识,结合上图理解。



void DeleteNode(const K& val)
{
if (_root == nullptr)
return;
//删除前先找到该节点
Node* parent = nullptr;
Node* cur = _root;

	while (cur)
	{
		if (cur->_val < val)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_val > val)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			break;
		}
	}
	if (cur == nullptr)
		return ;//找不到该节点
	//对该节点的三种状态进行判断
	
	//情况2或情况3\*\*
	if (cur->_left == nullptr)
	{
		//情况4\*\*
		//特殊:删除头结点--> 
		if (cur == _root)
		{
			_root = cur->_right;
		}
		else if (cur == parent->_left)
		{
			parent->_left = cur->_right;
		}
		else
		{
			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
		{
			parent->_right = cur->_left;
		}
		delete cur;
	}
	else
	{
		//这里是不涉及parent指针的解引用行为,所以不用考虑更换头结点
		//左右两个节点都存在
		//分两种:右孩子就是最小/和不是最小

		//情况1\*\* 
		Node\* delParent = cur;
		Node\* delcur = cur;
		delcur = delcur->_right;
		while (delcur->_left)
		{
			delParent = delcur;
			//找右子树的最小和左子树最大都可以
			delcur = delcur->_left;
		}
		if (delParent == cur)
		{
			//1.1
			swap(delcur->_val, delParent->_val);
			cur->_right = delcur->_right;
			delete delcur;
		}
		else
		{
			//1.2
			swap(cur->_val, delcur->_val);
			delParent->_left = delcur->_right;
			delete delcur;
		}
	}

}



---


**递归版本:**  
 递归版本采用头结点传引用有妙用  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5110326a954944be9e2810a968a05e42.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)





![img](https://img-blog.csdnimg.cn/img_convert/1971115bbaea7313172da4de7fa4e75e.png)
![img](https://img-blog.csdnimg.cn/img_convert/18e5f1dd299827c059a67fcd3d284336.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAXmpoYW9e,size_20,color_FFFFFF,t_70,g_se,x_16)





[外链图片转存中...(img-Yj1P8MEE-1714455530899)]
[外链图片转存中...(img-2IX8dKWC-1714455530899)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值