【C++】二叉搜索树

发布于:2025-04-21 ⋅ 阅读:(94) ⋅ 点赞:(0)

1.概念

二叉搜索树又称二叉排序树,可以是一棵空树,不为空具有以下性质:

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

2.实现

2.1创建节点

在这里插入图片描述

2.2查找

在这里插入图片描述
创建cur来代替_root来循环遍历
1.从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
2.最多查找高度次,走到到空,还没找到,这个值不存在。

递归版本

在私有域中封装具体实现细节,在公共域中提供接口使用,作为成员函数在类中可直接访问私有成员变量。

在这里插入图片描述
在这里插入图片描述
递归的终止条件是1.找到空节点(失败) 2.找到key所在节点(成功)
若当前的key值小于目标key值,则递归地在当前节点的右树中查找;反之在左树查找。不断缩小搜索范围,直到找到目标key值或搜索范围为空。

2.3插入

bool Insert(const k& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	//进行左右子树遍历找到空节点处插入
	//保存父节点为了最后链接新节点
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		//节点值在搜索二叉树中只有唯一存在
		else
		{
			return false;
		}
	}
	//链接新节点
	cur = new Node(key);
	//判断链接在父节点的左边还是右边
	if (parent->_key > key)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	return true;
}

创建cur来代替_root来循环遍历,提前保存父节点
1.树为空,则直接新增节点,赋值给root指针,插入成功
2.树不空,按二叉搜索树性质查找插入位置,插入新节点。若找到key值相同的节点就插入失败,因为二叉搜索树性质决定不允许有相同key值的节点同时存在。
3.找到空节点后创建新节点,注意记得用父节点链接上形成搜索二叉树,需要判断链接在父节点的左边还是右边。

递归版本

还是在私有域封装实现细节,公共域提供接口使用,作为类的成员函数可直接访问私有成员。

在这里插入图片描述
在这里插入图片描述
递归结束条件为找到空节点位置,创建新节点进行链接。
这里的神来之笔是Node*&root,每次递归下去的是当前节点指针的引用(),这样就可以省略提前保存父节点指针的步骤,直接进行链接即可root永远是父亲右指针和左指针的别名,这样就不用判断重新链接在父指针的左还是右。

为什么循环不能传指针的引用?
引用的问题是不能改变指针指向,循环中没办法使用,递归可以因为每次都会创建新的栈帧,不同域中可以定义同名变量,每个栈帧中的引用都不同,每次递归都创建新的引用

2.4删除

删除时有三种情况
1.删除节点没有孩子
2.删除节点只有一个孩子
3.有两个孩子
解决办法:
a:1和2属于直接删除场景,删除后父节点重新链接即可(nullptr或子节点)
b:3用替换法,用左子树的最大节点(最右节点),或者右子树的最小节点(最左节点)。这样能确保交换根节点和要删除节点的key值后左子树key值依然小于根节点key值,右子树key值依然大于根节点key值,满足二叉搜索树性质。最后删除 要删除节点位置即可。

bool Erase(const k& key)
{
	Node* cur = _root;
	Node* parent = cur;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else//找到要删除的节点了
		{
			//先判断没有孩子或只有一个孩子的情况
			//删除节点的左树为空
			if (cur->_left == nullptr)
			{
				//检查删除节点是根节点
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					//判断在父节点的哪一边删除再链接
					if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}
			}
			//删除节点的右树为空
			else if (cur->_right == nullptr)
			{
				//处理跟=根节点为要删除节点情况
				if (cur == _root)
				{
					_root = cur->_left;
				}
				if (parent->_left == cur)
				{
					parent->_left = cur->_left;
				}
				else
				{
					parent->_right = cur->_left;
				}
			}
			//要删除节点有两个孩子情况
			else
			{
				//找替代节点,以左边为例
				Node* parent = cur;
				Node* leftmax = cur->_left;
				while (leftmax->_right)
				{
					parent = leftmax;
					leftmax = leftmax->_right;
				}
				swap(cur->_key, leftmax->_key);
				//删除后链接
				if (parent->_right == leftmax)
				{
					parent->_right = leftmax->_left;
				}
				if (parent->_left == leftmax)
				{
					parent->_left = leftmax->_left;
				}
				cur = leftmax;
			}
			delete cur;
			return true;
		}
	}
	return false;
}

代码逻辑:
1.创建cur来代替_root来循环遍历,提前保存父节点
2.找要删除节点的位置,比较当前位置key值与目标key值,不断更新父节点位置和cur节点位置,循环遍历下去,直到找到目标位置
3.依次分析删除的三种情况,注意检查删除节点是根节点的情况
在这里插入图片描述
需要将根节点更新到子树不为空的位置,这样父节点也跟着更新了(Node* parent = cur),若不更新父节点为空,会造成非法访问的问题
4.重点分析删除节点有两个孩子情况:
左边和右边情况分别创建leftmax或rightmax来记录替代节点,父节点赋值为cur,可以避免父节点为空跳过循环中父节点赋值而造成的非法访问问题。找到后交换两节点中key值,再进行删除后的重新链接

递归版本

public:
bool EraseR(const k& key)
{
	return _EraseR(_root, key);
}
private:
bool _EraseR(Node*& root, const k& key)
{
	if (root == nullptr)
	{
		return false;
	}
	//寻找要删除的节点
	if (root->_key < key)
	{
		return _EraseR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _EraseR(root->_left, key);
	}
	else
	{
		//提前保存交换后要删除的根节点
		Node* del = root;
		//删除节点后的重新链接
		//左为空
		if (root->_left == nullptr)
		{
			root = root->_right;
		}
		//右为空
		else if (root->_right == nullptr)
		{
			root = root->_left;
		}
		//左右都不为空
		else
		{
			Node* leftmax = root->_left;
			while (leftmax->_right)
			{
				leftmax = leftmax->_right;
			}
			swap(leftmax->_key, root->_key);
			return _EraseR(root->_left, key);
		}
		delete del;
		return true;
	}
}

递归终止条件1.根节点为空返回false 2.删除成功返回true
代码逻辑:
1.递归查找要删除的节点
2.当前key值等于目标key值,进行删除逻辑:
保存要删除节点到临时指针del
然后重新链接树,左子树为空,将当前节点右子树链接到父节点;右子树为空,将当前节点左子树链接到父节点;左右子树都不为空时,找到左子树的最大节点(最右节点),或右子树的最小节点(最左节点),将该节点key值与根节点key值交换,然后递归的在根节点的左子树中删除目标key值,这样目标key值所在节点从有两个孩子变成没有孩子

辨析:
return _EraseR(root->_left, key);第一个参数若用leftmax
在这里插入图片描述
删除接口传参Node*& root,用的指针的引用,传下来的是节点指针的引用,所以不需要提前保存父节点进行重新链接。但此处传leftmax指针的引用实际上是指向1节点指针的引用(当前节点指针的引用),将3这个节点删除后8节点指针指向没有改变,导致结构混乱。应该传root->_left,指向3节点,发现3的右节点为空直接指向1节点,正确完成删除

在这里插入图片描述

感受指针引用的妙处

以右子树为空的情况分析:正常情况下,root是父亲右指针或左指针的别名,直接更改父节点指向,上文已分析过
在这里插入图片描述
当要删除节点为空节点时,此时root为_root的别名,同样可以直接更改_root的指向

2.5复制

在这里插入图片描述
拷贝构造函数使用参数 t 来获取原对象的数据,然后根据这些数据构造一个新对象,新对象在内存中是完全独立的副本。复用copy函数实现深拷贝
在这里插入图片描述
递归终止条件1.传入节点为空无需复制返回空指针 2.每次递归调用中创建一个key值与原树相同的节点
通过后序遍历递归复制左子树和右子树,在递归过程中创建节点,在返回过程中链接形成树型结构

2.6销毁

在这里插入图片描述
采用后序遍历的方式销毁二叉树,释放当前节点占用内存后,记得将root指针置空
在这里插入图片描述
析构函数复用销毁的实现

3.二叉搜索树的应用

key搜索模型:

K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。上述代码实现就是该模型的应用
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

key_value模型:

每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

namespace key_value
{
	template<class k, class v >
	struct BSTreeNode
	{
		BSTreeNode<k,v>* _left;
		BSTreeNode<k,v>* _right;
		k _key;
		v _value;

		BSTreeNode(const k& key,const v&value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			,_value(value)
		{}
	};
	template<class k,class v>
	class BSTree
	{
		typedef BSTreeNode<k,v> Node;
	public:
		BSTree()
			:_root(nullptr)
		{
		}
		//封装一次可以通过类直接调用
		void _Inorder()
		{
			Inorder(_root);
			cout << endl;
		}
		Node* FindR(const k& key)
		{
			return _FindR(_root, key);
		}
		bool InsertR(const k& key,const v&value)
		{
			return _InsertR(_root, key,value);
		}
		bool EraseR(const k& key)
		{
			return _EraseR(_root, key);
		}
	private:
		void Inorder(Node* _root)
		{
			if (_root == nullptr)
			{
				return;
			}
			Inorder(_root->_left);
			cout << _root->_key << ":"<<_root->_value<<endl;
			Inorder(_root->_right);
		}
		bool _EraseR(Node*& root, const k& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			//寻找要删除的节点
			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				//提前保存交换后要删除的根节点
				Node* del = root;
				//删除节点后的重新链接
				//左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//左右都不为空
				else
				{
					Node* leftmax = root->_left;
					while (leftmax->_right)
					{
						leftmax = leftmax->_right;
					}
					swap(leftmax->_key, root->_key);
					return _EraseR(root->_left, key);
				}
				delete del;
				return true;
			}
		}
		bool _InsertR(Node*& root, const k& key,const v&value)
		{
			if (root == nullptr)
			{
				root = new Node(key,value);
				return true;
			}

			if (root->_key < key)
			{
				return _InsertR(root->_right, key,value);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key,value);
			}
			else
			{
				return false;
			}
		}
		Node* _FindR(Node* root, const k& key)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return root;
			}
		}
		Node* _root;
	};
}

实现思路与key模型基本相同,只是节点中多增加了_value成员变量,BSTree类中多增加了一个模板参数v,实现插入功能时要同时插入value值。

  • 测试代码
void testBSTree1()
{
	key_value::BSTree<string, string> dict;
	dict.InsertR("mint", "薄荷");
	dict.InsertR("forever", "永远");
	dict.InsertR("love", "相爱");

	string str;
	while (cin >> str)
	{
		key_value::BSTreeNode<string, string>* ret = dict.FindR(str);
		if (ret)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "无此单词" << endl;
		}
	}
}

输入CTRLz+换行结束输入,正常退出ctrlc直接关掉程序

性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
在这里插入图片描述
1.理想情况下,树为完全平衡的二叉树,高度为 O(log 2 N),此时增删查改操作的时间复杂度均为 O(logN)
2.若数据是有序或接近有序的,树可能会退化成链表,性能急剧下降,此时时间复杂度为 O(N)


网站公告

今日签到

点亮在社区的每一天
去签到