深入浅出C++ ——二叉搜索树

一、二叉搜索树概念

  二叉搜索树又称二叉排序树/二次查找树,它是一棵空树或者是每颗子树都具有以下性质的二叉树

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

在这里插入图片描述

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

二、二叉搜索树操作

1. 二叉搜索树的查找

  • 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找
  • 最多查找高度次,走到到空,还没找到,这个值不存在。

2. 二叉搜索树的插入

  • 树为空,则直接新增节点,赋值给root指针。
  • 树不空,按二叉搜索树性质查找插入位置,插入新节点

3. 二叉搜索树的删除

  1. 左为空,父亲指向他的右。 也就是说左子节点为空,让它父节点指向该节点的右子节点,再直接删除该节点。
  2. 右为空,父亲指向他的左。 也就是说右子节点为空,让它父节点指向该节点的左子节点,再直接删除该节点。
  3. 左右子节点都不为空时,使用替换法删除

  在第一节的例子中,删除1、4、7、13、14、10节点属于前两种情况,删除3、8、10、6节点属于第三种情况。


替换法删除

  找到左子树的最右节点或者右子树的最左节点,替换该节点赋值给删除节点,直接删除替换节点,因为替换节点没子节点或者只有一个子节点,再归类到前两种情况。


修改

   K模型的搜索二叉树不支持修改,增删查的时间复杂度为O(h),h是树的高度,最坏的情况是h为N。


三、二叉搜索树的实现

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。 该种方式在现实生活中非常常见,例如:刷卡进楼。
template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		: _left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};
template<class K> //key
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* parent = nullptr;	//这里父节点指针给nullptr不会报错,因为考虑极端情况只有一个节点,那也会进入下面的循环中,执行parent = cur;,所以父节点->不会报错
		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->_right = cur;
		else
			parent->_left = cur;
		return true;
	}

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

	bool Erase(const K& key)
	{
		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						//找到了
			{
				//开始删除
				//1、左为空 ,父亲指向它的右 ,把另一个孩子托管给父亲
				//2、右为空 ,父亲指向它的左
				//3、左右都不为空
				if (cur->_left == nullptr) // 左为空 ,父亲指向它的右
				{
					if (cur == _root)//考虑极端情况删除的是_root根节点的情况
					{
						_root = cur->_right; //左为空,更新根
					}
					else
					{
						if (cur == parent->_left) //判断该节点是父亲的左还是右
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //右为空 ,父亲指向它的左
				{
					if (cur == _root)//考虑极端情况删除的是_root根节点的情况
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
					delete cur;
					cur = nullptr;
				}
				else//左右都不为空
				{
					//	替换法删除,替换的节点可以是左树的最大节点或者是右树的最小节点
					//	这里采用 找到右树的最小节点进行替换
					Node* minParent = cur; //	考虑极端情况,要删除的是根节点,所以minparent不可以为空
					Node* min = cur->_right;
					while (min->_left)
					{
						minParent = min;
						min = min->_left;
					}
					swap(cur->_key, min->_key);
					if (minParent->_left == min)  //判断minParent和min的关系
					{
						minParent->_left = min->_right;
					}
					else
						minParent->_right = min->_right;

					delete min;
					min = nullptr;
				}
				return true;
			}
		}
		return false;
	}

	void InOrder()//中序遍历打印
	{
		_InOrder(_root);//套用了一层,第一次传过去了_root,后面递归就传他的子树
		cout << endl;
	}

	bool FindR(const K& key)//递归版本的查找
	{
		return _Find(_root, key);
	}

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

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
	
	BSTree() = default;//C++11 强制编译器生成默认的构造

	~BSTree()
	{
		_Destsory(_root);
	}

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	// 传值传参
	BSTree<K>& operator = (BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

private:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* copyRoot = new Node(root->_key);
		copyRoot->_left = _Copy(root->_left);
		copyRoot->_right = _Copy(root->_right);
		return copyRoot;
	}
	void _Destsory(Node*& root)
	{
		if (root == nullptr)
			return;
		_Destsory(root->_left);
		_Destsory(root->_right);
		delete root;
		root = nullptr;
	}
	//_InOrder(_root)   递归调用不能写这个,不然每次传过去都是根节点,只能第一次传递根节点,用一个形参cur来接收根节点,
	void _InOrder(Node* cur) //递归必须 显示 的传递子树,而在外面调用的时候要传根节点_root,但是拿不到私有的_root。可以写一个getroot函数去拿到_root,或者像这样
	{
		if (cur == nullptr)
			return;
		_InOrder(cur->_left);
		cout << cur->_key << " ";
		_InOrder(cur->_right);
	}

	bool _Find(Node* cur, const K& key)
	{
		if (cur == nullptr)
			return false;

		if (cur->_key < key)
			return _Find(cur->_right, key);

		else if (cur->_key > key)
			return _Find(cur->left, key);

		else
			return true; //return true 按顺序依次从开辟的栈帧返回到最外层
	}

	bool _InsertR(Node*& cur, const K& key)
	{
		if (cur == nullptr)
		{
			cur = new Node(key); //cur是一个局部变量,所以要用引用
			return true;
		}
		if (cur->_key < key)
			return _InsertR(cur->_right, key);

		else if (cur->_key > key)
			return	_InsertR(cur->_right, key);

		else
			return false; //天然去重

	}
	bool _EraseR(Node*& cur, const K& key)
	{
		if (cur == nullptr)
			return false;

		if (cur->_key < key)
			return _EraseR(cur->_right, key);

		else if (cur->_key > key)
			return _EraseR(cur->_left, key);

		else  //删除
		{
			Node* del = cur;
			if (cur->_left == nullptr) //左为空
			{
				cur = cur->_right;     
			}
			else if (cur->_right == nullptr) //右为空
			{
				cur = cur->_left;
			}
			else              //左右都不为空
			{
				//找右树的最左节点
				Node* min = cur->_right;
				while (min->_left)
				{
					min = min->_left;
				}
				swap(cur->_key, min->_key);
				return _EraseR(cur->_right, key); //从这个点的右树开始删除key
			}
			delete del;
			del = nullptr;
			return true;
		}
	}
	Node* _root = nullptr;
};

应用:

int main()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto& e : a)
	{
		t.Insert(e);
	}
	//排序+去重
	t.InOrder(); 
	t.Erase(3);
	t.InOrder();
}

  1. KV模型:每一个关键码key,都有与之对应的值Value,即<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:
	bool Insert(const K& key, const V& value)
	{
		if (_root == nullptr)
		{
			_root = new Node(key, value);
			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, value);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

	bool Erase(const K& key)
	{
		//...
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << ":" << root->_value << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

应用:

int main()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		//BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.InOrder();
}

四、二叉搜索树的性能分析

  插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

  但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

  • 最优情况下,二叉搜索树为完全二叉树,或者接近完全二叉树,其平均比较次数为:log2 N
  • 最差情况下,二叉搜索树退化为单支树,或者类似单支,其平均比较次数为:N

  如果退化成单支树,二叉搜索树的性能就失去了。将二叉搜素树改进为AVL树和红黑树,不论按照什么次序插入关键码,性能都能达到最优。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++ STL中没有直接提供二叉搜索树的实现,但STL中有一些关于的容器,比如set和map,它们底层的实现就是基于红黑(一种平衡二叉搜索树)的。你可以使用这些容器来实现二叉搜索树的功能。关于二叉搜索树的一些知识,比如二叉的遍历、迭代、线索二叉、堆、Huffman编码、AVL等都可以在STL中找到相应的实现。 二叉搜索树的查找可以通过比较根节点的值和目标值的大小来判断是往左子还是往右子查找,并重复这个过程直到找到目标值或者遍历到叶子节点为止。常规实现使用循环来实现查找,递归实现使用递归函数来查找。 二叉搜索树的插入操作也可以通过递归或循环来实现,根据目标值和当前节点的值的大小关系来决定是往左子还是往右子插入新节点。 STL中的二叉搜索树容器如set和map提供了插入、删除和查找等功能,并且保持了二叉搜索树的性质。你可以使用这些容器来处理二叉搜索树相关的操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++ STL 数据结构 ](https://download.csdn.net/download/xinxipan/3008948)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [【C++ STL】-- 二叉搜索树](https://blog.csdn.net/weixin_64609308/article/details/128018280)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员Andrew

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

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

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

打赏作者

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

抵扣说明:

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

余额充值