c++数据结构之二叉搜索树

一.概念

二叉搜索树是一种特殊的二叉树,左子树上所有节点的值,一定小于等于根节点,右子树上所有节点的值,一定大于等于根节点。

二.关于复杂度的探讨

下面内容需要有关二叉树复杂度的知识储备,如有需要,请看这篇文章。

https://blog.csdn.net/2401_87941709/article/details/155384131?fromshare=blogdetail&sharetype=blogdetail&sharerId=155384131&sharerefer=PC&sharesource=2401_87941709&sharefrom=from_linkhttps://blog.csdn.net/2401_87941709/article/details/155384131?fromshare=blogdetail&sharetype=blogdetail&sharerId=155384131&sharerefer=PC&sharesource=2401_87941709&sharefrom=from_link

以一颗普通二叉搜索树来说,假设一个极端情况,去找树里有没有5,那么此时就会比对h次,我们通过等比数列求和知道,h = log(N),所以按理来说最坏复杂度应该是log(N)

但这仅仅是一颗普通二叉树的最坏情况,如果二叉树本身结构就是一种非常极端的情况呢?

这时候还是去找5,依旧比对h次,但此时h与n直接近似,那么复杂度就退化成**O(N)**了,也就是说效率大大降低,二叉树直接退化到跟list,vector坐一桌了;更可气的是二叉树的实现比list和vector更为复杂,那么遇到这种情况,还不如用容器两兄弟来的实在。所以该如何解决这种效率低下的极端结构下的极端情况呢?

答案是平衡二叉搜索树和哈希,在后面的文章会讲到。

三.实现搜索二叉树

注:搜索章节里,模板参数不再叫作T,而是叫做K(key,关键字的意思),来代表关键字的类型

1.Insert

Ⅰ.相同值是否可插入的探讨

Ⅱ.不允许冗余(相同)插入的情况

遍历的过程既可用循环,也可用递归。但递归过深容易栈溢出,所以这里用循环。

要插入一个节点,首先比较它与当下父节点的大小,如果待插入节点大于父节点,往右子树走,如果小于,往左子树走。

复制代码
template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
public:
	bool Insert(const K& key)
	{
		//判空
		if (_root == nullptr)
		{
			_root = new(Node);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{//让cur去遍历,每遍历一次之前就把cur的值赋值给parent,然后自己接着向下遍历。反复操作,直至cur为空。此时就可以在parent处插入子节点了。
			if (key > cur->_key)
			{//待插入值大,往右子树走
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{//待插入值小,往左子树走
				parent = cur;
				cur = cur->_left;
			}
			else//不允许存在值相同的节点
			{
				return false;
			}
		}
		Node* newnode = new Node(key);
		if (key > parent->_key)
		{
			parent->_right = newnode;
		}
		else if (key < parent->_key)
		{
			parent->_left = newnode;
		}
		return true;
	}
private:
	Node* _root = nullptr;
};

2.中序遍历

代码写好之后,_root和函数_Inorder()都是私有的,在使用上就很不方便,此时就有个小巧思,再在public区域封装个套壳函数:Inorder(),毕竟public区域里成员函数既可以去使用private区域的成员,也可以在类外被调用。

有个有趣的现象,二叉搜索树走中序遍历后的序列是顺序的。

复制代码
	void Inorder()
	{//套壳函数,方便访问private的成员。
		_Inorder(_root);
	}
private:
	void _Inorder(Node* root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_Inorder(root->_left);
		cout << root->_key << " ";
		_Inorder(root->_right);
	}
	Node* _root = nullptr;

3.查找

复制代码
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

上面三位都是弟弟,接下来好戏才开场。

4.正式拷打------删除

由易到难分为三种情况:

假设待删除的节点为N。

①.N是叶子节点,直接释放,并让N父节点指向N的指针指向空。

②.N只有一颗子树(无论左右),改变N父节点对应子节点指针,使其指向N的子节点。

③.N左右两颗子树都存在,此时若想效仿②中操作是行不通的,二叉树一个父节点只能有两个子节点,此时若N父节点除N外,还有一颗子树,那么将N的两颗子树接给N父节点后,它就有了三颗子树,是不符合二叉树定义的。

解决方案:替换法。假设替换节点为R。R为N左子树的最右节点(左子树里最大的节点),或是N右子树的最左节点(右子树最小)中任意一个,这能保证替换,删除完以后整颗树还是二叉搜索树。

再来说说替换,将N与R节点的值交换,然后再释放掉R节点,并让R的父节点指向R节点的指针指向空。

图例:

删除的代码实现

遍历

先让cur在树里遍历(cur初始为_root,cur->_key与key比对,key小于_key,cur向左;key大于_key,向右),把每次向下遍历前的cur赋值给parent,然后cur向下遍历,当cur指向节点的值与key相等,就找到了要删除的目标。

删除

①.待删除节点cur只有一颗子树(将待删除的叶子节点也划归于此)

先判断cur的子树哪边是空。

再判断cur究竟是parent的左子节点还是右子节点。

然后将指向parent子节点的指针指向cur的子节点。

删除根的极端情况:

此时需要改变_root的指向,让其指向值为3的节点(根节点变成3)

②.cur有两颗子树

先确定替换节点replace,此处用cur右子树最左节点

找到replace向下找到合适位置后,替换加删除。

复制代码
else
{
				//找替换节点,这里找cur右子树的最左节点。
				Node* replace = cur->_right;
				Node* replaceParent = nullptr;//须有替换节点的父节点,不然后面删除操作搞不了。
				while (replace->_left)
				{
					replaceParent = replace;
					replace = replace->_left;
				}
				//替换
				cur->_key = replace->_key;
				//删除
				replaceParent->_left = replace->_right;
				delete replace;
}
问题分析

but...man,what can i say?删除根节点8还是走不通,还空指针引用了????

来结合图和代码逻辑分析一下:

由于8这个根节点不满足两个向下遍历的if条件,那么cur = 根节点,进入删除分支以后,由于根节点右子树最左节点**(replace->_left)为空** ,那么replace和replaceparent不进while循环,那么replaceparent无法更新,还是空指针。 那么执行删除操作时,replaceparent->_left就是引用空指针。

解决方案

将replaceparent的初始值设为cur,而非nullptr。

同时还有一个问题:

并非所有replace都是replaceparent的左子节点。

需要去判断一下replace的位置:

相关推荐
郝学胜_神的一滴18 小时前
CMake 30:循环语法全解|foreach_while双循环精讲、迭代技巧与实战避坑指南
c++·cmake
刘马想放假2 天前
Modbus 全栈技术解析:TCP、RTU、ASCII、RTU over TCP
数据结构·网络协议
北域码匠3 天前
冒泡排序太慢?鸡尾酒排序双向优化,原生 C# 零第三方库完整代码
数据结构·排序算法·泛型·c# 算法·鸡尾酒排序·原生 c# 开发·冒泡排序优化·嵌入式算法
卷无止境3 天前
C++ 的Eigen 库全解析
c++
卷无止境3 天前
现代 C++特性大盘点:一门脱胎换骨的老语言
c++·后端
郝学胜_神的一滴3 天前
CMake 27:缓存变量的特性、语法、类型与实操全解
c++·cmake
博客18005 天前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴5 天前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
众少成多积小致巨6 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
Darling噜啦啦9 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试