二叉搜索树

数据结构、算法总述:数据结构/算法 C/C++-CSDN博客


二叉搜索树(BST):

二叉搜索树是一种二叉树的树形数据结构,其定义如下:

  • 空树是二叉搜索树。

  • 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。

  • 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。

  • 二叉搜索树的左右子树均为二叉搜索树。

eg:

特性:

  1. 二叉搜索树与普通的二叉树有所不同,如果我们按照中序遍历进行访问结点的值的时候我们可以发现它是一个升序的序列。
  2. 每个结点的左子树结点上边的值都小于该结点的值,右子树是上边的值都大于该结点的值。

创建节点:

cpp 复制代码
template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
 
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

BST树:

cpp 复制代码
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
    //成员函数
private:
    Node* _root=nullptr;
};

默认成员函数:

构造函数

这里的构造函数直接让编译器默认生成就可以,不需要自己实现,但是后面的拷贝构造函数写了之后编译器就不会默认生成了,但是我们可以强制让它默认生成构造函数,不过要利用C++11的特性,具体看代码:

cpp 复制代码
//强制编译器自己生成构造函数,忽视拷贝构造带来的影响
BSTree() = default;//C++11才支持
拷贝构造函数

注意这里的拷贝构造完成的是深拷贝,这里我们直接用前序递归的方式创建一颗与原来一样的二叉树即可。而递归前序拷贝结点的方式这里我们专门封装一个Copy函数即可。

cpp 复制代码
Node* CopyTree(Node* root)
{
	if (root == nullptr)
		return nullptr;
	Node* copyNode = new Node(root->_key);//拷贝根结点
	//递归创建拷贝一棵树
	copyNode->_left = CopyTree(root->_left);//递归拷贝左子树
	copyNode->_right = CopyTree(root->_right);//递归拷贝右子树
	return copyNode;
}
//拷贝构造函数--深拷贝
BSTree(const BSTree<K>& t)
{
	_root = CopyTree(t._root);
}
赋值运算符重载函数

这里直接给出现代写法:写法很巧妙,假设把t2赋值给t1,t2传参的时候直接利用传值传参调用拷贝构造生成t,t就是t2的拷贝,此时再调用swap函数交换t1和t 的_root根结点即可,而拷贝构造出来的t会在赋值运算符重载结束后自动调用自己的析构函数完成释放。

cpp 复制代码
//赋值运算符重载函数 t1 = t2
BSTree<K>& operator=(BSTree<K> t)//t就是t2的拷贝
{
	//现代写法
	swap(_root, t._root);
	return *this;
}
析构函数

析构函数是为了释放二叉搜索树的所有结点,这里我们优先采用后序的递归释放,可以采用封装一个Destory函数来专门用于递归删除结点

cpp 复制代码
void DestoryTree(Node* root)
{
	if (root == nullptr)
		return;
	//通过递归删除所有结点
	DestoryTree(root->_left);//递归释放左子树中的结点
	DestoryTree(root->_right);//递归释放右子树中的结点
	delete root;
}
//析构函数
~BSTree()	
{
	DestoryTree(_root);//复用此函数进行递归删除结点
	_root = nullptr;
}

相关操作:

查找

过程:

在以 root 为根节点的二叉搜索树中搜索一个值为 value 的节点。

分类讨论如下:

  • root 为空,返回 false
  • root 的权值等于 value,返回 true
  • root 的权值大于 value,在 root 的左子树中继续搜索。
  • root 的权值小于 value,在 root 的右子树中继续搜索。

代码(非递归):

cpp 复制代码
bool 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 true;
		}
	}
 
	return false;
}

代码(递归):

cpp 复制代码
bool _FindR(Node*& root, const K& key)
{
	if (root == nullptr)
		return false;
	if (root->_key < key)
	{
		return _FindR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _FindR(root->_left, key);
	}
	else
	{
		return true;
	}
}

插入

过程:

在以 root 为根节点的二叉搜索树中插入一个值为 value 的节点。

分类讨论如下:

  • root 为空,直接返回一个值为 value 的新节点。

  • root 的权值等于 value,该节点的附加域该值出现的次数自增

  • root 的权值大于 value,在 root 的左子树中插入权值为 value 的节点。

  • root 的权值小于 value,在 root 的右子树中插入权值为 value 的节点。

代码(非递归):

cpp 复制代码
bool Insert(const K& key)
{
    //树为空,则直接新增结点,赋值给_root指针
	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;
		}
	}
 
	cur = new Node(key);
    //判断是插入父结点的左部还是右部
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
 
	return true;
}

代码(递归):

cpp 复制代码
bool _FindR(Node*& root, const K& key)
{
	if (root == nullptr)
		return false;
	if (root->_key < key)
	{
		return _FindR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _FindR(root->_left, key);
	}
	else
	{
		return true;
	}
}

删除

过程:

在以 root 为根节点的二叉搜索树中删除一个值为 value 的节点。

先在二叉搜索树中搜索权值为 value 的节点,分类讨论如下:

  • 若该节点的附加 count 大于 ,只需要减少 count

  • 若该节点的附加 count

    • root 为叶子节点,直接删除该节点即可。

    • root 为链节点,即只有一个儿子的节点,返回这个儿子。

    • count 有两个非空子节点,一般是用它左子树的最大值(左子树最右的节点)或右子树的最小值(右子树最左的节点)代替它,然后将它删除。

代码(非递归):

cpp 复制代码
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
		{
			// 开始删除
			// 1、左为空
			// 2、右为空
			// 3、左右都不为空
			if (cur->_left == nullptr)
			{
                //判断下当前节点是否是_root,若是,无法用parent(当前为nullptr,防止野指针错误)
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}
 
				delete cur;
				cur = nullptr;
			}
			else if (cur->_right == nullptr)
			{
				if (_root == cur)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
 
				delete cur;
				cur = nullptr;
			}
			else
			{
				//记录删除节点父节点
				Node* minParent = cur;
				//找到右子树最小节点进行替换
				Node* min = cur->_right;
				while (min->_left)
				{
					minParent = min;
					min = min->_left;
				}
				swap(cur->_key, min->_key);
				//min在父的左孩子上
				if (minParent->_left == min)
					//万一最左节点还有右孩子节点,或者是叶子也直接指右为空
					minParent->_left = min->_right;
				//min在父的右孩子上(待删除节点在根节点,最左节点为根节点的右孩子)
				else
					minParent->_right = min->_right;
				delete min;
				min == nullptr;
			}
			return true;
		}
	}
	return false;
}

代码(递归):

cpp 复制代码
bool _EraseR(Node* root, const K& key)
{
	Node* del = root;
	if (root == nullptr)
		return false;
	if (root->_key < key)
		return _EraseR(root->_right, key);
	else if (root->_key > key)
		return _EraseR(root->_left, key);
	else
	{
		if (root->_left == nullptr)
			root = root->_right;
		else if (root->_right == nullptr)
			root = root->_left;
		else
		{
			//找右数的最左节点替换删除
			Node* min = root->_right;
			while (min->_left)
			{
				min = min->_left;
			}
			swap(root->_key, min->_key);
			//交换后结构改变不是搜索二叉树了,规定范围在右树(因为是右树最左节点替换)再递归
			return _EraseR(root->_right, key); 
		}
		delete del;
		return true;
				
	}
 
}

应用

(1)纯 key 模型 ,如: 有一个英文词典,快速查找一个单词是否在词典中快速查找某个名字在不在通讯录中

(2)Key-Value 模型,如: ①统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:< 单词,单词出现的次数 > ②梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号

性能分析

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

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log(N)

最差情况下:二叉搜索树退化为单支树(或者类似单支),其平均比较次数为 N

改进:AVL树、红黑树

相关推荐
兵哥工控4 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
Fuxiao___6 分钟前
不使用递归的决策树生成算法
算法
我爱工作&工作love我11 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
娃娃丢没有坏心思41 分钟前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h41 分钟前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20
白-胖-子1 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级
workflower1 小时前
数据结构练习题和答案
数据结构·算法·链表·线性回归
好睡凯1 小时前
c++写一个死锁并且自己解锁
开发语言·c++·算法
Sunyanhui11 小时前
力扣 二叉树的直径-543
算法·leetcode·职场和发展