前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【数据结构】二叉搜索树(二叉排序树)

【数据结构】二叉搜索树(二叉排序树)

作者头像
ephemerals__
发布2024-12-25 10:00:18
发布2024-12-25 10:00:18
9400
代码可运行
举报
运行总次数:0
代码可运行

前言

之前,我们学习了树这一数据结构,并尝试实现了堆以及链式二叉树的相关功能:

【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)_树形结构 编译器-CSDN博客 【数据结构】二叉树(c语言)(附源码)_二叉树数据结构代码-CSDN博客

今天,我们将在简单二叉树的基础上,进一步学习一种更为复杂的结构——二叉搜索树

之前我们利用c语言实现了顺序表、链表、二叉树等数据结构。但是在实现一些复杂数据结构时,c语言显得相对繁琐,并且容易出现代码冗余的问题。由于我们现在已经具备了一定的c++代码基础,因此在之后的数据结构学习中,我们将用c++实现。

正文开始

一、什么是二叉搜索树

二叉搜索树(Binary Search Tree),也叫二叉排序树或者二叉查找树, 是一种特殊的二叉树结构。它或者是一棵空树,或者具有以下特点:

1. 如果根节点的左子树不为空,则左子树上的所有节点都小于等于根节点的值。 2. 如果根节点的右子树不为空,则右子树上的所有节点都大于等于根节点的值。 3. 它的每一棵子树也都符合前两点特性(每棵子树也都是二叉搜索树)。

简单地说,二叉搜索树是一棵中序遍历结果有序的二叉树。

一般情况下,二叉搜索树不允许有相同的值出现。当然,你在设计二叉搜索树的时候也可以允许相同值出现,只要满足二叉搜索树的特性即可。注意:二叉搜索树不一定是一棵完全二叉树。

二、二叉搜索树的实现

接下来,我们尝试实现二叉搜索树的结构及其常用方法。

节点

首先是它的节点的结构。二叉搜索树的节点包含一个数据域和指向两孩子的指针:

代码语言:javascript
代码运行次数:0
复制
//节点
template<class K>
struct BSTNode
{
	K _key;//数据域
	BSTNode<K>* _left;//指向左孩子的指针
	BSTNode<K>* _right;//指向右孩子的指针

	//节点构造
	BSTNode(const K& key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

可以看到,节点的定义当中,模板参数被命名为"K"。这里的"K"通常表示(key)的类型,特别应用于key_value(键值对,可以理解为一个二元组,每一个key都有对应的value)的场景。一般情况下,key的值是不允许修改的,但是其对应的value可以修改。本次实现二叉搜索树的过程当中,我们仅实现带有key的结构,至于key_value结构,会在之后的AVL树和红黑树中实现。

属性和接口的声明

接下来是二叉搜索树类的成员变量和成员函数的声明:

代码语言:javascript
代码运行次数:0
复制
//二叉搜索树
template<class K>
class BST
{
	typedef BSTNode<K> Node;//重命名,简化代码
public:
	//强制生成无参构造
	BST() = default;

	//拷贝构造
	BST(const BST<K>& t);

	//析构
	~BST();

	//插入
	bool Insert(const K& key);

	//删除
	bool Erase(const K& key);

	//查找
	Node* Find(const K& key);

	//中序遍历
	void Inorder();
private:
	Node* _root = nullptr;//根节点的指针
};

插入

现在我们开始实现二叉搜索树节点的插入。为了保持搜索树的特性,有如下插入步骤:

1. 如果树为空,则直接将新节点赋值给根节点的指针。 2. 如果树不为空,则需要根据新节点的值进行搜索,如果某节点的值大于新节点,则向右走;若小于新节点,则向左走。走到空位置之后,按照值插入节点即可。 3. 如果要插入重复的值,则遇到相同值的节点之后可以向左走,也可以向右走,直到找到空位置插入。(我们的实现默认不支持插入重复值

代码实现:

代码语言:javascript
代码运行次数:0
复制
//插入
bool Insert(const K& key)
{
	if (_root == nullptr)//树为空,直接插入
	{
		_root = new Node(key);
		return true;
	}
	else//树不为空,进行搜索
	{
		Node* cur = _root;//从根节点开始搜索
		Node* parent = nullptr;//记录cur的父亲节点
		while (cur)//走到空为止
		{
			if (key < cur->_key)//值小向左走
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)//值大向右走
			{
				parent = cur;
				cur = cur->_right;
			}
			else//这里不允许重复的值插入
			{
				return false;
			}
		}

		//此时cur走到空,进行插入
		cur = new Node(key);
		if (key < parent->_key)//值小,往左边插
		{
			parent->_left = cur;
		}
		else//值大,往右边插
		{
			parent->_right = cur;
		}
		return true;
	}
}

这里我们定义了一个parent指针,用于记录cur的父亲节点。当cur走到空时,parent刚好指向插入位置的父亲节点,然后与parent值进行大小比较,插入新节点。

查找

二叉搜索树的特性使其具备了较高的查找效率。查找步骤如下:

1. 从根节点开始,与要查找的值进行比较,如果要找的值比根小,则向左走,否则向右走。循环往复。 2. 如果走到空,则说明不存在,返回空指针。 3. 如果不支持插入重复值,找到后返回节点地址。 4. 如果支持插入重复值,则找到后一般返回中序遍历结果的第一个节点

代码实现:

代码语言:javascript
代码运行次数:0
复制
//查找
Node* Find(const K& key)
{
	Node* cur = _root;//从根节点开始搜索
	while (cur)//走到空为止
	{
		if (key < cur->_key)//值小向左走
		{
			cur = cur->_left;
		}
		else if (key > cur->_key)//值大向右走
		{
			cur = cur->_right;
		}
		else//相等说明找到了,返回节点地址
		{
			return cur;
		}
	}

	//走到空说明没找到,返回空指针
	return nullptr;
}

删除

删除节点是所有接口中实现难度最大的一个。我们默认需要按值来删除元素,所以要先进行查找,找到之后再删除该节点。删除节点之后,为了保持二叉搜索树的原有特性,就需要调整其他节点。根据被删除的节点,有四种情况需要分析讨论:

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

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

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

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

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

接下来,我们尝试实现删除节点的操作:

代码语言:javascript
代码运行次数:0
复制
//删除
bool Erase(const K& key)
{
	//首先按值查找要被删除的节点
	Node* cur = _root;
	Node* parent = nullptr;//记录父亲节点
	while (cur)
	{
		if (key < cur->_key)//值小向左走
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)//值大向右走
		{
			parent = cur;
			cur = cur->_right;
		}
		else//找到了,开始删除
		{
			//有一个左孩子的情况 和 没有孩子的情况
			if (cur->_right == nullptr)
			{
				//特殊情况,要删除的节点是根节点
				if (cur == _root)
				{
					_root = cur->_left;//直接让根指针指向左孩子
				}
				else
				{
					//先判断自己是父亲节点的哪一个孩子
					//然后将左孩子托付给父亲节点
					if (cur == parent->_left)
						parent->_left = cur->_left;
					else
						parent->_right = cur->_left;
				}
				delete cur;//删除该节点
				return true;
			}
			//有一个右孩子的情况
			else if (cur->_left == nullptr)
			{
				//特殊情况,要删除的节点是根节点
				if (cur == _root)
				{
					_root = cur->_right;//直接让根指针指向右孩子
				}
				else
				{
					//先判断自己是父亲节点的哪一个孩子
					//然后将右孩子托付给父亲节点
					if (cur == parent->_left)
						parent->_left = cur->_right;
					else
						parent->_right = cur->_right;
				}
				delete cur;//删除该节点
				return true;
			}
			//有两个孩子的情况
			else
			{
				//首先寻找右子树的最小值
				Node* rightMin = cur->_right;//从右孩子开始搜索
				Node* rightMinParent = cur;//记录最小值的父亲节点

				while (rightMin->_left)//left走到空,rightMin即为右子树的最小值
				{
					rightMinParent = rightMin;
					rightMin = rightMin->_left;//一直向左走,找最小值
				}

				//将最小值赋值给被删除节点
				cur->_key = rightMin->_key;

				//将替代节点的右孩子托付给父亲
				//先判断自己是父亲节点的哪一个孩子
				if (rightMin == rightMinParent->_left)
					rightMinParent->_left = rightMin->_right;
				else
					rightMinParent->_right = rightMin->_right;
				delete rightMin;//删除该节点
				return true;
			}
		}
	}
	//没找到,返回false
	return false;
}

这里有两点需要注意:

1. 第一种情况可以和第二/第三种情况合并,将空指针托付给父亲节点不会影响整体操作。 2. 第四种情况当中,如果我们找的是右子树的最小值,那么它或是叶子节点(第一种情况),或只有右孩子(第三种情况),不会有左孩子;如果我们找的是左子树的最大值,那么它或是叶子节点(第一种情况),或只有左孩子(第二种情况),不会有右孩子。所以代码中可以非常确定托付哪一个孩子

拷贝构造

接下来我们实现拷贝构造。该接口比较简单,直接递归拷贝即可。代码如下:

代码语言:javascript
代码运行次数:0
复制
//拷贝构造
BST(const BST<K>& t)
{
	_root = _Copy(t._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;//返回根节点
}

析构

与拷贝构造相同,析构时递归释放每一个节点。代码如下:

代码语言:javascript
代码运行次数:0
复制
//析构
~BST()
{
	_Destroy(_root);
}
void _Destroy(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	_Destroy(root->_left);//销毁左子树
	_Destroy(root->_right);//销毁右子树
	delete root;//删除根节点
}

中序遍历

由于二叉搜索树的中序遍历结果是有序的,所以我们来实现一个中序遍历接口。中序遍历的代码与传统的二叉树完全相同。

实现如下:

代码语言:javascript
代码运行次数:0
复制
//中序遍历
void Inorder()
{
	_Inorder(_root);
	cout << endl;
}
void _Inorder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	_Inorder(root->_left);//遍历左子树
	cout << root->_key << ' ';//访问根节点
	_Inorder(root->_right);//遍历右子树
}

最后,我们来使用一下我们实现的二叉搜索树:

代码语言:javascript
代码运行次数:0
复制
int main()
{
	BST<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto& e : a)//循环插入
	{
		t.Insert(e);
		t.Inorder();
	}
	cout << endl;
	for (auto& e : a)//循环删除
	{
		t.Erase(e);
		t.Inorder();
	}
	return 0;
}

运行结果:

程序全部代码

二叉搜索树的全部实现代码如下:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
using namespace std;

//节点
template<class K>
struct BSTNode
{
	K _key;//数据域
	BSTNode<K>* _left;//指向左孩子的指针
	BSTNode<K>* _right;//指向右孩子的指针

	//节点构造
	BSTNode(const K& key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

//二叉搜索树
template<class K>
class BST
{
	typedef BSTNode<K> Node;//重命名,简化代码
public:
	//强制生成无参构造
	BST() = default;

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

	//析构
	~BST()
	{
		_Destroy(_root);
	}

	//插入
	bool Insert(const K& key)
	{
		if (_root == nullptr)//树为空,直接插入
		{
			_root = new Node(key);
			return true;
		}
		else//树不为空,进行搜索
		{
			Node* cur = _root;//从根节点开始搜索
			Node* parent = nullptr;//记录cur的父亲节点
			while (cur)//走到空为止
			{
				if (key < cur->_key)//值小向左走
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)//值大向右走
				{
					parent = cur;
					cur = cur->_right;
				}
				else//这里不允许重复的值插入
				{
					return false;
				}
			}

			//此时cur走到空,进行插入
			cur = new Node(key);
			if (key < parent->_key)//值小,往左边插
			{
				parent->_left = cur;
			}
			else//值大,往右边插
			{
				parent->_right = cur;
			}
			return true;
		}
	}

	//删除
	bool Erase(const K& key)
	{
		//首先按值查找要被删除的节点
		Node* cur = _root;
		Node* parent = nullptr;//记录父亲节点
		while (cur)
		{
			if (key < cur->_key)//值小向左走
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)//值大向右走
			{
				parent = cur;
				cur = cur->_right;
			}
			else//找到了,开始删除
			{
				//有一个左孩子的情况 和 没有孩子的情况
				if (cur->_right == nullptr)
				{
					//特殊情况,要删除的节点是根节点
					if (cur == _root)
					{
						_root = cur->_left;//直接让根指针指向左孩子
					}
					else
					{
						//先判断自己是父亲节点的哪一个孩子
						//然后将左孩子托付给父亲节点
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
					delete cur;//删除该节点
					return true;
				}
				//有一个右孩子的情况
				else if (cur->_left == nullptr)
				{
					//特殊情况,要删除的节点是根节点
					if (cur == _root)
					{
						_root = cur->_right;//直接让根指针指向右孩子
					}
					else
					{
						//先判断自己是父亲节点的哪一个孩子
						//然后将右孩子托付给父亲节点
						if (cur == parent->_left)
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}
					delete cur;//删除该节点
					return true;
				}
				//有两个孩子的情况
				else
				{
					//首先寻找右子树的最小值
					Node* rightMin = cur->_right;//从右孩子开始搜索
					Node* rightMinParent = cur;//记录最小值的父亲节点

					while (rightMin->_left)//left走到空,rightMin即为右子树的最小值
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;//一直向左走,找最小值
					}

					//将最小值赋值给被删除节点
					cur->_key = rightMin->_key;

					//将替代节点的右孩子托付给父亲
					//先判断自己是父亲节点的哪一个孩子
					if (rightMin == rightMinParent->_left)
						rightMinParent->_left = rightMin->_right;
					else
						rightMinParent->_right = rightMin->_right;
					delete rightMin;//删除该节点
					return true;
				}
			}
		}
		//没找到,返回false
		return false;
	}

	//查找
	Node* Find(const K& key)
	{
		Node* cur = _root;//从根节点开始搜索
		while (cur)//走到空为止
		{
			if (key < cur->_key)//值小向左走
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)//值大向右走
			{
				cur = cur->_right;
			}
			else//相等说明找到了,返回节点地址
			{
				return cur;
			}
		}

		//走到空说明没找到,返回空指针
		return nullptr;
	}

	//中序遍历
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
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;//删除根节点
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Inorder(root->_left);//遍历左子树
		cout << root->_key << ' ';//访问根节点
		_Inorder(root->_right);//遍历右子树
	}

	Node* _root = nullptr;//根节点的指针
};

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

接下来,我们简单分析一下二叉搜索树的性能。

设二叉树的节点数为N

在较好情况下,二叉搜索树接近于完全二叉树的状态,它的高度为log₂N

在极端情况下,二叉搜索树是单支树的状态,高度为N

综合而言, 二叉搜索树增、删、改的时间复杂度为O(N)

在较好情况下,其增删改的效率可接近于O(logN),但是它的效率受高度影响较大。所以传统的二叉搜索树显然不满足我们高效查找的需求。之后我们会学习自平衡的二叉搜索树(如AVL树、红黑树),它们会尽量降低二叉搜索树的高度,提高效率。

总结

今天我们学习了一种特殊的二叉树结构--二叉搜索树,它的特性使其具备了较好的查找效率。掌握二叉搜索树的知识,将为我们后续学习STL中的setmap容器打下坚实的基础。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、什么是二叉搜索树
  • 二、二叉搜索树的实现
    • 节点
    • 属性和接口的声明
    • 插入
    • 查找
    • 删除
    • 拷贝构造
    • 析构
    • 中序遍历
    • 程序全部代码
  • 三、二叉搜索树的性能分析
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档