[算法]二叉搜索树(BST)

二叉搜索树(Binary Search Tree),也称二叉排序树或二叉查找树。

一、二叉搜索树的性质

二叉搜索树是一棵二叉树,可以为空。

当二叉搜索树不为空时:

1、非空左子树的所有键值小于其根结点的键值。

2、非空右子树的所有键值大于其根结点的键值。

3、左、右子树都是二叉搜索树。

假如以序列8, 3, 1, 10, 6, 4, 7, 14, 13依次插入到二叉搜索树中,其二叉树的树状图为:

二、二叉搜索树的抽象数据类型定义

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 二叉树:二叉搜索树(Binary Search Tree) |
| 二叉搜索树 (BST) 是一种特殊类型的二叉树,其中每个顶点最多可以有两个子项。此结构遵循 BST 属性规定给定顶点的左侧子树中的每个结点的值必须小于给定顶点的值,并且右侧子树中的每个结点的值必须大于给定顶点的值。 |
| **模板类:**template<class T> **二叉树结点:**BSTNode **模板类:**template<class T> **搜索二叉树:**BSTree **二叉树节点:**typedef BSTNode<T> Node; 二叉搜索树操作: **构造:**BSTree(); 析构:~BSTree(); **查找:**Node* find(const T& key); **插入:**bool insert(const T& x); **删除:**bool erase(const T& key); **中序遍历结点并打印关键值:**void inOrder(); |

抽象数据类型定义的具体代码:

cpp 复制代码
//二叉搜索树的结点
template<class T>
struct BSTNode
{
	T _val; //结点存储的数据
	BSTNode<T>* _pLeft; //指向左孩子的指针
	BSTNode<T>* _pRight; //指向右孩子的指针

	//构造函数
	BSTNode(const T& val = T()):_val(val),_pLeft(nullptr),_pRight(nullptr){}
};

//二叉搜索树
template<class T>
class BSTree
{
private:
	typedef BSTNode<T> Node;
	Node* _root;//根结点
public:
	//构造
	BSTree(); 

	//查找
	Node* find(const T& key);

	//插入
	bool insert(const T& x);

	//删除
	bool erase(const T& key);

	//中序遍历节点并打印val
	void inOrder();

	//析构
	~BSTree();
};

二叉搜索树的构造、析构、以及中序遍历结点的实现直接给出,不再赘述。

cpp 复制代码
//二叉搜索树
template<class T>
class BSTree
{
private:
	typedef BSTNode<T> Node;
	Node* _root;//根结点

	//中序递归遍历
	void _inOrder(BSTNode<T>* root)
	{
		if (!root)
			return;

		_inOrder(root->_pLeft);
		cout << root->_val << " ";
		_inOrder(root->_pRight);
	}

	//后序遍历释放结点
	void destroy(BSTNode<T>* root)
	{
		if (!root)
			return;

		destroy(root->_pLeft);
		destroy(root->_pRight);
		delete root;
	}

public:
	//构造
	BSTree():_root(nullptr){}

	//查找
	Node* find(const T& key);

	//插入
	bool insert(const T& x);

	//删除
	bool erase(const T& key);

	//中序遍历节点并打印val
	void inOrder()
	{
		_inOrder(_root);
	}

	//析构
	~BSTree()
	{
		destroy(_root);
		_root = nullptr;
	}
};

三、二叉搜索树各操作的实现

(一)、查找

根据二叉搜索树的性质,查找某个关键值时,将关键值和顶点的值作比较,如果比顶点的值小,那么去到左子树继续进行查找,如果比顶点的值大,去到右子树继续进行查找,如此重复......如果关键值和顶点的值相等,那么该结点即为我们所要找的结点,返回该结点的指针,如果遍历到了叶子结点之后还不匹配,那么查找结束,该二叉搜索树不存在待查找的值,返回空。

以下面这棵树为例,演示一下查找4的过程。

比较4和8,4小于8,所以在左边搜索。

比较4和3,4大于3,所以在右边搜索。

比较4和6,4小于6,所以在左边搜索。

找到值4。

代码实现:

cpp 复制代码
//查找
Node* find(const T& key)
{
	Node* pCur = _root;

	while (pCur)
	{
		if (key > pCur->_val) //key比val大,去右边查找
			pCur = pCur->_pRight;
		else if (key < pCur->_val) //key比val小,去左边查找
			pCur = pCur->_pLeft;
		else //key和val相等
			return pCur; //返回该节点的指针
	}

	//找不到
	return nullptr;
}

查找的时间复杂度分析

由于二叉树的性质,二叉搜索树的查找次数最多为该二叉树的高度次,但是其时间复杂度并不为,只有当二叉搜索树近似一棵完全二叉树 时,其查找的时间复杂度才为,如果二叉树退化为一棵斜二叉树 ,那么其查找的时间复杂度为。从平均性能上来看,普通的搜索二叉树的查找时间复杂度为

(二)、插入

插入操作和上面的查找操作差不多,只要找到合适的空位插入即可,当待插入的值小于当前顶点的值时,往左走,当待插入的值大于顶点的值时,往右走,如此重复,当遍历到空时,这个时候的位置就是待插入结点所应该插入的位置,将待插入的结点和该位置的双亲结点连接起来即可。

下面是在二叉搜索树中插入一个5的过程:

比较5和8,5小于8,往左走。

比较5和3,5大于3,往右走。

比较5和6,5小于6,往左走。

比较5和4,5大于4,往右走。由于走到了尽头,则将5插入到4的右边。

代码的具体实现:

cpp 复制代码
//插入
bool insert(const T& x)
{
	//如果为空树,则待插入的结点即为根节点
	if (!_root)
	{
		_root = new Node(x);
		return true;
	}

	Node* pParent = nullptr;	//记录pCur的双亲节点
	Node* pCur = _root;	

	//找到待插入结点该插入的位置
	while (pCur)
	{
		if (x > pCur->_val) //x比val大,去右边
		{
			pParent = pCur;
			pCur = pCur->_pRight;
		}
		else if (x < pCur->_val) //x比val小,去左边
		{
			pParent = pCur;
			pCur = pCur->_pLeft;
		}	
		else //x和val相等,结点已经存在,插入失败
			return false;
	}

	//连接待插入的结点和双亲结点
	if (x > pParent->_val) //x的值大于双亲的值,插入到右边去
		pParent->_pRight = new Node(x);
	else //x的值小于双亲的值,插入到左边去
		pParent->_pLeft = new Node(x);
	
	return true;
}

代码实现需要注意的细节:

1、特殊处理空树的情况,如果是空树,那么待插入的结点即为根节点。

2、在往下遍历二叉树时,需要额外声明定义一个指向双亲结点的指针,用于保存双亲结点的位置以连接待插入的结点。

下面是对二叉搜索树进行阶段性测试的代码:

cpp 复制代码
void test1()
{
	BSTree<int> t1;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };

	//遍历数组插入数据,创建一棵二叉搜索树
	for (auto e : a)
	{
		t1.insert(e);
	}

	//中序遍历结点打印数据
	t1.inOrder();
	
	cout << endl;

	//查找结点
	BSTNode<int>* ret;
	ret = t1.find(4);

	if (ret)
		cout << ret->_val << endl;
}

运行结果:

1 3 4 6 7 8 10 13 14
4

因为左子树的值小于根结点的值,右子树的值大于根结点的值,所以按照中序来遍历二叉搜索树的所有结点就能得到一个有序的序列,所以二叉搜索树又叫二叉排序树。

(三)、删除

搜索二叉树的删除操作比较复杂,需要分情况讨论。

情况1:删除的结点是叶子节点

这种情况删除比较简单,直接删除掉叶子结点,并将其双亲结点中原本指向该结点的指针置空即可。


情况2:删除的结点有且只有一个孩子(即有一个子树)

将其双亲结点中原本指向待删除的结点的指针指向待删除的结点的孩子结点,然后将待删除的结点删除,结果相当于待删除的结点将孩子托付给了双亲照顾。

这种情况还有一种比较特殊的情况需要处理:当待删除的结点是根结点时,待删除的根节点没有双亲结点可以托付自己的孩子。这个时候只需要将其待删除的根结点直接删除,让其孩子成为新的根结点即可。


另外,情况一和情况二可以合为一种情况,因为情况一中相当于把叶子结点的孩子(nullptr)托付给了双亲结点,达到了删除叶子结点并将其双亲中原本指向待删除的结点的指针置为空的目的。
情况3:删除的结点具有两个孩子 (左子树和右子树都存在)

这种情况下直接原地删除待删除的结点比较麻烦,我们可以将其转化为情况二来解决。我们可以从待删除的结点的右子树中找最小的值(或者左子树中找最大的值)和待删除的结点的值进行交换,交换后,除了待删除的值,其结果仍然满足二叉搜索树的结构。

要找到右子树的最小值,只需要从其右子树的根节点开始,一直往左走,走到不能再走为止,该尽头处的结点就是右子树的最小值的结点了,这个结点的位置称为右子树中最小值的结点位置 ,这个结点一定没有左孩子(右孩子可能有也可能没有),所以删除它的话就相当于情况2一样:当右子树中最小结点和待删除的结点的值交换后,我们需要将右子树的最小值位置的结点的右孩子托付给其双亲结点,再删除右子树中最小值所在位置的结点,这样就达到了删除我们想要删除的结点的目的了。

下面是删除二叉树中的3的示意图:

需要注意的是:不一定都是把双亲结点的左指针与右子树最小值位置的结点的右孩子相连,还可能是双亲结点的右指针与右子树最小值位置的结点的右孩子进行相连。比如说下面这种情况删除3,是将其与双亲节点的右指针相连。

代码实现:

cpp 复制代码
//删除
bool erase(const T& key)
{
	Node* pParent = nullptr;	//记录pCur的双亲节点
	Node* pCur = _root;

	//先找到待删除的结点和其双亲结点
	while (pCur)
	{
		if (key > pCur->_val)
		{
			pParent = pCur;
			pCur = pCur->_pRight;
		}
		else if (key < pCur->_val)
		{
			pParent = pCur;
			pCur = pCur->_pLeft;
		}
		else
			break;
	}

	//如果待删除的结点不存在或搜索二叉树为空
	if (!pCur)
		return false;

	//情况一和情况二:删除的结点只有一个孩子
	if (pCur->_pLeft == nullptr)	//左孩子为空,待删除的结点只有右孩子
	{
		//情况二的特殊情况处理:如果删除的结点是根结点
		if (pCur == _root)
		{
			_root = pCur->_pRight; //右孩子成为新的结点
			delete pCur;
			return true;
		}

		if (key > pParent->_val) //待删除的结点比双亲结点的值大
		{
			//双亲的右指针指向待删除结点的右孩子
			pParent->_pRight = pCur->_pRight;
			delete pCur;
			return true;
		}
		else //待删除的结点比双亲结点的值小
		{
			//双亲的左指针指向待删除结点的右孩子
			pParent->_pLeft = pCur->_pRight;
			delete pCur;
			return true;
		}
	}
	else if (pCur->_pRight == nullptr) //右孩子为空,待删除的结点只有左孩子
	{
		//情况二的特殊情况处理:如果删除的结点是根结点
		if (pCur == _root)
		{
			_root = pCur->_pLeft; //左孩子成为新的结点
			delete pCur;
			return true;
		}

		if (key > pParent->_val) //待删除的结点比双亲结点的值大
		{
			//双亲的右指针指向待删除结点的左孩子
			pParent->_pRight = pCur->_pLeft;
			delete pCur;
			return true;
		}
		else //待删除的结点比双亲结点的值小
		{
			//双亲的左指针指向待删除结点的左孩子
			pParent->_pLeft = pCur->_pLeft;
			delete pCur;
			return true;
		}
	}

	//情况三,待删除的结点有左孩子和右孩子
	Node* pRightMinParent = pCur;	//记录右子树中最小值结点的双亲节点
	Node* pRightMin = pCur->_pRight;	//指向右子树中最小值的结点

	//不断往左走,直到走到尽头为止
	while (pRightMin->_pLeft)
	{
		pRightMinParent = pRightMin;
		pRightMin = pRightMin->_pLeft;
	}

	//将待删除的结点的值和右子树中最小值的结点交换
	pCur->_val = pRightMin->_val;

	
	if (pRightMinParent->_pLeft == pRightMin) //双亲结点的左指针指向最小值结点
	{
		pRightMinParent->_pLeft = pRightMin->_pRight;	//双亲结点左指针指向待删除结点的右孩子
		delete pRightMin;
	}
	else //双亲结点的右指针指向最小值结点
	{
		pRightMinParent->_pRight = pRightMin->_pRight;	//双亲结点右指针指向待删除结点的右孩子
		delete pRightMin;
	}

	return true;
}

测试代码:

cpp 复制代码
void test2()
{
	BSTree<int> t1;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };

	//遍历数组插入数据,创建一棵二叉搜索树
	for (auto e : a)
	{
		t1.insert(e);
	}

	t1.inOrder();
	cout << endl; 

	//测试删除结点
	for (auto e : a)
	{
		t1.erase(e);
		t1.inOrder();
		cout << endl;
	}
}

运行结果:

1 3 4 6 7 8 10 13 14

1 3 4 6 7 10 13 14

1 4 6 7 10 13 14

4 6 7 10 13 14

4 6 7 13 14

4 7 13 14

7 13 14

13 14

13

四、二叉搜索树的应用

1、K模型:K模型即只有key作为关键码,结点的结构中只需要存储Key即可,关键码即为需要搜索到的值。例如上面的代码展示的就是K模型,测试代码存储的关键码为整形数据。
应用:存储任意数据类型进行查找或排序。
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
应用:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
3、具有去重、查找、顺带排序的功能。
去重:因为二叉搜索树不会将已存在的关键值插入到二叉树中,所以将一组序列组成二叉树搜索树可以达到去重的目的。
查找:由于二叉搜索树的特殊性质,当二叉树没有退化成斜树或接近完全二叉树时的查找效率高。
排序:由于二叉树的特殊性质,当中序遍历二叉树的每个结点可以得到有序的序列。

下面是修改成键值对后的代码:

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

//二叉搜索树的结点
template<class K,class V>
struct BSTNode
{
	K _key; //关键值
	V _val; //关键值所对应的值
	BSTNode<K,V>* _pLeft; //指向左孩子的指针
	BSTNode<K,V>* _pRight; //指向右孩子的指针

	//构造函数
	BSTNode(const K& key = K(),const V& val = V()):_key(key), _val(val), _pLeft(nullptr), _pRight(nullptr) {}
};

//二叉搜索树
template<class K, class V>
class BSTree
{
private:
	typedef BSTNode<K,V> Node;
	Node* _root;//根结点

	//中序递归遍历
	void _inOrder(Node* root)
	{
		if (!root)
			return;

		_inOrder(root->_pLeft);
		cout << root->_val << " ";
		_inOrder(root->_pRight);
	}

	//后序遍历释放结点
	void destroy(Node* root)
	{
		if (!root)
			return;

		destroy(root->_pLeft);
		destroy(root->_pRight);
		delete root;
	}

public:
	//构造
	BSTree():_root(nullptr){}

	//查找
	Node* find(const K& key)
	{
		Node* pCur = _root;

		while (pCur)
		{
			if (key > pCur->_key) //key比key大,去右边查找
				pCur = pCur->_pRight;
			else if (key < pCur->_key) //key比key小,去左边查找
				pCur = pCur->_pLeft;
			else //key和val相等
				return pCur; //返回该节点的指针
		}

		//找不到
		return nullptr;
	}


	//插入
	bool insert(const K& key = K(), const V& val = V())
	{
		//如果为空树,则待插入的结点即为根节点
		if (!_root)
		{
			_root = new Node(key,val);
			return true;
		}

		Node* pParent = nullptr;	//记录pCur的双亲节点
		Node* pCur = _root;	

		//找到待插入结点该插入的位置
		while (pCur)
		{
			if (key > pCur->_key) //x比key大,去右边
			{
				pParent = pCur;
				pCur = pCur->_pRight;
			}
			else if (key < pCur->_key) //x比key小,去左边
			{
				pParent = pCur;
				pCur = pCur->_pLeft;
			}	
			else //x和val相等,结点已经存在,插入失败
				return false;
		}

		//连接待插入的结点和双亲结点
		if (key > pParent->_key) //x的值大于双亲的值,插入到右边去
			pParent->_pRight = new Node(key, val);
		else //x的值小于双亲的值,插入到左边去
			pParent->_pLeft = new Node(key,val);
		
		return true;
	}

	//删除
	bool erase(const K& key)
	{
		Node* pParent = nullptr;	//记录pCur的双亲节点
		Node* pCur = _root;

		//先找到待删除的结点和其双亲结点
		while (pCur)
		{
			if (key > pCur->_key)
			{
				pParent = pCur;
				pCur = pCur->_pRight;
			}
			else if (key < pCur->_key)
			{
				pParent = pCur;
				pCur = pCur->_pLeft;
			}
			else
				break;
		}

		//如果待删除的结点不存在或搜索二叉树为空
		if (!pCur)
			return false;

		//情况一和情况二:删除的结点只有一个孩子
		if (pCur->_pLeft == nullptr)	//左孩子为空,待删除的结点只有右孩子
		{
			//情况二的特殊情况处理:如果删除的结点是根结点
			if (pCur == _root)
			{
				_root = pCur->_pRight; //右孩子成为新的结点
				delete pCur;
				return true;
			}

			if (key > pParent->_key) //待删除的结点比双亲结点的值大
			{
				//双亲的右指针指向待删除结点的右孩子
				pParent->_pRight = pCur->_pRight;
				delete pCur;
				return true;
			}
			else //待删除的结点比双亲结点的值小
			{
				//双亲的左指针指向待删除结点的右孩子
				pParent->_pLeft = pCur->_pRight;
				delete pCur;
				return true;
			}
		}
		else if (pCur->_pRight == nullptr) //右孩子为空,待删除的结点只有左孩子
		{
			//情况二的特殊情况处理:如果删除的结点是根结点
			if (pCur == _root)
			{
				_root = pCur->_pLeft; //左孩子成为新的结点
				delete pCur;
				return true;
			}

			if (key > pParent->_key) //待删除的结点比双亲结点的值大
			{
				//双亲的右指针指向待删除结点的左孩子
				pParent->_pRight = pCur->_pLeft;
				delete pCur;
				return true;
			}
			else //待删除的结点比双亲结点的值小
			{
				//双亲的左指针指向待删除结点的左孩子
				pParent->_pLeft = pCur->_pLeft;
				delete pCur;
				return true;
			}
		}

		//情况三,待删除的结点有左孩子和右孩子
		Node* pRightMinParent = pCur;	//记录右子树中最小值结点的双亲节点
		Node* pRightMin = pCur->_pRight;	//指向右子树中最小值的结点

		//不断往左走,直到走到叶子结点为止
		while (pRightMin->_pLeft)
		{
			pRightMinParent = pRightMin;
			pRightMin = pRightMin->_pLeft;
		}

		//将待删除的结点的值和右子树中最小值的结点交换
		pCur->_key = pRightMin->_key;

		
		if (pRightMinParent->_pLeft == pRightMin) //双亲结点的左指针指向最小值结点
		{
			pRightMinParent->_pLeft = pRightMin->_pRight;	//双亲结点左指针指向待删除结点的右孩子
			delete pRightMin;
		}
		else //双亲结点的右指针指向最小值结点
		{
			pRightMinParent->_pRight = pRightMin->_pRight;	//双亲结点右指针指向待删除结点的右孩子
			delete pRightMin;
		}

		return true;
	}

	//中序遍历节点并打印val
	void inOrder()
	{
		_inOrder(_root);
	}

	//析构
	~BSTree()
	{
		destroy(_root);
		_root = nullptr;
	}
};

测试代码:

cpp 复制代码
void test3()
{
	// 输入单词,查找单词对应的中文翻译
	BSTree<string, string> dict;
	dict.insert("string", "字符串");
	dict.insert("tree", "树");
	dict.insert("left", "左边、剩余");
	dict.insert("right", "右边");
	dict.insert("sort", "排序");
	// 插入词库中所有单词
	string str;
	while (cin >> str)
	{
		BSTNode<string, string>* ret = dict.find(str);
		if (ret == nullptr)
		{
			cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
		}
		else
		{
			cout << str << "中文翻译:" << ret->_val << endl;
		}
	}
}

void test4()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
   "苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的节点中水果对应的次数++
		BSTNode<string, int>* ret = countTree.find(str);
		if (ret == nullptr)
		{
			countTree.insert(str, 1);
		}
		else
		{
			ret->_val++;
		}
	}
	countTree.inOrder();
}

运行结果:

test3:

man
单词拼写错误,词库中没有这个单词:man
string
string中文翻译:字符串
tree
tree中文翻译:树
left
left中文翻译:左边、剩余
right
right中文翻译:右边
sort
sort中文翻译:排序

test4:

6 3 2

相关推荐
酷酷的崽7981 小时前
【数据结构】——原来排序算法搞懂这些就行,轻松拿捏
数据结构·算法·排序算法
八月的雨季 最後的冰吻2 小时前
C--字符串函数处理总结
c语言·前端·算法
北南京海3 小时前
【C++入门(5)】类和对象(初始类、默认成员函数)
开发语言·数据结构·c++
阿拉伯的劳伦斯2924 小时前
LeetCode第一题(梦开始的地方)
数据结构·算法·leetcode
Mr_Xuhhh4 小时前
C语言深度剖析--不定期更新的第六弹
c语言·开发语言·数据结构·算法
吵闹的人群保持笑容多冷静4 小时前
2024CCPC网络预选赛 I. 找行李 【DP】
算法
桃酥4034 小时前
算法day22|组合总和 (含剪枝)、40.组合总和II、131.分割回文串
数据结构·c++·算法·leetcode·剪枝
山脚ice4 小时前
【Hot100】LeetCode—55. 跳跃游戏
算法·leetcode
桃酥4034 小时前
算法day21|回溯理论基础、77. 组合(剪枝)、216.组合总和III、17.电话号码的字母组合
java·数据结构·c++·算法·leetcode·剪枝
小丁爱养花4 小时前
DFS算法专题(一)——二叉树中的深搜【回溯与剪枝的初步注入】
java·开发语言·算法·leetcode·深度优先·剪枝