【C++STL】二叉搜索树(BST)

目录

一.二叉搜索树基本的操作

1.1.查找操作

1.2.插入操作

1.3.删除操作

1.4.第一版源代码

1.5.二叉搜索树的性能分析

二.二叉搜索树的改造

2.1.K模型

2.1.1.改造思路

2.1.2.完整代码

[2.2.K-V 模型](#2.2.K-V 模型)

2.2.1.改造思路

2.2.2.完整代码

2.3.二叉树常见算法题


一.二叉搜索树基本的操作

二分搜索树(英语:Binary Search Tree),也称为 二叉查找树 、二叉搜索树 、有序二叉树或排序二叉树。

满足以下几个条件:

  • 若它的左子树不为空,左子树上所有节点的值都小于它的根节点。
  • 若它的右子树不为空,右子树上所有的节点的值都大于它的根节点。
  • 也就是左⼦树结点值<根结点值<右⼦树结点值

它的左、右子树也都是二分搜索树。

如下图所示:

相较于堆,⼆叉搜索树是⼤⼩关系更为严格的数据结构。但是并不需要必须是⼀棵完全⼆叉树,也就 是树的形态是任意的。

如下图所⽰,都是⼆叉搜索树:

根据⼆叉树的定义,左⼦树结点值<根结点值<右⼦树结点值,所以对⼆叉搜索树进⾏中序遍历,可 以得到⼀个递增的有序序列。

构造⼀颗⼆叉搜索树的⽬的,其实并不是为了排序,⽽是为了提⾼查找和插⼊删除关键字的速度。

接下来我们将会一边讲解二叉搜索树的实现原理,一边带大家来模拟实现这么一个二叉搜索树

首先,我们需要先将结点类型给定义出来

cpp 复制代码
// 二叉搜索树节点模板类
template<class K>
struct BSTreeNode
{
	// 指向左孩子的指针
	BSTreeNode<K>* _left;
	// 指向右孩子的指针
	BSTreeNode<K>* _right;
	// 节点中存储的关键码
	K _key;

	// 构造函数,用给定key初始化节点,左右指针置空
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

当然,有了节点还是不够的,我们还需要将二叉树的模型搭建出来。我们其实只需要保存一个根节点即可。

cpp 复制代码
// 二叉搜索树模板类
template<class K>
class BSTree
{
	// 将节点类型重命名为Node,方便使用
	typedef BSTreeNode<K> Node;
......
private:
	Node* _root;   // 二叉搜索树的根节点
};

1.1.查找操作

⼆叉搜索树的查找是从根结点开始,沿某个分⽀逐层向下⽐较的过程,

  • 若⼆叉搜索树⾮空,先将给定值与根结点的关键字⽐较,
  • 若相等,则查找成功;
  • 若不等,如果⼩于根结点的关键字,则在根结点的 左⼦树上查找,否则在根结点的右⼦树上查找。

⽽这也是⼀个递归的过程。

以下实例在二分搜索树中寻找 43 元素

(1) 元素 43 比根节点 42 大,需要在右子节点继续比较。

(2) 元素 43 比 59 小,需要在左子节点继续比较。

(3) 元素 43 比 51 小,需要在左子节点继续比较。

(4) 查找 51 的左子节点 43,正好和相等,结束。

  • 时间复杂度:

最坏情况下会从根节点开始,查找到叶⼦结点。因此时间复杂度是和树的⾼度有关的,⽽树⾼最差会 变成⼀条单链表,因此时间复杂度为O(N) 。

那么我们很快就能写出下面这个代码

cpp 复制代码
// 二叉搜索树模板类
template<class K>
class BSTree
{
	// 将节点类型重命名为Node,方便使用
	typedef BSTreeNode<K> Node;
public:

// 查找key是否存在,存在返回true,否则返回false
	bool Find(const K& key)
	{
		Node* cur = _root;
		// 从根开始遍历
		while (cur)
		{
			if (cur->_key < key)          // key较大,向右查找
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)     // key较小,向左查找
			{
				cur = cur->_left;
			}
			else                          // 找到目标key
			{
				return true;
			}
		}
		// 未找到
		return false;
	}

......
private:
	Node* _root;   // 二叉搜索树的根节点
};

1.2.插入操作

⼆叉搜索树作为⼀种动态树表,其特点是树的结构通常不是⼀次⽣成的,⽽是在查找的过程的过程 中,当树中不存在关键字值等于给定值的结点时再进⾏插⼊的。插⼊的结点⼀定是⼀个新添加的叶结 点,且是查找失败时的查找路径上访问的最后⼀个结点的左孩⼦或右孩⼦。

  • 若原⼆叉树为空树,则直接插⼊结点;
  • 否则根据⼆叉搜索树的特性,将插⼊的关键字 key与根结点 对⽐,
  • 若关键字 key ⼩于根结点值,则插⼊到左⼦树,
  • 若关键字 key ⼤于根结点值,则插⼊到右 ⼦树。

以下实例向如下二分搜索树中插入元素 61 的步骤:

(1)需要插入的元素 61 比 42 大,比较 42 的右子树根节点。

(2)61 比 59 大,所以需要把 61 移动到 59 右子树相应位置,而此时为空,直接插入作为 59 的右子节点。

插入操作也是一个递归过程,分三种情况,等于、大于、小于。

  • 时间复杂度

插入与查找过程一致,因此时间复杂度为O(N).

cpp 复制代码
// 二叉搜索树模板类
template<class K>
class BSTree
{
	// 将节点类型重命名为Node,方便使用
	typedef BSTreeNode<K> Node;
public:

// 插入节点,成功返回true,如果key已存在则返回false
	bool Insert(const K& key)
	{
		// 树为空,直接创建根节点
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		// parent用于记录当前节点的父节点,方便后续插入
		Node* parent = nullptr;
		Node* cur = _root;
		// 循环查找插入位置
		while (cur)
		{
			if (cur->_key < key)          // key大于当前节点,向右走
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)     // key小于当前节点,向左走
			{
				parent = cur;
				cur = cur->_left;
			}
			else                          // key已存在,插入失败
			{
				return false;
			}
		}
        
		// 创建新节点
		cur = new Node(key);
		// 根据key与父节点key的比较,决定插入到左子树还是右子树
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
        
		return true;
	}

......
private:
	Node* _root;   // 二叉搜索树的根节点
};

二叉搜索树插入新节点的核心思路是:从根节点开始,根据键值大小关系,一步步向下查找合适的插入位置,直到找到一个空位置,然后将新节点挂载上去。同时需要保证不会插入重复的键值。


第一步:处理空树的情况

如果树是空的(根节点为空),那么直接创建新节点作为根节点,插入成功。


第二步:查找插入位置

如果树不为空,则需要从根节点开始,用待插入的键值与当前节点比较:

  • 如果待插入的键值大于当前节点,则向右子树移动;

  • 如果小于当前节点,则向左子树移动;

  • 如果相等,说明树中已存在相同键值的节点,根据二叉搜索树的规则(通常不允许重复),插入失败,直接返回 false

在移动过程中,需要记录当前节点的父节点,因为最后插入时需要在父节点下面挂载新节点。


第三步:插入新节点

当找到某个节点的左子树或右子树为空时(即 cur 变为空指针),就找到了合适的插入位置。此时,根据待插入键值与父节点键值的大小关系,决定将新节点作为父节点的左孩子还是右孩子:

  • 如果待插入键值大于父节点的键值,则将新节点挂载到父节点的右指针上;

  • 如果待插入键值小于父节点的键值,则将新节点挂载到父节点的左指针上。

然后,插入成功,返回 true

整个过程就是利用二叉搜索树的有序性,通过比较大小不断缩小搜索范围,最终在空位插入新节点。

1.3.删除操作

我们默认需要按值 来删除元素,所以要先进行查找,找到之后再删除该节点。删除节点之后,为了保持二叉搜索树的原有特性,就需要调整其他节点。

根据被删除的节点,有四种情况需要分析讨论:

1. 被删除节点的左右孩子均为空

2. 被删除的节点只有左孩子

3. 被删除的节点只有右孩子

4. 被删除的节点既有左孩子,又有右孩子

若结点既有左子树,又有右子树,有两种策略:

  • a. 令结点的直接后继替代该结点,然后从二叉搜索树中删去这个直接后继。
  • 结点的直接后继:中序遍历中,该结点的后继,也就是该结点的右子树最左下结点(该结点一定没有左子树)。
  • b. 令结点的直接前驱替代该结点,然后从二叉搜索树中删去这个直接前驱。
  • 结点的直接前驱:中序遍历中,该结点的前驱,也就是该结点的左子树最右下结点(该结点一定没有右子树)。

这里说明一下寻找右子树最小值 的方法:从右孩子开始,一直访问其左孩子节点,直到访问到空为止。这里访问到的最后一个节点即是右子树的最小值。(左子树最大值也同理)

案例⼀:删除50这个结点,并⽤直接前驱来替代。

案例二:删除50这个结点,并⽤直接后驱来替代。

  • 时间复杂度:

查找前驱和后继的操作,最差也会遍历整个⼆叉树,因此时间复杂度为 O(N) 。

那么我们现在来具体实现一下:

二叉搜索树删除节点的核心思路是:先找到要删除的节点,然后根据它的子节点情况分三种方式处理,最后释放内存。整个过程要保证删除后树依然保持二叉搜索树的性质(左子树所有节点小于根,右子树所有节点大于根)。


第一步:查找要删除的节点

从根节点开始,用待删除的键值与当前节点比较:

  • 如果键值大于当前节点,则向右子树继续查找;

  • 如果键值小于当前节点,则向左子树继续查找;

  • 如果相等,则找到了目标节点,准备删除。

    在查找过程中,需要记录当前节点的父节点,因为删除时可能需要修改父节点的指针。

如果一直找到空节点都没找到,说明树中不存在该键值,删除失败。


第二步:处理找到的节点

假设当前要删除的节点为 cur,它的父节点为 parent。根据 cur 的子节点情况,分为以下三种情形:

情形一:左子树为空

此时 cur 只有右子树(也可能右子树也为空,即叶子节点)。删除方法:**直接用 cur 的右子节点替换 cur 的位置。**这样子的话,可能需要修改cur的父节点的左/右孩子指针,指向 cur 的右子节点。

  • 如果 cur 是根节点,则直接将根指针指向它的右子节点。

  • 如果 cur 不是根节点**,**我们需要根据 cur 是父节点的左孩子还是右孩子,将父节点对应的指针指向 cur 的右子节点。如果cur是父节点的左孩子,那么直接让cur的父节点的左孩子指针指向cur的右节点。如果cur是父节点的右孩子,那么直接让cur的父节点的右孩子指针指向cur的右节点。

情形二:右子树为空

类似地,cur 只有左子树。删除方法:直接用 cur 的左子节点替换 cur 的位置。

  • 如果是根节点,根指针指向左子节点。

  • 否则,根据 cur 是父节点的左孩子还是右孩子,将父节点对应的指针指向 cur 的左子节点。如果cur是父节点的左孩子,那么直接让cur的父节点的左孩子指针指向cur的左节点。如果cur是父节点的右孩子,那么直接让cur的父节点的右孩子指针指向cur的左节点。

情形三:左右子树都不为空

这是最复杂的情况,因为不能简单用一个子节点替换,否则会丢失另一棵子树。常用方法是找一个"替身"节点来顶替 cur,这个替身必须能维持二叉搜索树的性质。

通常有两种选择:

  • 以cur结点为根节点的左子树中的最大节点(即左子树中最右边的节点);

  • 以cur结点为根节点的右子树中的最小节点(即右子树中最左边的节点)。

这两种节点都满足:大于左子树所有节点,小于右子树所有节点,可以放在 cur 的位置而不破坏树的结构。

代码中采用的是左子树最大节点法。

具体步骤:

  1. cur 的左孩子开始,一直向右查找,直到某个节点的右子节点为空,这个节点就是左子树的最大节点,记为 leftMax。同时记录它的父节点parent。

  2. cur 的键值与 leftMax 的键值交换。此时,要删除的节点变成了 leftMax(因为它的键值现在是要删除的值),而 cur 节点存储了原来 leftMax 的值,树的结构暂时不变。

  3. **现在问题转化为删除 leftMax 节点。**注意,leftMax 一定没有右子树(因为它是左子树中的最大节点),所以它可能只有左子树或为叶子。这正好属于前面情形一或情形二(左子树可能为空)。

  4. 删除 leftMax:用它的左子节点替换它本身(如果左子节点为空,则用空替换)。根据 leftMax 是其父节点的左孩子还是右孩子,修改leftMax父节点相应的指针。如果leftMax是父节点parent的左孩子,那么直接让leftMax的父节点parent的左孩子指针指向leftMax的左节点。如果leftMax是父节点parent的右孩子,那么直接让leftMax的父节点parent的右孩子指针指向cur的左节点。

  5. 最后,实际释放内存的是 leftMax 节点(现在已被交换过键值,但地址不变),而不是原来的 cur


第三步:释放内存

无论哪种情形,在调整好指针链接后,都要用 delete 释放被删除节点的内存,并返回 true 表示删除成功。

如果查找过程中未找到节点,则返回 false

整个过程中,需要小心处理根节点的情况,以及确保指针修改正确,避免丢失子树。对于左右子树都不为空的情况,通过找替身节点交换值的方法,巧妙地避开了复杂的孩子重链接,是二叉搜索树删除的标准做法之一。

cpp 复制代码
// 删除指定key的节点,成功返回true,失败返回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 // 找到了要删除的节点cur
			{
				// 情况1:待删除节点的左子树为空
				if (cur->_left == nullptr)
				{
					// 如果cur是根节点,直接让根指向其右子树
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						// 判断cur是其父节点的左孩子还是右孩子,然后用cur的右子树替代cur
						if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}// 情况2:待删除节点的右子树为空
				else if (cur->_right == nullptr)
				{
					// 如果cur是根节点,直接让根指向其左子树
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						// 用cur的左子树替代cur
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
				} // 情况3:待删除节点的左右子树都不为空
				else
				{
					// 找左子树中的最大节点(或右子树中的最小节点)作为替代节点
					// 这里选择左子树的最大节点(即左子树中最右边的节点)
					Node* parent = cur;           // 注意:这里重新定义了一个parent,隐藏了外层的parent,用于记录替代节点的父节点
					Node* leftMax = cur->_left;    // 从cur的左孩子开始找
					// 一直向右走,找到左子树的最大节点
					while (leftMax->_right)
					{
						parent = leftMax;
						leftMax = leftMax->_right;
					}

					// 将替代节点的key与待删除节点的key交换
					swap(cur->_key, leftMax->_key);
                    //现在值换掉了,leftMax里面存储的值变成我们需要删除的那个值了,现在变成我们需要删除leftMax节点了

					// 现在待删除的节点变成了leftMax(它一定没有右子树)
					// 将leftMax的左子树链接到其父节点的相应位置
                    //leftMax一定没有右节点,只可能有左节点
					if (parent->_left == leftMax)
					{
						parent->_left = leftMax->_left;
					}
					else
					{
						parent->_right = leftMax->_left;
					}

					// 更新cur指针,使其指向待删除的节点(即leftMax),以便后面统一delete
					cur = leftMax;
				}

				// 释放待删除节点内存
				delete cur;
				return true;
			}
		}

		// 未找到key,删除失败
		return false;
	}

1.4.第一版源代码

那么现在,我们的这个二叉搜索树的第一版的代码就算是写完了

我们的这个二叉树目前只是完成了最核心的这3个接口。

BSTreeNode_v1.hpp

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;

// 二叉搜索树节点模板类
template<class K>
struct BSTreeNode
{
	// 指向左孩子的指针
	BSTreeNode<K>* _left;
	// 指向右孩子的指针
	BSTreeNode<K>* _right;
	// 节点中存储的关键码
	K _key;

	// 构造函数,用给定key初始化节点,左右指针置空
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

// 二叉搜索树模板类
template<class K>
class BSTree
{
	// 将节点类型重命名为Node,方便使用
	typedef BSTreeNode<K> Node;
public:
	// 构造函数,初始化根节点为空
	BSTree()
		:_root(nullptr)
	{}

	// 插入节点,成功返回true,如果key已存在则返回false
	bool Insert(const K& key)
	{
		// 树为空,直接创建根节点
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		// parent用于记录当前节点的父节点,方便后续插入
		Node* parent = nullptr;
		Node* cur = _root;
		// 循环查找插入位置
		while (cur)
		{
			if (cur->_key < key)          // key大于当前节点,向右走
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)     // key小于当前节点,向左走
			{
				parent = cur;
				cur = cur->_left;
			}
			else                          // key已存在,插入失败
			{
				return false;
			}
		}
        
		// 创建新节点
		cur = new Node(key);
		// 根据key与父节点key的比较,决定插入到左子树还是右子树
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

	// 查找key是否存在,存在返回true,否则返回false
	bool Find(const K& key)
	{
		Node* cur = _root;
		// 从根开始遍历
		while (cur)
		{
			if (cur->_key < key)          // key较大,向右查找
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)     // key较小,向左查找
			{
				cur = cur->_left;
			}
			else                          // 找到目标key
			{
				return true;
			}
		}
		// 未找到
		return false;
	}

	// 删除指定key的节点,成功返回true,失败返回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 // 找到了要删除的节点cur
			{
				// 情况1:待删除节点的左子树为空
				if (cur->_left == nullptr)
				{
					// 如果cur是根节点,直接让根指向其右子树
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						// 判断cur是其父节点的左孩子还是右孩子,然后用cur的右子树替代cur
						if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}// 情况2:待删除节点的右子树为空
				else if (cur->_right == nullptr)
				{
					// 如果cur是根节点,直接让根指向其左子树
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						// 用cur的左子树替代cur
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
				} // 情况3:待删除节点的左右子树都不为空
				else
				{
					// 找左子树中的最大节点(或右子树中的最小节点)作为替代节点
					// 这里选择左子树的最大节点(即左子树中最右边的节点)
					Node* parent = cur;           // 注意:这里重新定义了一个parent,隐藏了外层的parent,用于记录替代节点的父节点
					Node* leftMax = cur->_left;    // 从cur的左孩子开始找
					// 一直向右走,找到左子树的最大节点
					while (leftMax->_right)
					{
						parent = leftMax;
						leftMax = leftMax->_right;
					}

					// 将替代节点的key与待删除节点的key交换
					swap(cur->_key, leftMax->_key);
                    //现在值换掉了,leftMax里面存储的值变成我们需要删除的那个值了,现在变成我们需要删除leftMax节点了

					// 现在待删除的节点变成了leftMax(它一定没有右子树)
					// 将leftMax的左子树链接到其父节点的相应位置
                    //leftMax一定没有右节点,只可能有左节点
					if (parent->_left == leftMax)
					{
						parent->_left = leftMax->_left;
					}
					else
					{
						parent->_right = leftMax->_left;
					}

					// 更新cur指针,使其指向待删除的节点(即leftMax),以便后面统一delete
					cur = leftMax;
				}

				// 释放待删除节点内存
				delete cur;
				return true;
			}
		}

		// 未找到key,删除失败
		return false;
	}

	// 中序遍历接口,输出树中所有元素
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 中序遍历递归函数(私有成员)
	void _InOrder(Node* root)
	{
		if (root == NULL)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

private:
	Node* _root;   // 二叉搜索树的根节点
};

test_v1.cpp

cpp 复制代码
#include"BSTreeNode_v1.hpp"

// 测试函数,演示二叉搜索树的基本操作
void TestBSTree1()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> t;
	// 依次插入数组元素
	for (auto e : a)
	{
		t.Insert(e);
	}

	// 打印中序遍历结果
	t.InOrder();

	// 删除几个节点并观察结果
	t.Erase(4);
	t.InOrder();

	t.Erase(6);
	t.InOrder();

	t.Erase(7);
	t.InOrder();

	t.Erase(3);
	t.InOrder();

	// 删除所有节点
	for (auto e : a)
	{
		t.Erase(e);
	}
	// 最终树应为空
	t.InOrder();
}

int main()
{
    TestBSTree1();
}

完全没有一点问题

1.5.二叉搜索树的性能分析

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

那大家思考一下,搜索二叉树的查找的时间复杂度是多少?

那这个其实在不同情况下是不一样的:

如果二叉搜索树处于比较平衡的情况(接近完全二叉树),比如这样的

这种情况最坏的查找无非也就查找高度次(那如果结点数量为N,它的高度通常保持在logN的水平),所以这样它的时间复杂度就是O(logN)。

但是,避免不了出现这样的情况

二叉搜索树退化为单支树(或者接近单支),那这时查找的时间复杂度就应该是O(N)。

所以,二叉搜索树的查找的时间复杂度

  • 最优情况下,二叉搜索树趋于平衡,其平均比较次数为:logN,时间复杂度为O(logN)
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: ,时间复杂度为O(N)

那么问题来了:如果退化成单支树,二叉搜索树的性能就失去了。

那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。

这个到时候讲到再说...

二.二叉搜索树的改造

2.1.K模型

2.1.1.改造思路

搜索二叉树的第一个应用是K模型,什么是K模型呢,介绍一下:

其实呢就是一个在不在的问题。

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

比如:

  • 给一个单词word,判断该单词是否拼写正确,具体方式如下:
  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误

那我们上面实现的搜索二叉树其实就是按照K模型搞的。但是我们可以进行二次封装一下

首先,我们还是需要先将节点类型给定义出来

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 复制代码
// 二叉搜索树类模板(key模型)
	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node; // 节点类型重命名
	public:
......

	private:
		Node* _root; // 树的根节点指针
	};

然后,其实最核心的几个操作,我们在上面最基础的那一版已经实现了。

只不过,我们在最基础的那一版里面使用的是循环,但是,我们这里除了提供了循环的版本,还另外增加了递归的版本

那么仔细斟酌一下:

cpp 复制代码
// 二叉搜索树类模板(key模型)
	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node; // 节点类型重命名
	public:
		// 构造函数,初始化空树
		BSTree()
			:_root(nullptr)
		{}

		// 拷贝构造函数,深拷贝另一棵树
		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}

		// 赋值运算符重载(现代写法,利用传值拷贝构造临时对象后交换)
		BSTree<K>& operator=(BSTree<K> t)
		{
			swap(_root, t._root); // 交换根指针
			return *this;
		}

		// 析构函数,释放所有节点
		~BSTree()
		{
			Destroy(_root);
		}

		// 非递归查找接口
		bool Find_NR(const K& key)
		{
			return _Find_NR(key);
		}

		// 非递归插入接口
		bool Insert_NR(const K& key)
		{
			return _Insert_NR(key);
		}

		// 非递归删除接口
		bool Erase_NR(const K& key)
		{
			return _Erase_NR(key);
		}

		// 中序遍历(有序输出,方便测试)
		void InOrder()
		{
			_InOrder(_root);
			std::cout << std::endl;
		}

		// 递归查找接口
		bool Find_R(const K& key)
		{
			return _Find_R(_root, key);
		}

		// 递归插入接口
		bool Insert_R(const K& key)
		{
			return _Insert_R(_root, key);
		}

		// 递归删除接口
		bool Erase_R(const K& key)
		{
			return _Erase_R(_root, key);
		}

	private:
......
}

对于这个非递归版本的,我们就直接套用上面那个最基本的即可

cpp 复制代码
// 插入节点(非递归)
		bool _Insert_NR(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;
		}

		// 查找节点(非递归)
		bool _Find_NR(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_NR(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 // 找到了要删除的节点 cur
				{
					// 情况1:左孩子为空
					if (cur->_left == nullptr)
					{
						if (cur == _root) // 删除根节点
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_right == cur)
								parent->_right = cur->_right;
							else
								parent->_left = cur->_right;
						}
					}
					// 情况2:右孩子为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_right == cur)
								parent->_right = cur->_left;
							else
								parent->_left = cur->_left;
						}
					}
					// 情况3:左右孩子均不为空
					else
					{
						// 找左子树的最大节点(或右子树的最小节点)作为替代节点
						Node* parent = cur; // 注意:此处隐藏了外层的parent,用于记录替代节点的父节点
						Node* leftMax = cur->_left; // 从左子树开始找最大
						while (leftMax->_right) // 一直向右,直到最右
						{
							parent = leftMax;
							leftMax = leftMax->_right;
						}

						// 交换当前节点和替代节点的键值
						std::swap(cur->_key, leftMax->_key);

						// 将替代节点的左孩子(如果有)挂接到其父节点
						if (parent->_left == leftMax)
							parent->_left = leftMax->_left;
						else
							parent->_right = leftMax->_left;

						cur = leftMax; // 让cur指向实际要删除的节点(原替代节点)
					}

					delete cur; // 释放节点
					return true;
				}
			}
			return false; // 未找到键值
		}

那么除了这个非递归版本的,我们还设计了递归版本的

cpp 复制代码
// 递归删除核心实现(使用引用指针,简化父节点连接)
		bool _Erase_R(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key) // 键值较大,去右子树删除
			{
				return _Erase_R(root->_right, key);
			}
			else if (root->_key > key) // 键值较小,去左子树删除
			{
				return _Erase_R(root->_left, key);
			}
			else // 找到要删除的节点
			{
				Node* del = root; // 保存当前节点指针,以便删除

				// 情况1:左为空
				if (root->_left == nullptr)
				{
					root = root->_right; // 用右孩子替换当前节点(引用直接修改父节点指向)
				}
				// 情况2:右为空
				else if (root->_right == nullptr)
				{
					root = root->_left; // 用左孩子替换当前节点
				}
				// 情况3:左右都不为空
				else
				{
					// 找左子树的最大节点
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}
					// 交换键值
					std::swap(root->_key, leftMax->_key);
					// 递归到左子树删除那个最大节点(此时它已经被交换,变成了待删除的键)
					return _Erase_R(root->_left, key);
				}

				delete del; // 释放节点
				return true;
			}
		}

		// 递归插入核心实现(引用指针简化)
		bool _Insert_R(Node*& root, const K& key)
		{
			if (root == nullptr) // 找到插入位置,创建新节点
			{
				root = new Node(key);
				return true;
			}

			if (root->_key < key) // 大于当前节点,向右递归
			{
				return _Insert_R(root->_right, key);
			}
			else if (root->_key > key) // 小于当前节点,向左递归
			{
				return _Insert_R(root->_left, key);
			}
			else // 键值已存在,插入失败
			{
				return false;
			}
		}

		// 递归查找核心实现
		bool _Find_R(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key) // 向右查找
			{
				return _Find_R(root->_right, key);
			}
			else if (root->_key > key) // 向左查找
			{
				return _Find_R(root->_left, key);
			}
			else // 找到
			{
				return true;
			}
		}

在这里,我们需要特别注意这个删除元素的思路

  • 1. 什么是 Node*& root?

这是一个指向指针的引用。也就是说,root 是一个引用,它引用的是一个 Node* 类型的指针变量。

在递归调用中,比如 _Erase_R(root->_left, key),这里传递的参数 root->_left 本身就是一个指针(左孩子指针)。由于参数是引用,所以函数内部操作 root 就相当于直接操作调用者传入的那个指针变量本身。

  • 2. 为什么能修改父节点的指向?

假设我们有这样一个树结构:

cpp 复制代码
      10
     /  \
    5    15
   / \
  3   7

我们想删除节点 5。在递归过程中,当我们从根节点 10 开始,比较 key=5,发现 5 < 10,于是进入左子树,调用 _Erase_R(10->_left, 5)。此时,10->_left 是一个指针,指向节点 5。而函数参数 Node*& root 就绑定到了 10->_left 这个指针变量上。

也就是说,在函数内部,root 就是 10->_left 的别名。对 root 的任何赋值,都会直接改变 10->_left 的值。

  • 3. 在删除节点时如何操作?

当递归到节点 5 时,root 指向节点 5(即 root 的值是节点 5 的地址),同时 root 就是父节点(10)的左指针的引用。

现在要删除节点 5,考虑情况:

  • 如果节点 5 的左子树为空,那么我们可以直接把节点 5 的右子树接到父节点上。代码中:

    cpp 复制代码
    if (root->_left == nullptr)
    {
        root = root->_right; // 关键!
    }

    这里 root = root->_right; 的意思是将当前节点替换为它的右孩子。因为 root 是父节点左指针的引用,所以这个赋值实际上就是执行了 parent->_left = root->_right。这样就完成了父节点指针的更新,直接跳过了要删除的节点。

  • 如果节点 5 的右子树为空,类似地,root = root->_left; 就将父节点的左指针指向了节点 5 的左孩子。

  • 如果左右都不为空,则先交换键值,然后递归到左子树去删除那个已经交换过去的 key。这时因为已经交换了键值,原本的节点 5 现在可能存储了另一个值,但结构未变,递归调用 _Erase_R(root->_left, key) 会进入左子树,最终删除那个替代节点。

4. 为什么不需要显式 parent 指针?

因为在每一层递归中,参数 root 都是上一层节点中指向当前节点的指针的引用。这个引用天然地记录了父节点与当前节点的连接关系。当我们修改 root 时,就是在修改父节点的指针。

例如,在递归调用链中:

  • 第一次调用:_Erase_R(_root, key),这里 root 是 _root 的引用,可以直接修改根指针。

  • 第二次调用:_Erase_R(_root->_left, key),这里 root 是 _root->_left 的引用。

  • 以此类推。

因此,不需要像非递归那样用一个额外的 parent 变量来记录父节点,因为函数调用栈已经隐含了路径信息,并且通过引用可以回溯修改。

  1. 示例说明

假设我们要删除节点 5(左右子树都不为空)。流程:

  1. 从根 10 开始,10 > 5,调用 _Erase_R(10->_left, 5),此时 root 引用 10->_left(指向节点 5)。

  2. 进入节点 5,发现 key 相等。因为左右都不为空,找到左子树的最大节点 3(假设 3 没有右孩子)。

  3. 交换节点 5 和节点 3 的键值,现在节点 5 存储 3,节点 3 存储 5。

  4. 然后递归调用 _Erase_R(5->_left, 5)(注意此时 5->_left 还是原来的左孩子,但节点 5 的键值已经变成 3,所以实际上要去左子树中找键值 5)。

  5. 进入节点 3(此时节点 3 存储 5),发现 key 相等。节点 3 可能没有右孩子(因为它是最大节点),所以很可能属于左为空或右为空的情况,于是执行 root = root->_leftroot = root->_right,将节点 3 的子树接到其父节点上。

  6. 删除节点 3,回溯结束。

整个过程,通过引用参数,每一步修改都直接作用在父节点的指针上,无需额外记录父节点。

2.1.2.完整代码

BSTreeNode_k.hpp

cpp 复制代码
#pragma once // 防止头文件重复包含
#include<iostream>

namespace key
{
	// 二叉搜索树节点结构体模板
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode<K>* _left;  // 左孩子指针
		BSTreeNode<K>* _right; // 右孩子指针
		K _key;                 // 节点存储的键值

		// 构造函数,初始化节点
		BSTreeNode(const K& key)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
		{}
	};

	// 二叉搜索树类模板(key模型)
	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node; // 节点类型重命名
	public:
		// 构造函数,初始化空树
		BSTree()
			:_root(nullptr)
		{}

		// 拷贝构造函数,深拷贝另一棵树
		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}

		// 赋值运算符重载(现代写法,利用传值拷贝构造临时对象后交换)
		BSTree<K>& operator=(BSTree<K> t)
		{
			std::swap(_root, t._root); // 交换根指针
			return *this;
		}

		// 析构函数,释放所有节点
		~BSTree()
		{
			Destroy(_root);
		}

		// 非递归查找接口
		bool Find_NR(const K& key)
		{
			return _Find_NR(key);
		}

		// 非递归插入接口
		bool Insert_NR(const K& key)
		{
			return _Insert_NR(key);
		}

		// 非递归删除接口
		bool Erase_NR(const K& key)
		{
			return _Erase_NR(key);
		}

		// 中序遍历(有序输出,方便测试)
		void InOrder()
		{
			_InOrder(_root);
			std::cout << std::endl;
		}

		// 递归查找接口
		bool Find_R(const K& key)
		{
			return _Find_R(_root, key);
		}

		// 递归插入接口
		bool Insert_R(const K& key)
		{
			return _Insert_R(_root, key);
		}

		// 递归删除接口
		bool Erase_R(const K& key)
		{
			return _Erase_R(_root, key);
		}

	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 Destroy(Node*& root)
		{
			if (root == nullptr)
				return;

			Destroy(root->_left);  // 递归销毁左子树
			Destroy(root->_right); // 递归销毁右子树
			delete root;           // 释放当前节点
			root = nullptr;        // 指针置空
		}

		// 插入节点(非递归)
		bool _Insert_NR(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;
		}

		// 查找节点(非递归)
		bool _Find_NR(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_NR(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 // 找到了要删除的节点 cur
				{
					// 情况1:左孩子为空
					if (cur->_left == nullptr)
					{
						if (cur == _root) // 删除根节点
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_right == cur)
								parent->_right = cur->_right;
							else
								parent->_left = cur->_right;
						}
					}
					// 情况2:右孩子为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_right == cur)
								parent->_right = cur->_left;
							else
								parent->_left = cur->_left;
						}
					}
					// 情况3:左右孩子均不为空
					else
					{
						// 找左子树的最大节点(或右子树的最小节点)作为替代节点
						Node* parent = cur; // 注意:此处隐藏了外层的parent,用于记录替代节点的父节点
						Node* leftMax = cur->_left; // 从左子树开始找最大
						while (leftMax->_right) // 一直向右,直到最右
						{
							parent = leftMax;
							leftMax = leftMax->_right;
						}

						// 交换当前节点和替代节点的键值
						std::swap(cur->_key, leftMax->_key);

						// 将替代节点的左孩子(如果有)挂接到其父节点
						if (parent->_left == leftMax)
							parent->_left = leftMax->_left;
						else
							parent->_right = leftMax->_left;

						cur = leftMax; // 让cur指向实际要删除的节点(原替代节点)
					}

					delete cur; // 释放节点
					return true;
				}
			}
			return false; // 未找到键值
		}

		// 递归删除核心实现(使用引用指针,简化父节点连接)
		bool _Erase_R(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key) // 键值较大,去右子树删除
			{
				return _Erase_R(root->_right, key);
			}
			else if (root->_key > key) // 键值较小,去左子树删除
			{
				return _Erase_R(root->_left, key);
			}
			else // 找到要删除的节点
			{
				Node* del = root; // 保存当前节点指针,以便删除

				// 情况1:左为空
				if (root->_left == nullptr)
				{
					root = root->_right; // 用右孩子替换当前节点(引用直接修改父节点指向)
				}
				// 情况2:右为空
				else if (root->_right == nullptr)
				{
					root = root->_left; // 用左孩子替换当前节点
				}
				// 情况3:左右都不为空
				else
				{
					// 找左子树的最大节点
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}
					// 交换键值
					std::swap(root->_key, leftMax->_key);
					// 递归到左子树删除那个最大节点(此时它已经被交换,变成了待删除的键)
					return _Erase_R(root->_left, key);
				}

				delete del; // 释放节点
				return true;
			}
		}

		// 递归插入核心实现(引用指针简化)
		bool _Insert_R(Node*& root, const K& key)
		{
			if (root == nullptr) // 找到插入位置,创建新节点
			{
				root = new Node(key);
				return true;
			}

			if (root->_key < key) // 大于当前节点,向右递归
			{
				return _Insert_R(root->_right, key);
			}
			else if (root->_key > key) // 小于当前节点,向左递归
			{
				return _Insert_R(root->_left, key);
			}
			else // 键值已存在,插入失败
			{
				return false;
			}
		}

		// 递归查找核心实现
		bool _Find_R(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key) // 向右查找
			{
				return _Find_R(root->_right, key);
			}
			else if (root->_key > key) // 向左查找
			{
				return _Find_R(root->_left, key);
			}
			else // 找到
			{
				return true;
			}
		}

		// 中序遍历递归实现
		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}

			_InOrder(root->_left);       // 遍历左子树
			std::cout << root->_key << " ";   // 访问根
			_InOrder(root->_right);      // 遍历右子树
		}

	private:
		Node* _root; // 树的根节点指针
	};

	
}

我们写一个测试代码看看

test_k.cpp

cpp 复制代码
#include "BSTreeNode_k.hpp"
#include <iostream>

using namespace std;

// 测试非递归接口
void TestNR() {
    cout << "========== 测试非递归接口 ==========" << endl;
    key::BSTree<int> t;
    int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
    for (auto e : a) {
        t.Insert_NR(e);
    }
    cout << "插入后中序遍历: ";
    t.InOrder();  // 预期输出:1 3 4 6 7 8 10 13 14

    cout << "查找 6: " << (t.Find_NR(6) ? "找到" : "未找到") << endl;
    cout << "查找 100: " << (t.Find_NR(100) ? "找到" : "未找到") << endl;

    // 逐一删除,测试各种情况
    t.Erase_NR(4);   // 删除叶子节点
    cout << "删除 4 后: ";
    t.InOrder();

    t.Erase_NR(6);   // 删除有一个孩子的节点
    cout << "删除 6 后: ";
    t.InOrder();

    t.Erase_NR(8);   // 删除根节点(有两个孩子)
    cout << "删除 8 后: ";
    t.InOrder();

    t.Erase_NR(13);  // 删除有一个孩子的节点
    cout << "删除 13 后: ";
    t.InOrder();

    t.Erase_NR(3);   // 删除有一个孩子的节点
    cout << "删除 3 后: ";
    t.InOrder();

    // 清空树
    for (auto e : a) {
        t.Erase_NR(e);
    }
    cout << "删除所有元素后: ";
    t.InOrder();  // 空树,无输出
    cout << endl;
}

// 测试递归接口
void TestR() {
    cout << "\n========== 测试递归接口 ==========" << endl;
    key::BSTree<int> t;
    int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
    for (auto e : a) {
        t.Insert_R(e);
    }
    cout << "插入后中序遍历: ";
    t.InOrder();

    cout << "查找 6: " << (t.Find_R(6) ? "找到" : "未找到") << endl;
    cout << "查找 100: " << (t.Find_R(100) ? "找到" : "未找到") << endl;

    t.Erase_R(4);
    cout << "删除 4 后: ";
    t.InOrder();

    t.Erase_R(6);
    cout << "删除 6 后: ";
    t.InOrder();

    t.Erase_R(8);
    cout << "删除 8 后: ";
    t.InOrder();

    t.Erase_R(13);
    cout << "删除 13 后: ";
    t.InOrder();

    t.Erase_R(3);
    cout << "删除 3 后: ";
    t.InOrder();

    for (auto e : a) {
        t.Erase_R(e);
    }
    cout << "删除所有元素后: ";
    t.InOrder();
    cout << endl;
}

// 测试拷贝构造和赋值运算符
void TestCopy() {
    cout << "\n========== 测试拷贝构造和赋值 ==========" << endl;
    key::BSTree<int> t1;
    int a[] = {5, 3, 7, 2, 4, 6, 8};
    for (auto e : a) {
        t1.Insert_R(e);
    }
    cout << "原始树 t1: ";
    t1.InOrder();

    key::BSTree<int> t2(t1); // 拷贝构造
    cout << "拷贝构造 t2: ";
    t2.InOrder();

    key::BSTree<int> t3;
    t3 = t1;                 // 赋值运算符
    cout << "赋值 t3: ";
    t3.InOrder();

    // 修改 t1,检查 t2 和 t3 是否独立
    t1.Erase_R(5);
    cout << "删除 t1 的 5 后 t1: ";
    t1.InOrder();
    cout << "t2 保持不变: ";
    t2.InOrder();
    cout << "t3 保持不变: ";
    t3.InOrder();
}

int main() {
    TestNR();
    TestR();
    TestCopy();
    return 0;
}

2.2.K-V 模型

2.2.1.改造思路

我们上面每个节点存储的都是一个值。我们查询,删除,插入都是针对一个值来进行的,那么我们现在就是将每个节点换成键值对。

我们排序就根据键来排序

那么我们现在就先把节点类型定义出来

cpp 复制代码
// 二叉搜索树节点结构体模板(键值对模型)
	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)
		{}
	};

很好,有了节点类型,我们就能去对这个二叉搜索树进行定义了。

cpp 复制代码
// 二叉搜索树类模板(key-value模型)
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
......

	private:
		Node* _root; // 根节点指针
	};

然后我们还是需要补充3个核心接口,也就是插入,查询,删除

核心思想还是和之前的K模型没有啥大区别的。只不过是节点类型变了一下

cpp 复制代码
// 递归删除核心实现(与key模型相同,只是节点类型不同)
		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(root->_key, leftMax->_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; // 找到,返回节点指针
			}
		}

2.2.2.完整代码

BSTreeNode_kv.hpp

cpp 复制代码
#pragma once
#include<iostream>

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

	// 二叉搜索树类模板(key-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:
		// 递归删除核心实现(与key模型相同,只是节点类型不同)
		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(root->_key, leftMax->_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; // 找到,返回节点指针
			}
		}

		// 中序遍历递归实现,输出键值对
		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl; // 输出键:值
			_InOrder(root->_right);
		}

	private:
		Node* _root; // 根节点指针
	};
}

test_kv.cpp

cpp 复制代码
#include <string>
#include "BSTreeNode_kv.hpp"
#include <iostream>
using namespace std;

// 测试函数1:模拟英译汉词典
void TestBSTree1()
{
    // 示例:可以存储字符串到日期类的映射,这里简单使用字符串到字符串
    key_value::BSTree<string, string> dict;
    dict.InsertR("insert", "插入");
    dict.InsertR("sort", "排序");
    dict.InsertR("right", "右边");
    dict.InsertR("date", "日期");

    string str = "sort";

    key_value::BSTreeNode<string, string> *ret = dict.FindR(str);
    if (ret)
    {
        cout << ret->_value << endl; // 输出对应的中文
    }
    else
    {
        cout << "无此单词" << endl;
    }
}

// 测试函数2:统计水果出现次数
void TestBSTree2()
{
    string arr[] = {"西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉"};
    key_value::BSTree<string, int> countTree; // 键为水果名,值为出现次数
    for (auto &str : arr)
    {
        auto ret = countTree.FindR(str);
        if (ret == nullptr) // 首次出现
        {
            countTree.InsertR(str, 1);
        }
        else
        {
            ret->_value++; // 次数加1
        }
    }

    countTree.InOrder(); // 输出每种水果及其出现次数
}

int main()
{
    TestBSTree1();
    TestBSTree2();
}

很完美吧。

2.3.二叉树常见算法题

606. 根据二叉树创建字符串 - 力扣(LeetCode)

102. 二叉树的层序遍历 - 力扣(LeetCode)

107. 二叉树的层序遍历 II - 力扣(LeetCode)

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

二叉搜索树与双向链表_牛客题霸_牛客网

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

144. 二叉树的前序遍历 - 力扣(LeetCode)

94. 二叉树的中序遍历 - 力扣(LeetCode)

145. 二叉树的后序遍历 - 力扣(LeetCode)

相关推荐
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #20:有效的括号(数组模拟法、递归消除法等五种实现方案详细解析)
算法·leetcode··括号匹配·数组模拟法·递归消除法
yxc_inspire2 小时前
2026年寒假牛客训练赛补题(五)
算法
不想看见4042 小时前
6.3Permutations -- 回溯法--力扣101算法题解笔记
笔记·算法·leetcode
诗词在线2 小时前
孟浩然诗作数字化深度实战:诗词在线的意象挖掘、检索优化与多场景部署
大数据·人工智能·算法
芜湖xin2 小时前
【题解-Acwing】113. 特殊排序
算法·插入排序·二分
cccyi72 小时前
Redis基础
c++·redis
D_evil__3 小时前
【Effective Modern C++】第五章 右值引用、移动语义和完美转发:28. 理解引用折叠
c++
enjoy嚣士3 小时前
Java 之 实现C++库函数等价函数遇到的问题
java·开发语言·c++
代码栈上的思考3 小时前
双指针法:从三道经典题看双指针的核心思想
数据结构·算法