C++ 搜索二叉树

目录

1.二叉搜索树概念

[2. 实现=二叉搜索树](#2. 实现=二叉搜索树)

[2.1. 二叉搜索树的插入](#2.1. 二叉搜索树的插入)

2.2查找

2.3删除节点

3.二叉树的应用(KV结构)


1.二叉搜索树概念

**二叉搜索树又称二叉排序树,它或者是一棵空树,**或者是具有以下性质的二叉树:

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

2. 实现=二叉搜索树

这是一个二叉搜索树:

cpp 复制代码
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

定义结构+初始化列表:

cpp 复制代码
template<class K>
//定义节点
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
	//初始化
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

成员变量:

cpp 复制代码
template<class K>
class BSTree
{
    typedef BSTreeNode<K> Node;
	
private:
	Node* _root = nullptr;
};

2.1. 二叉搜索树的插入

(默认定义,搜索二叉树数据不允许冗余)

代码:

cpp 复制代码
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->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

	return true;
}

代码解释:

  1. 检查根节点是否为空 :如果根节点为空,则创建一个新节点作为根节点并返回true
  2. 遍历树以找到插入位置 :使用parentcur指针来遍历树,直到找到一个空位置(即curnullptr)或找到与key相等的节点。
  3. 处理重复键 :如果找到与key相等的节点,函数返回false,表示插入失败(因为BST中通常不允许重复的键)。
  4. 插入新节点 :在遍历结束后,根据parent节点的键值来决定新节点是应该作为左子节点还是右子节点。

测试用例:

此处中序遍历(有序),我们需要访问到_root,但root是私有的,要么提供一个get函数,或者把测试函数设置成友元(没必要),这里我们用的方法是将_InOrder函数套了一层,因为我们默认是从根开始访问:

cpp 复制代码
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	private:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}
	private:
		Node* _root = nullptr;


void BST1()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> t1;
	for (auto x : a)
	{
		t1.Insert(x);
	}
	t1.InOrder();
}

运行结果:


2.2查找

cpp 复制代码
	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 cur;
	}
  • while (cur): 开始一个循环,只要cur不是nullptr(即当前节点存在),就继续执行循环体。

    • if (cur->_key < key): 如果当前节点的键小于给定的键,

      • cur = cur->_right;: 移动到当前节点的右子节点。
    • else if (cur->_key > key): 如果当前节点的键大于给定的键,

      • cur = cur->_left;: 移动到当前节点的左子节点。
    • else: 如果当前节点的键与给定的键相等,

      • return cur;: 返回当前节点的指针。

测试用例:

cpp 复制代码
void BST1()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> t1;
	for (auto x : a)
	{
		t1.Insert(x);
	}
	t1.InOrder();
}

运行结果:


2.3删除节点

cpp 复制代码
		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
				{
					// 删除
					// 左为空,父亲指向我的右
					if (cur->_left == nullptr)
					{
						//if(parent == nullptr)
						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(parent == nullptr)
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							// 右为空,父亲指向我的左
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}

						delete cur;
					}
					else
					{
						// 左右都不为空,替换法删除
						// 
						// 查找右子树的最左节点替代删除
						Node* rightMinParent = cur;
						Node* rightMin = cur->_right;
						while (rightMin->_left)
						{
							rightMinParent = rightMin;
							rightMin = rightMin->_left;
						}

						swap(cur->_key, rightMin->_key);

						if (rightMinParent->_left == rightMin)
							rightMinParent->_left = rightMin->_right;
						else
							rightMinParent->_right = rightMin->_right;

						delete rightMin;
					}

					return true;
				}
			}

			return false;
		}
  1. 初始化 :
    • parent:用于追踪当前节点cur的父节点。
    • cur:从根节点_root开始遍历。
  2. 查找过程 :
    • 使用while循环遍历树,直到找到目标节点(键值为key的节点)或遍历到叶子节点的子节点为空。
    • 更新parentcur以指向正确的节点。
  3. 删除逻辑 :
    • 当找到目标节点时,有三种情况需要考虑:
      1. 目标节点没有左子节点(cur->_left == nullptr):
        • 如果目标节点是根节点,则将根节点设置为右子节点。
        • 否则,根据目标节点是父节点的左子节点还是右子节点,更新父节点的对应指针。
        • 释放目标节点的内存。
      2. 目标节点没有右子节点(cur->_right == nullptr):
        • 与情况1类似,但这次更新指向左子节点。
      3. 目标节点既有左子节点又有右子节点:
        • 在目标节点的右子树中找到最小的节点(即最左边的节点)。
        • 交换目标节点和最小节点的键值。
        • 删除右子树中的最小节点(现在具有目标节点的键值)。
  4. 返回值 :
    • 如果成功找到并删除了目标节点,则返回true
    • 如果遍历到叶子节点的子节点为空且未找到目标节点,则返回false

注意:

  • 在处理根节点时,直接比较cur == _root,这是正确的。但在注释掉的代码部分(//if(parent == nullptr)),检查parent是否为nullptr是不必要的,因为当cur是根节点时,parent始终为nullptr

测试用例:

cpp 复制代码
void BST3()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> t1;
	for (auto x : a)
	{
		t1.Insert(x);
	}
	t1.InOrder();
	cout << endl;
	t1.Erase(8);
	t1.InOrder();
}

运行结果:

3.二叉树的应用(KV结构)

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
  1. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方

式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对。
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

这是搜索二叉树的KV模型:

实际每个节点,就是多了一个与key关联的值,搜索二叉树的实现逻辑只是与key有关。

cpp 复制代码
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;

		// pair<K, V> _kv;

		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:
		// logN
		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 cur;
		}

		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
				{
					// 删除
					// 左为空,父亲指向我的右
					if (cur->_left == nullptr)
					{
						//if(parent == nullptr)
						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(parent == nullptr)
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							// 右为空,父亲指向我的左
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}

						delete cur;
					}
					else
					{
						// 左右都不为空,替换法删除
						// 
						// 查找右子树的最左节点替代删除
						Node* rightMinParent = cur;
						Node* rightMin = cur->_right;
						while (rightMin->_left)
						{
							rightMinParent = rightMin;
							rightMin = rightMin->_left;
						}

						swap(cur->_key, rightMin->_key);

						if (rightMinParent->_left == rightMin)
							rightMinParent->_left = rightMin->_right;
						else
							rightMinParent->_right = rightMin->_right;

						delete rightMin;
					}

					return true;
				}
			}

			return false;
		}

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

测试案例:使用BSTree<string, int>来统计一个字符串数组中各个字符串的出现次数。

遍历字符串数组arr,对于数组中的每一个字符串str

  1. 调用countTreeFind方法,查找该字符串是否已经存在于树中。
  2. 如果Find方法返回nullptr(或者类似表示未找到的值),说明这个字符串还没有被统计过,因此调用Insert方法将它插入到树中,并设置其值为1(表示这是第一次出现)。
  3. 如果Find方法返回了一个非nullptr的值(表示找到了对应的节点),说明这个字符串已经存在于树中,那么就将该节点对应的值(即出现次数)自增1。
cpp 复制代码
	void TestBSTree()
	{
		// 统计次数
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
		BSTree<string, int> countTree;
		for (const auto& str : arr)
		{
			auto ret = countTree.Find(str);
			if (ret == nullptr)
			{
				countTree.Insert(str, 1);
			}
			else
			{
				ret->_value++;
			}
		}

		countTree.InOrder();
	}
}

运行结果:

相关推荐
白榆maple13 分钟前
(蓝桥杯C/C++)——基础算法(下)
算法
JSU_曾是此间年少18 分钟前
数据结构——线性表与链表
数据结构·c++·算法
sjsjs1124 分钟前
【数据结构-合法括号字符串】【hard】【拼多多面试题】力扣32. 最长有效括号
数据结构·leetcode
此生只爱蛋1 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
何曾参静谧2 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
昂子的博客2 小时前
基础数据结构——队列(链表实现)
数据结构
咕咕吖2 小时前
对称二叉树(力扣101)
算法·leetcode·职场和发展
九圣残炎2 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
lulu_gh_yu2 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法