【C++进阶:二叉树进阶】二叉搜索树的操作和key模型、key/value模型的实现 | 二叉搜索树的应用 | 二叉搜索树的性能分析

【写在前面】

从这里开始 C++ 的语法就告一段落了。二叉树在前面的数据结构和算法初阶中就讲过,本文取名为二叉树进阶是因为:

  • 二叉树进阶有着承上启下的作用,承上就是借助二叉搜索树,对二叉树初阶部分进行收尾总结,启下就是 map 和 set 的特性需要先铺垫二叉搜索树,二叉搜索树也是一种树形结构。
  • 对于二叉搜索树的特性了解,有助于更好的理解 map 和 set 的特性。
  • 二叉树中部分面试题稍微有些难度,在二叉树初阶讲解不适合。
  • 有些 OJ 题使用 C 语言实现比较麻烦。

我们之前说过,普通二叉树单纯的存储数据价值不大,因为仅仅是单纯的存储数据,你不如去使用顺序表和链表这样友善的多。它一定要套一种应用场景,二叉树的其中一个重要的应用场景就是我们本文中要学习的二叉搜索树。

一、二叉搜索树
💦 概念

二叉搜索树又称搜索二叉树或二叉排序树 (如果你对下图的二叉搜索树中序遍历,你会发现它是有序并且升序的,这是它的附带特征,实际我们要排序一组数据也不会使用它,因为其底层需要存储值以外的东西,以此来进行关联,后面加入平衡后还会存储平衡因子、颜色等,那排序直接使用 vector 不香嘛,它的存储是很纯粹的,注意搜索树这里严格来说并不是排序,而是排序 + 去重,因为搜索树一般不支持冗余,所以一般要排序也不会真的使用到搜索树,除非数据就在其中,恰好要排序 + 去重),我们当前学习的搜索二叉树一般是不允许修改的,不过下面我们会学习搜索二叉树的 key/value 模型,它支持修改,改的是 value。它或者是一棵空树,或者是具有以下特性的二叉树:

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

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

  • 它的左右子树也分别都是二叉搜索树。

    在这里插入图片描述

💦 二叉搜索树操作
1、查找

以前我们的普通二叉树查找时通常都是暴力查找,此时二叉搜索树的优势就体现了。

在这里插入图片描述

  • 若根节点不为空:

    如果 (根节点 key == 查找 key),返回 true;

    如果 (根节点 key > 查找 key),在其左子树查找;

    如果 (根节点 key < 查找 key),在其右子树查找;

    否则,返回 false;

  • 二叉搜索树结构查找一个值最多查找高度次。

2、插入

这个数据所插入的位置是唯一的,插入数据后,要保证二叉搜索树。注意默认情况下,二叉搜索树是不允许冗余的,比如下图不能再插入 8,但是后面 STL 对其改造后允许冗余。

插入具体过程如下:

在这里插入图片描述

3、删除

删除才是二叉搜索树比较麻烦的,也是值得我们去探讨的。删除数据后,要保证二叉搜索树。

首先查找删除的元素是否在二叉搜索树中,如果不存在,则返回,否则要删除的结点可能分下面四种情况:

  a. 要删除的节点无孩子节点;

  b. 要删除的节点只有左孩子节点;

  c. 要删除的节点只有右孩子节点;

  d. 要删除的节点有左右孩子节点;

a 的删除没啥,找到目标位置释放,并把目标位置的父亲的左或右置空;b 和 c 也还好,找到目标位置后,把目标位置的父亲的左或右关联到目标位置的孩子,再把目标位置释放;d 的删除有点啥,这里使用替代删除,也就是找左树的最大节点或者右树的最小节点,且左树的最大节点一定是最右节点,右树的最小节点一定是最左节点 (左右树只有一个节点除外),找到目标位置、找到替代位置后,把目标位置替换,把替代位置的父亲与替代位置的孩子关联,最后再把替代位置释放;看起来待删除节点有 4 种情况,实际情况 a 可以与情况 b 或者 c 合并起来,因此真正的删除过程应该如下:

  • 情况 ab,删除该节点且使被删除节点的双亲节点指向被删除节点的左孩子节点;
  • 情况 ac,删除该节点且使被删除节点的双亲节点指向被删除节点的右孩子节点;
  • 情况 d,在它的右子树中寻找中序下的第一个节点 (关键码最小),用它的值补到被删除节点中,再来处理该节点的删除问题;

在这里插入图片描述

💦 二叉搜索树key模型的实现
1、基本结构
namespace KEY
{
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;
	};
	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
	private:
		Node* _root = nullptr;
	};
}
  • 以前我们定义模板时命名都是 T,表示 type。而这里我们用 K,表示 key,它是 key 模型,下面我们还会变形一下讲 key/value 模型,搜索树通常要演化这两种形态,它们是常用的搜索模型。
2、Insert
bool Insert(const K& key)
{
	//空树,直接插入
	if(_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	//查找要插入的位置
	Node* cur = _root;
	while(cur)
	{
		if(cur->_key < key)//往右子树查找
		{
			cur = cur->_right;	
		}	
		else if(cur->_key > key)//往左子树查找
		{
			cur = cur->_left;	
		}
		else
		{
			return false;//默认不支持冗余	
		}
	}
	//插入? ? ?
	cur = new Node(key);
	return true;
}
  • 这里在找到要插入的位置后,new 了一个节点,并不代表已经插入成功,因为这个节点没有与父亲关联起来,且出了作用域就会内存泄漏,因为 cur 是唯一能标识这个节点的位置,而 cur 是一个局部变量,所以必须在 cur 销毁之前,将它们进行关联。

✔ 修正

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;//默认不支持冗余	
		}
	}
	//new节点
	cur = new Node(key);  
	if(parent->_key < cur->_key)//新节点链接到父的左还是右,还需要再比一次
	{
		parent->_right = cur;//关联
	}
	else
	{
		parent->_left = cur;//关联
	}
	return true;
}
3、InOrder
void InOrder(Node* root)
{
	if(root == nullptr)
	{
		return;	
	}
	InOrder(root->_left);
	cout << root->_key << " ";
	InOrder(root->_right);
}
  • 这里类里成员函数里面按以前的方法写递归会陷入一个困境中 —— InOrder 需要传树的根,但是根是私有的。你可以实现一个 GetRoot,然后 main 函数里调用时 t.InOrder(GetRoot()),但是这样怪怪的,你还要把根给暴露出去。这里不就是想拿到根嘛,更好的方式是 main 函数里 t.InOrder(),成员函数里面套一层无参的 InOrder 去调用上面有参的 InOrder。此时它们俩构成函数重载,但是这样不大好,所以把有参的 InOrder 改成 _InOrder,更规范点你还可以把它定义成私有的,只供内部使用。所以一般要访问成员变量,为了封装性,需要套一层。

✔ 修正

public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if(root == nullptr)
		{
			return;	
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
4、Find
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;	
}
  • Find 没有给布尔值是为了改造 key/value 结构
5、Erase
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)//ab
			{
				//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
				if(cur == parent->_left)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;	
				}
				delete cur;
			}
			else if(cur->_right == nullptr)//ac
			{
				//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
				if(cur == parent->_left)
				{
					parent->_left = cur->_left;	
				}
				else
				{
					parent->_right = cur->_left;	
				}
				delete cur;
			}
			else//d
			{
				//找右树的最小节点去替代删除
				Node* minRightParent = nullptr;
				//cur->right一定不为空
				Node* minRight = cur->_right;
				//最小节点
				while(minRight->_left)
				{
					minRightParent = minRight;
					minRight = minRight->_left;
				}
				//替代
				cur->_key = minRight->_key;
				//minRight的左一定为空,但右不一定为空,无论是否为空,minRightParent要指向minRight的右 ? ? ?
				minRightParent->_left = minRight->_right;
				delete minRight;
			}
			return true;
		}
	}	
	return false;
}
  • 在删除仅有一个孩子的节点时,也就是 ab 和 ac 情况时还需要再判断目标节点是父亲的左还是右,如果不这么做,就可能出现下面的 bug。
    在这里插入图片描述

  • 对于上面的代码在删除 7 时,程序会崩溃,因为 minRight 是 8,而 minRight->_left 是空,循环都没进去,minRightParent 也就没初始化喽,虽然能替代成功,但是在 minRightParent->_left 时会空指针解引用崩溃。解决方法就是定义 minRightParent 时给 cur,且释放节点之前节点的父亲和节点的孩子的关联需要再判断,之前我们认为 minRight 一定是 minRightParent 的左,其实不然。

    在这里插入图片描述

✔ 修正

else//d
{
	//找右树的最小节点去替代删除
	Node* minRightParent = cur;
	//cur->right一定不为空
	Node* minRight = cur->_right;
	//最小节点
	while(minRight->_left)
	{
		minRightParent = minRight;
		minRight = minRight->_left;
	}
	//替代
	cur->_key = minRight->_key;
	//minRight的左一定为空,但右不一定为空,minRightParent->_left不一定是minRight
	if(minRight == minRightParent->_left)//删除5,找6
	{
		minRightParent->_left = minRight->_right;
	}
	else//删除7,找8
	{
		minRightParent->_right = minRight->_right;
	}
	delete minRight;
}
  • 至此,我们删除情况 ab、ac、d 都没问题,但是当我们挨个对搜索二叉树的节点删除时,却发现程序崩溃了。我们在循环代码中添加 t.InOrder(),发现是删除最后一个值时崩溃的,因为此时 9 是根,循环里就直接走到删除的代码块中,去判断目标节点的左时,其中确定目标位置父亲的左还是右和目标位置的孩子关联时对空指针解引用,因为 parent 定义时是 nullptr。由此我们还发现了一种 parent 也是空的场景,先把根的左树全删除,再删除根,也会出现相同的问题。解决方法就是如果目标节点是根,也就是没有父亲,就让它的左或右作根。
    在这里插入图片描述

✔ 修正

else
{
	//删除
	if(cur->_left == nullptr)//ab
	{
		//没有父亲
		if(cur == _root)
		{
			_root = cur->_right;//右作根
		}
		else
		{
			//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
			if(cur == parent->_left)
			{
				parent->_left = cur->_right;
			}
			else
			{
				parent->_right = cur->_right;	
			}
		}
		delete cur;
	}
	else if(cur->_right == nullptr)//ac
	{
		//没有父亲
		if(cur == _root)
		{
			_root = cur->_left;
		}
		else
		{
			//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
			if(cur == parent->_left)
			{
				parent->_left = cur->_left;	
			}
			else
			{
				parent->_right = cur->_left;	
			}
		}
		delete cur;
	}
}	
6、InsertR
public:
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
private:
	bool _InsertR(Node* root, const K& key)
	{
		if(root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		else
		{
			if(root->_key < key)
			{
				return _InsertR(root->_right, key);	
			}
			else if(root->_key > key)
			{
				return _InsertR(root->_left, key);	
			}
			else
			{
				return false;	
			}
		}
	}
  • 其实能用循环那就用循环,你可以认为递归大白话就是你少做一些事情,编译器多做一些事情,虽然使用递归代码量少了不少,但是却是编译器负重前行换来的,因为递归需要频繁的建立栈帧,且递归太深,有可能 stackoverflow,那么就得不偿失了,所以这里一般我们也不会实现递归版本,因为如果这棵树比较复杂,那么开销就会非常大,而这里依然要实现的原因是下面的引用用的很巧妙。
  • 这里的 root 是一个局部变量,出了作用域就销毁了,所以需要在销毁前与它的父亲链接。这里找父亲的方式有很多种,比如可以多增加一个参数,每次递归把当前的 root 传给 rootParent,最后再比较链接;或者将参数 Node* root 改为 Node*& root,就已经插入成功了,因为如下图,目标位置是 9 的 _right 的别名,此时再 new 节点给 root,就直接跟 9 链接起来了,且第一次插入时 _root 为空,此时 root 就是 _root 的别名。
    在这里插入图片描述

✔ 修正

public:
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
private:
	bool _InsertR(Node*& root, const K& key)
	{
		if(root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		else
		{
			if(root->_key < key)
			{
				return _InsertR(root->_right, key);	
			}
			else if(root->_key > key)
			{
				return _InsertR(root->_left, key);	
			}
			else
			{
				return false;	
			}
		}
	}
7、FindR
public:
	Node* FindR(const K& key)
	{
		return _FindR(_root, key);
	}
private:
	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;	
		}
	}			
8、EraseR
public:
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);	
	}
private:
	bool _EraseR(Node*& root, const K& key)
	{	
		if(root == nullptr)
		{
			return false;	
		}
		else if(root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if(root->_left > key)
		{
			return _EraseR(root->_left, key);	
		}
		else
		{
			//删除
			Node* del = root;
			if(root->_left == nullptr)//ab
			{
				root = root->_right;
			}
			else if(root->_right == nullptr)//ac
			{
				root = root->_left;
			}
			else//d
			{
				//替代
				Node* minRight = root->_right;
				while(minRight->_left)
				{
					minRight = minRight->_left;	
				}
				root->_key = minRight->_key; 
				//大事化小,小事化了
				return _EraseR(root->_right, minRight->_key);
			}
			delete del;
			return true;
		}
	}
  • 可以看到这里递归的参数使用引用的方式,代码量少了很多,你甚至在情况 ab、ac 时只要找到目标节点,不用再判断目标节点父亲的左还是右。比如这里要删除 8,程序走到外层 else 时,这里的 root 不仅是 7 节点 _right 的别名,也是 8 节点的地址,7 节点 _right 的别名意味着 root 的改变就是 7 节点的 _right 的改变,8 节点的地址就意味着可以拿到 9 节点,它们并不矛盾,此时 root = root->_right 就可以完成链接。并且这里针对于先把 5 的左树删除再删除 5 的情况也能解决,解决点是套了一层 EraseR,root 是 _root 的别名,所以 root->_key == key,程序就走到 else,root 的左为空,root = root->_right,也就是 _root 的 _right 指向 7 节点。
    在这里插入图片描述
  • 对于 d 情况,引用就没用了,因为要找替代节点。但是还是能用点递归的,找到替代的节点替代后,现在要删除替代节点,想想递归的思想 “ 大事化小,小事化了 ”,比如要删除 5,替代完后就转换为以 7 节点为根删除 6。这里要明白的是转换为递归删除右子树的 minright,如果右子树越大,付出的代价就越大,同时这里二叉树进阶如果使用 C 语言来实现,那么这里就得传二级指针,它相对引用理解起来就费劲多了。
    在这里插入图片描述
9、构造函数和析构函数和拷贝构造函数和赋值重载的实现
public:
	//BSTree() = default;//C++11
	BSTree()
		: _root(nullptr)
	{}
	~BSTree()
	{
		_Destroy(_root);	
	}
	//BSTree(const BSTree& t)
	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}
	//BSTree& operator=(BSTree& t)
	BSTree<K>& operator=(BSTree<K>& t)//现代写法 t1 = t2
	{
		std::swap(_root, t._root);
		return *this;
	}
private:
	Node* _Copy(Node* root)
	{
		if(root = nullptr)
		{
			return nullptr;	
		}
		//深拷贝根
		Node* newRoot = new Node(root->_key);
		//递归拷贝左右子树
		newRoot->_left = _Copy(root->_left);
		newRoot->_right = _Copy(root->_right);
		//返回根
		return newRoot;
	}
	void _Destroy(Node* root)
	{
		if(root == nullptr)
		{
			return;	
		}	
		//后序
		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
	}
  • 构造函数我们可以不实现,_root 直接给 nullptr 缺省值。
  • 析构函数需要实现,这里我们使用后序的方式进行处理。
  • 拷贝构造函数也需要实现,但是对于树而言的拷贝构造代价就大的多了,这里需要做深拷贝。
  • 赋值重载也需要实现,这里采用现代写法,前提需要实现拷贝构造。这里 t 是 t2 的拷贝,this 是 t1 的拷贝,t 是 t1 想要的,把 t 的根与 t1 的根交换,t1 和 t2 就是相同的,t 是局部的,出了作用域自动调用析构函数。
  • 因为写了拷贝构造,所以这里会报 “ 没有默认的构造函数可用 ”,这里除了写默认构造函数,还有一种方法是 C++11 的语法,它会强制编译器自动生成。
  • 这里要注意有些地方写拷贝构造时参数那会省略 <K>,用的是类名,没用模板的类型,这样是可以的,包括下面的赋值重载,但是还是不建议这样写,这里只是了解有这种写法。
10、完整代码

🧿 BinarySearchTree.h

#pragma once
#include<iostream>
using namespace std;

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)
		{}
	};
	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		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;//默认不支持冗余	
				}
			}
			//new节点,这里需要在BSTreeNode补构造函数
			cur = new Node(key);
			if (parent->_key < cur->_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)
		{
			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)//ab
					{
						//没有父亲
						if (cur == _root)
						{
							_root = cur->_right;//右作根
						}
						else
						{
							//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
							if (cur == parent->_left)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}
					else if (cur->_right == nullptr)//ac
					{
						//没有父亲
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					else//d
					{
						//找右树的最小节点去替代删除
						Node* minRightParent = cur;
						//cur->right一定不为空
						Node* minRight = cur->_right;
						//最小节点
						while (minRight->_left)
						{
							minRightParent = minRight;
							minRight = minRight->_left;
						}
						//替代
						cur->_key = minRight->_key;
						//minRight的左一定为空,但右不一定为空,minRightParent->_left不一定是minRight
						if (minRight == minRightParent->_left)//删除5,找6
						{
							minRightParent->_left = minRight->_right;
						}
						else//删除7,找8
						{
							minRightParent->_right = minRight->_right;
						}
						delete minRight;
					}
					return true;
				}
			}
			return false;
		}
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}
		//BSTree() = default;//C++11
		BSTree()
			: _root(nullptr)
		{}
		~BSTree()
		{
			_Destroy(_root);
		}
		//BSTree(const BSTree& t)
		BSTree(const BSTree<K>& t)
		{
			_root = _Copy(t._root);
		}
		//BSTree& operator=(BSTree t)
		BSTree<K>& operator=(BSTree<K> t)//现代写法 t1 = t2
		{
			std::swap(_root, t._root);
			return *this;
		}
	private:
		Node* _Copy(Node* root)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			//深拷贝根
			Node* newRoot = new Node(root->_key);
			//递归拷贝左右子树
			newRoot->_left = _Copy(root->_left);
			newRoot->_right = _Copy(root->_right);
			//返回根
			return newRoot;
		}
		void _Destroy(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			//后序
			_Destroy(root->_left);
			_Destroy(root->_right);
			delete root;
		}
		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			else if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_left > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				//删除
				Node* del = root;
				if (root->_left == nullptr)//ab
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)//ac
				{
					root = root->_left;
				}
				else//d
				{
					//替代
					Node* minRight = root->_right;
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}
					root->_key = minRight->_key;
					//大事化小,小事化了
					return _EraseR(root->_right, minRight->_key);
				}
				delete del;
				return true;
			}
		}
		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;
			}
		}
		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				root = new Node(key);
				return true;
			}
			else
			{
				if (root->_key < key)
				{
					return _InsertR(root->_right, key);
				}
				else if (root->_key > key)
				{
					return _InsertR(root->_left, key);
				}
				else
				{
					return false;
				}
			}
		}
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};
}

🧿 Test.cpp

#include"BinarySearchTree.h"

void TestBSTree1()
{
	int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
	KEY::BSTree<int> t;
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

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

	t.Erase(5);
	t.InOrder();
}
void TestBSTree2()
{
	int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
	KEY::BSTree<int> t;
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	//挨个删除
	for (auto e : a)
	{
		t.Erase(e);
		t.InOrder();
	}
	//先删除左树,再删除根
	/*t.Erase(0);
	t.Erase(1);
	t.Erase(2);
	t.Erase(3);
	t.Erase(4);
	t.Erase(5);
	t.InOrder();*/
}
void TestBSTree3()
{
	int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
	KEY::BSTree<int> t;
	for (auto e : a)
	{
		t.InsertR(e);
	}
	t.InOrder();

	//挨个删除
	for (auto e : a)
	{
		t.Erase(e);
		t.InOrder();
	}
	//先删除左树,再删除根
	/*t.Erase(0);
	t.Erase(1);
	t.Erase(2);
	t.Erase(3);
	t.Erase(4);
	t.Erase(5);
	t.InOrder();*/
}
void TestBSTree4()
{
	int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
	KEY::BSTree<int> t;
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	KEY::BSTree<int> copy = t;
	copy.InOrder();

	KEY::BSTree<int> assign;
	assign = t;
	assign.InOrder();
}
int main()
{
	TestBSTree1();
	cout << "-------------------" << endl;
	TestBSTree2();
	cout << "-------------------" << endl;
	TestBSTree3();
	cout << "-------------------" << endl;
	TestBSTree4();

	return 0;
}
二、二叉搜索树的应用

二叉搜索树在实际中应用的场景有两个 —— key 的查找模型和 key/value 的查找模型,其中 key 的查找模型对应 STL 中的 set,它确定一个值在不在一个集合中,类似学生宿舍楼的门禁,你刷卡时,就是在系统中查找你的卡号,卡号就是 key;key/value 的查找模型对应 STL 中的 map,类似高铁站刷身份证进站,这里就是通过身份证号查找用这个身份证号买的车票信息,身份证号就是 key,车票信息就是 value。

💦 二叉搜索树key/value模型的实现
1、基本结构
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:
	private:
		Node* _root = nullptr;
	};
}
2、Insert
bool Insert(const K& key, const V& value)
{
	//空树,直接插入
	if (_root == nullptr)
	{
		_root = new Node(key, value);
		return true;
	}
	//查找要插入的位置
	//...
	//new节点,这里需要在BSTreeNode补构造函数
	cur = new Node(key, value);
	//...
}
3、Find
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;
}
  • 查找不需要改变,这里查找后把节点返回,就意味着有 key 也有 value。
4、Erase
//...
  • 删除不需要改变,无论是 key 还是 key/value 模型,都是用 key 去删除。
5、InOrder
public:
	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);
	}
  • 中序里就把 _value 也输出。
10、完整代码

🧿 BinarySearchTree.h

#pragma once
#include<iostream>
#include<string>
using namespace std;

namespace KEY
{
	//...
}
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:
		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;//默认不支持冗余	
				}
			}
			//new节点,这里需要在BSTreeNode补构造函数
			cur = new Node(key, value);
			if (parent->_key < cur->_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)
		{
			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)//ab
					{
						//没有父亲
						if (cur == _root)
						{
							_root = cur->_right;//右作根
						}
						else
						{
							//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
							if (cur == parent->_left)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}
					else if (cur->_right == nullptr)//ac
					{
						//没有父亲
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							//确定目标位置父亲的左还是右和目标位置的孩子关联(目标位置的左右孩子在外层if已经确定了)
							if (cur == parent->_left)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					else//d
					{
						//找右树的最小节点去替代删除
						Node* minRightParent = cur;
						//cur->right一定不为空
						Node* minRight = cur->_right;
						//最小节点
						while (minRight->_left)
						{
							minRightParent = minRight;
							minRight = minRight->_left;
						}
						//替代
						cur->_key = minRight->_key;
						//minRight的左一定为空,但右不一定为空,minRightParent->_left不一定是minRight
						if (minRight == minRightParent->_left)//删除5,找6
						{
							minRightParent->_left = minRight->_right;
						}
						else//删除7,找8
						{
							minRightParent->_right = minRight->_right;
						}
						delete minRight;
					}
					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;
	};
}

🧿 Test.cpp

#include"BinarySearchTree.h"

void TestBSTree1()
{
	KEY_VALUE::BSTree<string, string> dict;
	dict.Insert("sort", "排序");
	dict.Insert("insert", "插入");
	dict.Insert("tree", "树");
	dict.Insert("left", "左边");
	dict.Insert("right", "右边");
	//...
	string str;
	while(cin >> str)
	{
		if(str == "Q")
		{
			break;	
		}
		else
		{
			auto ret = dict.Find(str);
			if(ret == nullptr)
			{
				cout << "拼写错误,请检查你的单词" << endl;	
			}
			else
			{
				cout << ret->_key <<  "->" << ret->_value << endl;	
			}
		}
	}
}
void TestBSTree2()
{
	string str[] = {"sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" };
	KEY_VALUE::BSTree<string, int> countTree;
	for(auto& e : str)
	{
		auto ret = countTree.Find(e);
		if(ret == nullptr)
		{
			countTree.Insert(e, 1);//第一次出现
		}	
		else
		{
			ret->_value++;//非第一次出现
		}
	}
	countTree.InOrder();
}
int main()
{
	TestBSTree1();
	TestBSTree2();
	
	return 0;
}
  • 为了改造方便,突出重点,一些不是很必要的接口就省略了。
  • 实际 key/value 模型的场景很多,这里 TestBSTree1 简单测试下字典。
  • 还有一种场景不容易想到,TestBSTree2 是统计每个单词出现的次数,以前是先排序,写一个循环,相同就 ++,这是不好统计的。而这里我们借助二叉搜索树统计,当然以后还有很多其它的方式可以进行统计,这里统计字符串出现的次数也是经典的 key/value 模型。这里是利用二叉搜索树是不支持冗余的特性统计的,所以第一次出现的单词直接插入,非第一次出现的单词就 _value++。
三、二叉搜索树的性能分析

实际二叉搜索树并不常用,因为二叉搜索树有缺陷,上面我们说二叉搜索树结构查找一个值最多查找高度次,同时我们知道只有满二叉树才能控制它是 log2N,完全二叉树也接近 log2N,但是二叉搜索树不一定是满二叉树或完全二叉树,因为对于同一个关键码集合,如果关键码插入的次序不同,可能得到不同结构的二叉搜索树:

在这里插入图片描述

  • 最好情况下,二叉搜索树为完全二叉树,时间复杂度为 log2N。
  • 最坏情况下,二叉搜索树退化为单支树,时间复杂度为 N。
  • 二叉搜索树理论上比顺序表、链接更快,但是它非常依赖树的形状。
  • 如果退化成单支树,那么二叉搜索树的性能就失去了,所以有人就对此进行控制和改进,使树的形状平衡,这里就引出了数据结构中最难的两棵树叫 AVL 树和红黑树,后面还要拿这两个棵去封装 map 和 set 以此来了解 STL。最后使得无论按照什么次序插入关键码,都可以使二叉搜索树的性能最佳。
  • 11
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
判断给定二叉树是否为二叉排序树的方法如下: 1. 对于每个节点,其左子树所有节点的值都小于该节点的值,右子树所有节点的值都大于该节点的值。 2. 对于整棵树,每个节点都满足上述条件。 可以采用中序遍历的方法依次比较每个节点的值,如果中序遍历的结果是单调递增的,则该树为二叉排序树。以下是基于此方法的 C++ 代码实现: ```c++ #include <iostream> #include <vector> #include <stack> using namespace std; // 二叉树结点定义 struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; // 中序遍历二叉树 void inorderTraversal(TreeNode* root, vector<int>& v) { stack<TreeNode*> s; TreeNode* p = root; while (p != NULL || !s.empty()) { while (p != NULL) { s.push(p); p = p->left; } if (!s.empty()) { p = s.top(); s.pop(); v.push_back(p->val); p = p->right; } } } // 判断二叉树是否为二叉排序树 bool isBST(TreeNode* root) { if (root == NULL) return true; vector<int> v; inorderTraversal(root, v); for (int i = 1; i < v.size(); i++) { if (v[i] <= v[i-1]) return false; } return true; } // 测试 int main() { TreeNode* root = new TreeNode(5); root->left = new TreeNode(3); root->right = new TreeNode(6); root->left->left = new TreeNode(2); root->left->right = new TreeNode(4); root->right->right = new TreeNode(7); if (isBST(root)) { cout << "该二叉树二叉排序树" << endl; } else { cout << "该二叉树不是二叉排序树" << endl; } return 0; } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

跳动的bit

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

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

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

打赏作者

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

抵扣说明:

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

余额充值