「前言」文章的大致内容是二叉搜索树。二叉搜索树是数据结构初阶二叉树的一部分,二叉搜索树为后序所学的 map 和 set 做准备。

一、二叉搜索树

1.1 概念

二叉搜索树(BST,Binary Search Tree)又称二叉排序树,它或者是一棵空树,二叉树搜索树具有以下性质:

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

比如,下面就是一颗二叉搜索树。

左子树小于根节点,右子树大于根节点,每颗子树都保持这样的特性,这样的树就称为二叉搜索树。

二叉搜索树进行中序遍历,遍历出来的结果是有序的(升序),二叉搜索树一般都是去重的,即没有相同的值。

1.2 二叉搜索树操作

二叉搜索树的主要接口就是插入、查找、删除、遍历。

  1. 查找(Find):
    1. 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
    2. 最多查找高度次,走到到空,还没找到,这个值不存在。
  2. 插入(Insert):插入一个节点,插入后保持二叉搜索树的性质,二叉搜索树实现下面解释
  3. 删除(Erase):删除一个节点,删除后保持二叉搜索树的性质,二叉搜索树实现下面解释
  4. 遍历(InOrder):使用中序遍历即可,遍历结果为升序序列

二、二叉搜索树实现

2.1 框架总览

要实现二叉搜索树,我们首先需要实现一个节点类,结点类当中包含三个成员变量:结点的值、左指针、右指针,结点类当中只需实现一个构造函数即可

 二叉搜索树类(BSTree)里面的成员变量只要包含一个根节点 _root 即可

为了书写简单,对 BSTreeNode 进行 typedef 为 Node

typedef BSTreeNode Node

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

	K _key;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
};

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
			
private:
	Node* _root;
};

2.2 实现接口总览

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

	K _key;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
};

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	//构造函数
	BSTree()
	{}
	//拷贝构造
	BSTree(const BSTree<K>& t)
	{}
	//赋值重载
	BSTree<K>& operator=(BSTree<K> t)
	{}
	//析构
	~BSTree()
	{}

	bool Insert(const K& key)
	{}

	bool Find(const K& key)
	{}

	bool Erase(const K& key)
	{}
	//遍历搜索树
	void InOrder()
	{}

private:
	Node* _root;
};

2.2.1 构造函数

构造函数非常简单,构造一个空树即可

//构造函数
	BSTree()
		:_root(nullptr)
	{}

2.2.2 拷贝构造

由于要进行递归操作,就不能把递归操作写在拷贝构造函数里面,需要另写一个函数,这个函数设置为私有,拷贝构造也是深拷贝

//拷贝构造
BSTree(const BSTree<K>& t)
{
	_root = Copy(t._root);
}

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

2.2.3 赋值重载

赋值重载直接使用现代写法,复用拷贝构造,不解释了,赋值重载也是深拷贝

//赋值重载
BSTree<K>& operator=(BSTree<K> t)
{
	swap(_root, t._root);
	return *this;
}

2.2.4 析构函数

析构函数也是如此,需要进行递归操作,需要另写一个函数进行递归操作

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

private:
	void Destroy(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
	}

2.2.5 二叉搜索树的遍历

遍历使用中序遍历即可,遍历出来的结果是升序的,因为无法使用类成员变量,就无法传根进行递归遍历,所以也需要另写一个函数

代码如下: 

//遍历搜索树
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}

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

2.2.6 插入函数

根据二叉搜索树的性质,其插入操作非常简单:

  1. 如果是空树,则直接将插入结点作为二叉搜索树的根结点
  2. 如果不是空树,则按照二叉搜索树的性质进行结点的插入

若不是空树,插入结点的具体操作如下:

  • 若待插入结点的值小于根结点的值,则需要将结点插入到左子树当中
  • 若待插入结点的值大于根结点的值,则需要将结点插入到右子树当中
  • 若待插入结点的值等于根结点的值,则插入结点失败(二叉搜索树)

插入成功返回 true,插入失败返回 false 

代码如下: 


	bool Insert(const K& key)
	{
		//_root为空树
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		//不为空树,进行查找
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)//遇到空就结束
		{
			if (_root->_key > key)//在左子树
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (_root->_key < key)//在右子树
			{
				parent = cur;
				cur = cur->_right;
			}
			else//相等,直接返回,不允许插入相同值
			{
				return false;
			}
		}
		cur = new Node(key);
		if (parent->_key > key)//parent节点的_key比key大,插入到左边
		{
			parent->_left = cur;
		}
		else//parent节点的_key比key小,插入到右边
		{
			parent->_right = cur;
		}
		return true;
	}

2.2.7 查找函数

根据二叉搜索树的特性,,分以下几种情况:

  1. 若树为空树,则查找失败,返回 false
  2. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找
  3. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找
  4. 若key值等于当前结点的值,则查找成功,返回 true
  5. 查找完了,还是没有找到,返回 false

代码如下: 

    bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else//找到了
			{
				return true;
			}
		}
		//空树或者找不到
		return false;
	}

2.2.8 删除函数

二叉搜索树的删除函数是最难实现的,若是在二叉树当中没有找到待删除结点,则直接返回 false 表示删除失败即可,但若是找到了待删除结点,此时就有以下三种情况:

  1. 待删除结点的左子树为空
  2. 待删除结点的右子树为空
  3. 待删除结点的左右子树均不为空

下面进行一对一处理:

(1)待删除结点的左子树为空,即右子树不为空

若待删除结点的左子树为空,那么当我们在二叉搜索树当中找到该结点后,只需先让其父结点指向该结点的右孩子结点,然后再将该结点释放便完成了该结点的删除,进行删除操作后仍保持二叉搜索树的特性

注意:如果删除的节点为叶子节点,即待删除节点左右子树均为空,这个情况包含在这种情况里面

动图演示(演示其中一种情况):删除10

  1. 待删除结点不是根结点,此时parent不为nullptr
  2. 待删除结点是其父结点的右孩子
  3. 父结点的右指针指向待删除结点的右子树即可

 (2)待删除结点的右子树为空,即左子树不为空

若待删除结点的右子树为空,那么当我们在二叉搜索树当中找到该结点后,只需先让其父结点指向该结点的左孩子结点,然后再将该结点释放便完成了该结点的删除,进行删除操作后仍需要保持二叉搜索树的特性

动图演示(演示其中一种情况):删除14

  1. 待删除结点不是根结点,此时parent不为nullptr
  2. 待删除结点是其父结点的右孩子
  3. 父结点的右指针指向待删除结点的左子树即可

 (3)待删除结点的左右子树均不为空

若待删除结点的左右子树均不为空,那么当我们在二叉搜索树当中找到该结点后,可以使用替换法进行删除

有两种替换方法:

  • 可以将让待删除结点左子树当中值最大的结点
  • 或是待删除结点右子树当中值最小的结点代替待删除结点被删除(下面都以最小的结点代替待删除为例)

然后将待删除结点的值改为代替其被删除的结点的值即可,而代替待删除结点被删除的结点,必然左右子树当中至少有一个为空树,因此删除该结点的方法与前面说到的情况(1)(2)的方法相同

注意:只能是待删除结点左子树当中值最大的结点,或是待删除结点右子树当中值最小的结点代替待删除结点被删除,因为只有这样才能使得进行删除操作后的二叉树仍保持二叉搜索树的特性

动图演示(演示其中一种情况):删除3

  1. 选用最小值进行替换(右子树),此时minRight的左为空,左为空或者右为空与选用最小值和最大值有关
  2. 值替换,minRight换不换没影响,minRight是要被删除的
  3. minRight是其父结点的左孩子
  4. 父结点的左指针指向minRight的右子树即可

代码如下:

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else// 找到了要删除的节点
		{
			// (1)左为空,右不为空
			if (cur->_left == nullptr)
			{
				if (cur == _root) // 要删除的为根节点,此时parent为nullptr
				{
					_root = cur->_right; // 二叉搜索树的根结点改为根结点的右孩子即可
				}
				else // 待删除结点不是根结点,此时parent不为nullptr
				{
					if (cur == parent->_left) // 待删除结点是其父结点的左孩子
					{
						parent->_left = cur->_right; // 父结点的左指针指向待删除结点的右子树即可
					}
					else // 待删除结点是其父结点的右孩子
					{
						parent->_right = cur->_right; // 父结点的右指针指向待删除结点的右子树即可
					}
				}
				delete cur;
			}
			else if (cur->_right == nullptr) // (2)左不为空,右为空
			{
				if (cur == _root) // 要删除的为根节点,此时parent为nullptr
				{
					_root = cur->_left; // 根结点改为根结点的左孩子即可
				}
				else // 待删除结点不是根结点,此时parent不为nullptr
				{
					if (cur == parent->_left) // 待删除结点是其父结点的左孩子
					{
						parent->_left = cur->_left; // 父结点的左指针指向待删除结点的左子树即可
					}
					else // 待删除结点是其父结点的右孩子
					{
						parent->_right = cur->_left; // 父结点的右指针指向待删除结点的左子树即可
					}
				}
				delete cur;
			}
			else// (3)左右不为空,替换删除
			{
				// 左右不为空,需要左子树的最大值 或 右子树的左小值充当新的根(对被删除节点进行值替换,然后进行删除)
				Node* minParent = cur;
				Node* minRight = cur->_right; // 这里选用最小值进行替换(右子树)
				while (minRight->_left)
				{
					minParent = minRight;
					minRight = minRight->_left;
				}
				cur->_key = minRight->_key; // 值替换,minRight换不换没影响,minRight是要被删除的
				// 分两种情况,此时minRight的左为空
				if (minRight == minParent->_left) // minRight是其父结点的左孩子
				{
					minParent->_left = minRight->_right; // 父结点的左指针指向minRight的右子树即可
				}
				else // minRight是其父结点的右孩子
				{
					minParent->_right = minRight->_right; // 父结点的右指针指向minRight的右子树即可
				}
				delete minRight;
			}
			return true;
		}
	}
	return false;
}

 2.3 二叉搜索数完整代码

BSTree.h

#pragma once

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

	K _key;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
};

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);
		_root = nullptr;
	}

	bool Insert(const K& key)
	{
		//_root为空树
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		//不为空树,进行查找
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)//遇到空就结束
		{
			if (_root->_key > key)//在左子树
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (_root->_key < key)//在右子树
			{
				parent = cur;
				cur = cur->_right;
			}
			else//相等,直接返回,不允许插入相同值
			{
				return false;
			}
		}
		cur = new Node(key);
		if (parent->_key > key)//parent节点的_key比key大,插入到左边
		{
			parent->_left = cur;
		}
		else//parent节点的_key比key小,插入到右边
		{
			parent->_right = cur;
		}
		return true;
	}

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else//找到了
			{
				return true;
			}
		}
		//空树或者找不到
		return false;
	}

	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else// 找到了要删除的节点
			{
				// (1)左为空,右不为空
				if (cur->_left == nullptr)
				{
					if (cur == _root) // 要删除的为根节点,此时parent为nullptr
					{
						_root = cur->_right; // 二叉搜索树的根结点改为根结点的右孩子即可
					}
					else // 待删除结点不是根结点,此时parent不为nullptr
					{
						if (cur == parent->_left) // 待删除结点是其父结点的左孩子
						{
							parent->_left = cur->_right; // 父结点的左指针指向待删除结点的右子树即可
						}
						else // 待删除结点是其父结点的右孩子
						{
							parent->_right = cur->_right; // 父结点的右指针指向待删除结点的右子树即可
						}
					}
					delete cur;
				}
				else if(cur->_right == nullptr) // (2)左不为空,右为空
				{
					if (cur == _root) // 要删除的为根节点,此时parent为nullptr
					{
						_root = cur->_left; // 根结点改为根结点的左孩子即可
					}
					else // 待删除结点不是根结点,此时parent不为nullptr
					{
						if (cur == parent->_left) // 待删除结点是其父结点的左孩子
						{
							parent->_left = cur->_left; // 父结点的左指针指向待删除结点的左子树即可
						}
						else // 待删除结点是其父结点的右孩子
						{
							parent->_right = cur->_left; // 父结点的右指针指向待删除结点的左子树即可
						}
					}
					delete cur;
				}
				else// (3)左右不为空,替换删除
				{
					// 左右不为空,需要左子树的最大值 或 右子树的左小值充当新的根(对被删除节点进行值替换,然后进行删除)
					Node* minParent = cur;
					Node* minRight = cur->_right; // 这里选用最小值进行替换(右子树)
					while (minRight->_left)
					{
						minParent = minRight;
						minRight = minRight->_left;
					}
					cur->_key = minRight->_key; // 值替换,minRight换不换没影响,minRight是要被删除的
					// 分两种情况,此时minRight的左为空
					if (minRight == minParent->_left) // minRight是其父结点的左孩子
					{
						minParent->_left = minRight->_right; // 父结点的左指针指向minRight的右子树即可
					}
					else // minRight是其父结点的右孩子
					{
						minParent->_right = minRight->_right; // 父结点的右指针指向minRight的右子树即可
					}
					delete minRight;
				}
				return true;
			}
		}
		return false;
	}
	//遍历搜索树
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void Destroy(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
	}

	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 _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root;
};

Test.cpp

#include <iostream>
using namespace std;

#include "BSTree.h"

void Test()
{
	BSTree<int> t;
	t.Insert(4);
	t.Insert(3);
	t.Insert(5);
	t.Insert(4);
	t.Insert(6);
	t.Insert(1);
	t.Insert(7);
	t.Insert(9);
	t.Insert(9);
	t.Insert(2);
	t.Insert(10);
	t.Insert(13);
	t.Insert(27);
	t.Insert(0);
	t.InOrder();
	//拷贝构造
	BSTree<int> t2 = t;
	t2.InOrder();
	//查找
	cout << t2.Find(1) << endl;
	cout << t2.Find(8) << endl;
	//赋值重载
	BSTree<int> t3;
	t3 = t;
	t3.InOrder();
	//删除
	t3.Erase(4);
	t3.InOrder();
	t3.Erase(5);
	t3.InOrder();
	t3.Erase(3);
	t3.InOrder();
	t3.Erase(7);
	t3.InOrder();
	t3.Erase(0);
	t3.InOrder();
	t3.Erase(13);
	t3.InOrder();
	t3.Erase(0);
	t3.InOrder();
	t3.Erase(9);
	t3.InOrder();
}

int main()
{
	Test();
	return 0;
}

三、二叉搜索树的应用

3.1 K模型

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

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

  1. 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  2. 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误 

3.2 KV模型

KV模型:每一个关键码 Key,都有与之对应的值 Value,即<Key, Value>的键值对,通过Key查找 Value

该种方式在现实生活中非常常见:

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

四、二叉搜索树的性能分析

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

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

对于有n个结点的二叉搜索树:

  1. 最优的情况下,二叉搜索树为完全二叉树,其平均比较次数为:O(logN)
  2. 最差的情况下,二叉搜索树退化为单支树,其平均比较次数为:O(N / 2)

所以实际上,二叉搜索树在极端情况下是没办法保证效率的,因此由二叉搜索树又衍生出来了AVL树、红黑树,后序文章介绍。

--------------- END ---------------

「 作者 」 枫叶先生
「 更新 」 2023.2.27
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。