最全【数据结构】二叉搜索树剖析(附源码)_二叉查找树的分析,资深大牛带你了解源码

img
img

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

需要这份系统化资料的朋友,可以戳这里获取

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


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+删除的是头结点。

在这里插入图片描述
用一个例子将上面几种情况都解释一下,假设现有一颗{63,55,90,42,58,60,70,10,42,67,83}的树。

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

情况一 :我们以删除63为例:
1. 要找到替换的节点,我们就要找到90这颗子树的最小节点,再递归下去就是找到70这颗子树的最小节点…也就是最终的67这个节点,然后我们可以覆盖式的修改63。
在这里插入图片描述
2. 然后我们把问题就可以缩小成为,删除底下的67节点,这个节点一定具备只有右孩子/或没有孩子节点,也就是情况2和情况3,而这两种情况都可以合成一种解决方案:
让被删除节点67(最底下的67)的父节点(70)的左孩子指针指向67的右孩子

在这里插入图片描述

情况一 第二种情况
删除55的情况:
这也是左右孩子都有的情况,但和前一个例子不同,它的替换节点就是它的右孩子,这个时候要删除,对于新的要删除58节点,它的删除方式就是让父节点58的右孩子指针指向它的右孩子
在这里插入图片描述

情况二
删除90的情况:
我们可以用父节点的右孩子指针指向70即可,注意:倘若删除节点在父节点的什么位置,就让父节点对应的左右指针指向删除节点的非空节点。
在这里插入图片描述

情况三:
删除83的情况:虽然把他单独分类成一种情况,但是它是可以用情况二的逻辑去处理的,让70判断83在左还是右子树,然后在用指针指向83的左孩子或者右孩子都可以。
在这里插入图片描述
情况四
以{1,2,3,4}为例子,当我们删除1的时候:
倘若按照情况二的方式处理,那么我们会有一个parent为NULL,并且链接2的情况,所以实际上我们要在情况二的条件判断加多一个parent指针是否为NULL的情况。
解决方案:实际上让删除节点的左或者右孩子成为新的头结点就可以了。
在这里插入图片描述
非递归版本:
代码对各种情况进行了标识,结合上图理解。

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

	}


递归版本:
递归版本采用头结点传引用有妙用
在这里插入图片描述

对于情况1,我们可以将情况划分,当我们在删除63的时候,实际上可以划分为将67赋值给63,并且在90这颗子树里面删除67
对于情况2/3,传引用我们就可以不需要头结点了,因为后面传参的节点都是前面的引用。
对于情况4,在传引用的情况下,实际上也解决了。

情况1图解:
在这里插入图片描述

情况2/3图解:
在这里插入图片描述
情况4图解:
在这里插入图片描述

	void DeleteNodeR(Node\*& root, const K& val)
	{
		//删除前先递归找到该节点
		if (root == nullptr)
			return;

		if (root->_val < val)
		{
			DeleteNodeR(root->_right, val);
		}
		else if (root->_val>val)
		{
			DeleteNodeR(root->_left, val);
		}
		else
		{
			Node\* del = root;
			
			if (root->_left == nullptr)
			{
			//情况2/3/4
				//传引用的妙用
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
			//情况2/3/4
				root = root->_left;
			}
			else
			{
			//情况1
				Node\* cur = root->_right;
				while (cur->_left)
				{
					cur = cur->_left;
				}
				//swap(cur->\_val, root->\_val); -->破坏了树的结构
				root->_val = cur->_val;
				DeleteNodeR(root->_right, cur->_val);
				return;
			}
			delete del;

		}
	}

	void DeleteNodeR(const K& val)
	{
		DeleteNodeR(_root, val);
	}

总结:
虽然都递归的写法要比迭代的写法要短,但是我们发现若在情况1当中,我们找到了替换节点,实际上一步就可以把他删除(迭代写法一步就删除了),但是采用递归的话又需要重新在子树当中去找到新的删除的节点,所以实际上没有迭代的写法优。
ps:
并且递归的弊端还有:栈帧的开销,栈溢出的危险。

5.默认成员函数编写


5.1、构造函数
	//构造函数
	BSTree() = default;

5.2、拷贝构造

拷贝构造当中我们用先序遍历的递归构造一颗树。
BSTree(const BSTree<K>& bs)//const BSTree& bs:默认成员函数中的传参可以不指定模板类型。

//拷贝构造
	Node\* Copy(Node\* copyroot)
	{
		if (copyroot == nullptr)
			return nullptr;

		Node\* root = new Node(copyroot->_val);
		root->_left = Copy(copyroot->_left);
		root->_right = Copy(copyroot->_right);

		return root;
	}
	BSTree(const BSTree<K>& bs)//const BSTree& bs
	{
		_root = Copy(bs._root);
	}

5.3、赋值重载

img
img

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

需要这份系统化资料的朋友,可以戳这里获取

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

BSTree& bs)//const BSTree& bs
{
_root = Copy(bs._root);
}


#### 5.3、赋值重载



[外链图片转存中...(img-DpOnkXUn-1715474092779)]
[外链图片转存中...(img-ZGzHUeb1-1715474092779)]

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

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


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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值