嗨~大家好,这里是春栀怡铃声的博客~

“做你害怕的事,然后发现,不过如此~”

目录

二叉搜索树的定义

实现二叉搜索树

实现节点

二叉搜索树的基本框架

增加数据

查找数据

删除数据

拷贝构造

运算符重载

析构函数

中序遍历

二叉树搜索的应用场景

key搜索场景

key/value搜索场景

key/value二叉搜索树代码实现


二叉搜索树的定义

二叉搜索树(Binary Search Tree),又称二叉查找树或二叉排序树。是一棵具有下列性质的二叉树:

  1. 若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值。

  2. 若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值。

  3. 它的左、右子树本身也分别都是二叉搜索树。

  4. (通常情况下)树中没有键值相等的节点。

一句话总结口诀: “左小、右大、根中间”。

想象一下我们玩“猜数字”游戏,范围是 1 到 100。 你猜“50”,我告诉你“太小了”。这时候你绝对不会再去猜 1 到 50 之间的数字,而是直接去猜 75,直接把一半的选项淘汰掉二叉搜索树就是把这个“猜数字”的过程实体化了。 每一个节点就是一个裁判,当你拿着一个数字来找它时,它会告诉你:“比我小,去我的左边找;比我大,去我的右边找。” 这种每次都能排除一半选项的机制,就是它效率极高的秘密。

实现二叉搜索树

实现节点

template<class K>
struct BTSNode
{
	BTSNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{ }
	K _key;
	BTSNode<K>* _left;
	BTSNode<K>* _right;
};

实现二叉树的节点,将节点设置成stuct类    默认节点的内容都可以类外可以访问。

需要一个指向其他节点的左指针,需要拎一个指向其他指针的右指针

初始化列表来初始化节点储存的值、左指针、右指针

二叉搜索树的基本框架

template<class K>
class BSTree
{
	typedef BTSNode<K> BTNode;
public:



private:
	BTNode* _root=nullptr;
};

public  之后实现的是下面我们要实现的函数

private 中存放的是二叉搜索树的根节点

增加数据

typedef BTSNode<K> BTNode;		
bool Insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new BTNode(key);
				return true;
			}
			BTNode* cur = _root;
			BTNode* parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
					return false;
			}
			if (parent->_key > key)
				parent->_left = new BTNode(key);
			else
				parent->_right = new BTNode(key);
			return true;
		}

如果在二叉搜索树插入数据时,二叉搜索树是一个空树,说明新插入的这个节点将作为根节点

二叉搜索树不是空树,我们需要定义2个节点指针cur    和 parent

cur 用来遍历来找到适合新插入节点的位置,parent 用来让原有二叉搜索树和新节点产生联系

接下来我们看cur 怎么遍历

while (cur)
            {
                if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                    return false;
            }

cur起始位置从根节点开始

如果cur的值大于插入的值,那么插入的位置应该在根节点的左子树,先让parent 走到cur 的位置,然后让cur=cur->left

如果cur的值小于插入的值,那么插入的位置应该在根节点的右子树,先让parent 走到cur 的位置,然后让cur=cur->right

在这里我是实现的是没有相同值的二叉搜索树,当检测到新插入的值等于cur 时,返回false  .

cur 走完这个遍历后,cur就来到了新节点要插入的位置了,此时要继续判断是父节点的左指针指向新节点      还是右指针指向新节点

if (parent->_key > key)
                parent->_left = new BTNode(key);
            else
                parent->_right = new BTNode(key);

如果父节点的值大于新插入的结点的值,父节点的左指针指向新节点

如果父节点的值小于新插入的结点的值,父节点的右指针指向新节点

最后返回true

查找数据

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

通过遍历cur 来查找

cur起始位置从根节点开始

如果cur的值大于插入的值,那么插入的位置应该在根节点的左子树,让cur=cur->left

如果cur的值小于插入的值,那么插入的位置应该在根节点的右子树,让cur=cur->right

如果cur 的值等于 要查找的值,说明已经找到了。

完成while 循环还未找到,返回false

删除数据

bool Erase(const K& key)
{
	BTNode* cur = _root;
	BTNode* parent = nullptr;
	while (cur)
	{
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_left;
					
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}

					else
					{
						parent->_right = cur->_left;
					}
					
				}
				delete cur;
			}

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



			else
			{
				BTNode* replaceParent = cur;
				BTNode* replace = cur->_right;
				while (replace->_left)
				{
					replaceParent = replace;
					replace = replace->_left;
				}
				cur->_key = replace->_key;
				
				if (replaceParent->_left == replace)
				    replaceParent->_left = replace->_right;
			    else
				    replaceParent->_right = replace->_right;
				delete replace;

			}
				return true;
		}
	}
	return false;
}

删除节点的方法是:

通过while 循环先找到该节点位置。

如果出了循环还未找到,直接返回false即可

cur起始位置从根节点开始

如果cur的值大于插入的值,那么要删除的位置应该在根节点的左子树,先让parent 走到cur 的位置,然后让cur=cur->left

如果cur的值小于插入的值,那么要删除的位置应该在根节点的右子树,先让parent 走到cur 的位置,然后让cur=cur->right

删除节点分为2类:

1.只有一个孩子的节点和叶子节点(没有孩子)

if (cur->_right == nullptr)
            {
                if (cur == _root)
                {
                    _root = cur->_left;
                    
                }
                else
                {
                    if (parent->_left == cur)
                    {
                        parent->_left = cur->_left;
                    }

                    else
                    {
                        parent->_right = cur->_left;
                    }
                    
                }
                delete cur;
            }

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

删除只有一个孩子的节点,

我们以右指针指向空  画图示例:

结合示例来解析代码

首先我们要判断这个要删除的节点是不是根节点

如果是,需要判断要删除的节点的左右指针哪个指向空

if (cur == _root)
                {
                    _root = cur->_left;
                }

不是根节点

 else
                {
                    if (parent->_left == cur)
                    {
                        parent->_left = cur->_left;
                    }

                    else
                    {
                        parent->_right = cur->_left;
                    }
                    
                }
                delete cur;

左指针为空和上面的方式相同

2.有2个孩子的节点

else
			{
				BTNode* replaceParent = cur;
				BTNode* replace = cur->_right;
				while (replace->_left)
				{
					replaceParent = replace;
					replace = replace->_left;
				}
				cur->_key = replace->_key;
				
				if (replaceParent->_left == replace)
				    replaceParent->_left = replace->_right;
			    else
				    replaceParent->_right = replace->_right;
				delete replace;

			}

采用替换法删除N

N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,

我们这里采用   N右子树的值最小结点R(最左结点)替代N

循环找到右子树中最左的的节点。

while (replace->_left)
                {
                    replaceParent = replace;
                    replace = replace->_left;
                }

替换

cur->_key = replace->_key;

拷贝构造

BSTree(BSTree<K>&t )
{
        _root=Copy(t._root);
}
        
 BTNode* Copy(BTNode* root)
 {
                if (root == nullptr)
                        return nullptr;
                BTNode* newRoot=new BTNode(root->key);
                newRoot->_left = Copy(root->_left);
                newRoot->_right = Copy(root->_right);
                return newRoot;
 }

此处是深拷贝,需要借助函数实现,并且采用递归的方式

因为是传参 传的是指向节点的指针,我们给的形参也必须是 BTNode*

BTNode* newRoot=new BTNode(root->key);

运算符重载

BSTree& operator=(BSTree tmp)
{
        swap(_root, tmp._root);
        return *this;
}

简单来说,它的作用是把另一个二叉搜索树(右值)的内容安全地赋值给当前树(左值),同时自动清理当前树原本的旧数据。

以下是它的核心运作原理和分步实现细节:

1. 核心奥秘:传值传参 (BSTree tmp)

注意参数列表中,tmp按值传递 (Pass-by-value),而不是按引用传递 (const BSTree& tmp)。

  • 当你执行 tree1 = tree2; 时,编译器会自动调用 BSTree拷贝构造函数

  • 这个拷贝构造函数会克隆一份 tree2 的数据,并用这些数据生成一个局部的临时对象,也就是参数 tmp

  • 此时,tmp 拥有了一棵和 tree2 一模一样的新树(深拷贝),而 tree1 还是原来的旧树。

2. (乾坤大挪移):交换指针 (swap(_root, tmp._root))

进入函数体后,调用了 swap 函数。

  • 这里仅仅交换了当前对象 (this) 的根节点指针 _root 和临时对象 tmp 的根节点指针 tmp._root

  • 交换后: 当前对象 (this) 的 _root 指向了刚才拷贝出来的新树;而 tmp_root 则指向了当前对象原本的“旧树”。

3. (借刀杀人)自动清理旧内存 (函数结束时)

当函数执行完毕准备 return 时,局部对象 tmp 会离开它的作用域,因此它的生命周期结束了。

  • 编译器会自动调用 tmp析构函数

  • 记住,刚才交换之后,tmp 此时手里拿着的是当前对象 (tree1) 的旧数据

  • 所以,tmp 销毁时,顺手就把你原来树里的那些不需要的旧节点内存全部释放干净了。

4. 完美收尾

返回当前对象的引用,这是为了支持连续赋值操作,例如 tree1 = tree2 = tree3;

析构函数

 ~BSTree()
 {
        Destory(_root);
        _root = nullptr;
 }
 void Destory(BTNode* root)
{
       if (root == nullptr)
           return;
        Destory(root->_left);
        Destory(root->_right);
        delete root;
}

因为有深拷贝,所以我们的析构函数要手动实现

采用递归函数

别忘记将_root置为nullptr

中序遍历


public:
void InOrder()
{
        _InOrder(_root);
        cout << endl;
}

private:
void _InOrder(BTNode* root)
{
        if (root == nullptr)
         return;

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

利用递归完成中序遍历

中序遍历:左节点  根节点   右节点

需要注意的是我们的中序遍历直接写的话需要访问类的私有成员_root   ,这样我们在类外面使用中序遍历就不能正常使用

所以我们设计1个

void InOrder()
{
        _InOrder(_root);
        cout << endl;
}

在类外使用中序遍历时  直接调用InOrder()   

 将  _InOrder(_root)函数规定成类的成员函数,可以访问_root   ,相当于在InOrder() 中调用_InOrder()   

而_InOrder() 实现方法是

void _InOrder(BTNode* root)
{
        if (root == nullptr)
         return;

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

如果root 为空,直接返回

采用递归的方法实现中序遍历

二叉树搜索的应用场景

key搜索场景

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。
场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。


场景2:检查⼀篇英文章单词拼写是否正确,将词库中所有单词放⼊二叉搜索树,读取文章中的单
词,查找是否在二叉搜索树中,不在则波浪线标红提示。

key/value搜索场景

每⼀个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树性质了,可以修改value。


场景1:简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。

key/value二叉搜索树代码实现

namespace key_value
{
	template<class K,class V>
	struct BTSNode
	{
		BTSNode(const K& key,const V& value)
			:_key(key)
			,_value(value)
			,_left(nullptr)
			,_right(nullptr)
		{ }
		K _key;
		V _value;
		BTSNode<K,V>* _left;
		BTSNode<K,V>* _right;
	};


	template<class K, class V>
	class BSTree
	{
		typedef BTSNode<K,V> BTNode;
	public:
		BSTree() = default;
		BSTree(BSTree<K,V>&t )
		{
			_root=Copy(t._root);
		}

		BSTree& operator=(BSTree tmp)
		{
			swap(_root, tmp._root);
			return *this;
		}

		~BSTree()
		{
			Destory(_root);
			_root = nullptr;
		}

		bool Insert(const K& key,const V& value)
		{
			if (_root == nullptr)
			{
				_root = new BTNode(key,value);
				return true;
			}
			BTNode* cur = _root;
			BTNode* parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
					return false;
			}
			if (parent->_key > key)
				parent->_left = new BTNode(key,value);
			else
				parent->_right = new BTNode(key,value);
			return true;
		}
  BTNode* Find(const K& key)
  {
	BTNode* 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)
		{
			BTNode* cur = _root;
			BTNode* parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
							
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}

							else
							{
								parent->_right = cur->_left;
							}
							
						}
						delete cur;
					}

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



					else
					{
						BTNode* replaceParent = cur;
						BTNode* replace = cur->_right;
						while (replace->_left)
						{
							replaceParent = replace;
							replace = replace->_left;
						}
						cur->_key = replace->_key;
						
						if (replaceParent->_left == replace)
						    replaceParent->_left = replace->_right;
					    else
						    replaceParent->_right = replace->_right;
						delete replace;

					}
						return true;
				}
			}
			return false;
		}
		BTNode* Copy(BTNode* root)
		{
			if (root == nullptr)
				return nullptr;
			BTNode* newRoot=new BTNode(root->key);
			newRoot->_left = Copy(root->_left);
			newRoot->_right = Copy(root->_right);
			return newRoot;
		}


		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		void Destory(BTNode* root)
		{
			if (root == nullptr)
				return;
			Destory(root->_left);
			Destory(root->_right);
			delete root;
		}
		private:
			void _InOrder(BTNode* root)
			{
				if (root == nullptr)
				 return;

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


	private:
		BTNode* _root=nullptr;
	};

}

BSTree() = default;

强制生成默认构造,因为在这段代码中已经有了拷贝构造,编译器就不会生成构造了,必须要强制生成默认构造

template<class K,class V>
	struct BTSNode
	{
		BTSNode(const K& key,const V& value)
			:_key(key)
			,_value(value)
			,_left(nullptr)
			,_right(nullptr)
		{ }
		K _key;
		V _value;
		BTSNode<K,V>* _left;
		BTSNode<K,V>* _right;
	};

新增加了一个值_value   每一个_key 对应一个_value   

_value 不参与到具体函数的实现逻辑,只是一个外显的值

相当于投屏的显示器。

所以代码和之前实现二叉搜索树的基本逻辑相同,在插入时别忘了_value

new BTNode(key,value);

注意:

查找函数的返回类型改变,逻辑不变

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

感谢花时间阅读这篇内容!

如果觉得有价值,欢迎点赞支持、收藏备用,或分享给同行。你的认可,是我持续输出高质量内容的最大动力。

我们下期再见喽!!!

更多推荐