二叉搜索树的 C++ 实现与详解

目录

 

一、引言

二、二叉搜索树的定义

二叉搜索树是一种特殊的二叉树,它满足以下性质:

三、创造二叉搜索树的目的

四、复杂度分析

五、C++ 代码实现

六.代码剖析

1.insert分析

2.erase分析

3.图示

总结


 

一、引言

 

在数据结构的领域中,二叉搜索树(Binary Search Tree,简称 BST)是一种非常重要且常用的数据结构。它具有高效的查找、插入和删除操作性能,在许多实际应用中发挥着关键作用。本文将通过 C++ 代码详细介绍二叉搜索树的实现,并对其关键操作进行深入分析。

二、二叉搜索树的定义

 

二叉搜索树是一种特殊的二叉树,它满足以下性质:

  1. 对于树中的任意一个节点,其左子树中所有节点的值都小于该节点的值。
  2. 对于树中的任意一个节点,其右子树中所有节点的值都大于该节点的值。

三、创造二叉搜索树的目的

高效查找:搜索二叉树的特性是对于树中的每个节点,其左子树中所有节点的值都小于该节点的值,而其右子树中所有节点的值都大于该节点的值。这种特性使得在搜索二叉树中进行查找操作时,可以利用二分查找的思想,平均情况下查找的时间复杂度为O(logn),其中n是树中节点的数量。相比顺序存储结构(如数组)的顺序查找(时间复杂度为O(n)),以及链表结构的顺序查找,在数据量较大时,搜索二叉树的查找效率更高。

在实现二叉搜索树的时候还发现了一个有趣的特性就是用中序遍历的时候他成有序数列,我称为附带价值。

  

四、复杂度分析

1.在理想情况下,即搜索二叉树是平衡的,树的高度为h=logn(n为节点个数)。每次比较都能将搜索范围缩小一半,类似于二分查找,所以查找操作的时间复杂度为O(logn)。

 

然而,在最坏情况下,比如二叉搜索树退化成了一个单链表(所有节点都只有右子树或左子树),此时树的高度为n,查找操作就需要从根节点依次遍历到目标节点,时间复杂度变为O(n)。

 

五、C++ 代码实现

 

下面是一个简单的二叉搜索树的 C++ 模板类实现:

二叉树的基础:

template <class T>
struct SBtreeNode
{
	T _key;
	SBtreeNode<T>* _left;
	SBtreeNode<T>* _right;
	SBtreeNode(const T& key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
	{

	}
};

实现二叉搜索树:

template <class T>
class SBtree
{
	typedef SBtreeNode<T> Node;
public:
	bool Insert(const T& 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;
			}
		}
		if (parent->_key < key)
		{
			cur = new Node(key);
			parent->_right = cur;
		}
		else
		{
			cur = new Node(key);
			parent->_left = cur;
		}
		return true;
	}
	bool Find(const T& 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;
	}
	bool Erase(const T& key)
	{
		Node* parend=nullptr;
		Node* cur=_root;
		while (cur)
		{
			if (cur->_key < key)
			{
				parend = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parend = cur;
				cur = cur->_left;
			}
			else
			{
				if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (parend->_key < key)
						{
							parend->_right = cur->_right;
						}
						else
						{
							parend->_left = cur->_right;
						}
					}
					delete cur;
				}
				else if(cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (parend->_key < key)
						{
							parend->_right = cur->_left;
						}
						else
						{
							parend->_left = cur->_left;
						}
					}
					delete cur;
				}
				else
				{
					Node* replace = cur->_right;
					Node* replaceparent = cur;
					while (replace->_left)
					{
						replaceparent = replace;
						replace = replace->_left;
					}

					swap(cur->_key,replace->_key);

	 				if (replaceparent->_left == replace)
						replaceparent->_left = replace->_right;
					else
						replaceparent->_right = replace->_right;

					delete replace;
				}
				return true;
			}
		}
		return false;
	}

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

六.代码剖析

在这里我们主要剖析insert和erase

1.insert分析

bool Insert(const T& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);//根节点为空创造新节点并返回true
		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   //相同返回false
		{
			return false;
		}
	}     
	if (parent->_key < key)//找到这里说明cur已经为空并找好了key要插入的位置
	{
		cur = new Node(key);
		parent->_right = cur;
	}                   
	else
	{
		cur = new Node(key);
		parent->_left = cur;
	}
	return true;
}
  1. 根节点为空的情况:如果二叉搜索树的根节点 _root 为空,说明树为空,此时直接创建一个新节点并将其作为根节点,插入成功并返回 true
  2. 寻找插入位置:使用两个指针 parent 和 cur 遍历二叉搜索树。cur 用于当前遍历的节点,parent 用于记录 cur 的父节点。在遍历过程中,根据二叉搜索树的性质(左子树节点值小于根节点值,右子树节点值大于根节点值),比较当前节点的值和要插入的值 key 的大小关系,决定向左子树还是右子树继续遍历。如果发现 cur->_key 等于 key,说明该值已经存在于树中,插入失败,返回 false
  3. 插入新节点:当 cur 为空时,说明已经找到了插入位置,此时根据 parent 的值和 key 的大小关系,将新节点插入到 parent 的左子树或右子树。插入成功后返回 true

2.erase分析

bool Erase(const T& key)
{
	Node* parend=nullptr;
	Node* cur=_root;
	while (cur)  //为空跳出没有找到且未删除
	{
		if (cur->_key < key)    //若cur的_key小于要插入的key
		{                       //说明key在cur的右子树里
			parend = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)//若cur的_key大于要插入的key
		{                        //说明key在cur的左子树里
			parend = cur;
			cur = cur->_left;
		}
		else                     //找到
		{
			if (cur->_left == nullptr)//判断是否为单叶子,若为单叶子就好办了
			{                         
				if (cur == _root)     //判断是否为头节点是则直接等于cur_right因为_left那                            
				{                     //一边已经没有东西
					_root = cur->_right;
				}
				else
				{
					if (parend->_key < key)//判断是父节点的左还是右节点
					{
						parend->_right = cur->_right;
					}
					else
					{
						parend->_left = cur->_right;
					}
				}
				delete cur;
			}
			else if(cur->_right == nullptr)//同上//判断是否为单叶子,若为单叶子就好办了
			{                                             
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					if (parend->_key < key)//判断是父节点的左还是右节点
					{
						parend->_right = cur->_left;
					}
					else
					{
						parend->_left = cur->_left;
					}
				}
				delete cur;
			}
			else   //上面全判断完还是不满足就说明要删除的节点为双叶子节点
			{
				Node* replace = cur->_right;//找右节点的最小节点
				Node* replaceparent = cur;  //cur就是要删除的节点
				while (replace->_left)     //找右节点的最小节点
				{
					replaceparent = replace;
					replace = replace->_left;
				}

				swap(cur->_key,replace->_key);//交换key

	 			if (replaceparent->_left == replace) //判断是左还是右
					replaceparent->_left = replace->_right;
				else
					replaceparent->_right = replace->_right;

				delete replace; //记得删除
			}
			return true;
		}
	}
	return false;
	}

 

  1. 查找待删除节点:使用两个指针 parent和 cur 从根节点开始遍历二叉搜索树,根据节点键值和目标键值 key 的大小关系,决定向左子树或右子树继续查找,直到找到目标节点或遍历到空节点。
  2. 删除节点:当找到目标节点后,根据该节点的子节点情况进行不同处理:
    • 左子节点为空:直接将该节点的右子节点连接到其父节点相应位置(若该节点为根节点,则更新根节点),然后删除该节点。
    • 右子节点为空:直接将该节点的左子节点连接到其父节点相应位置(若该节点为根节点,则更新根节点),然后删除该节点。
    • 左右子节点都不为空:找到该节点右子树中的最小节点(即右子树中最左边的节点)作为替换节点,将替换节点的值与待删除节点的值交换,然后删除替换节点。
  3. 返回结果:如果成功删除节点,返回 true;若未找到目标节点,返回 false

3.图示

else                     //找到
		{
			if (cur->_left == nullptr)//判断是否为单叶子,若为单叶子就好办了
			{                         
				if (cur == _root)     //判断是否为头节点是则直接等于cur_right因为_left那                            
				{                     //一边已经没有东西
					_root = cur->_right;
				}
				else
				{
					if (parend->_key < key)//判断是父节点的左还是右节点
					{
						parend->_right = cur->_right;
					}
					else
					{
						parend->_left = cur->_right;
					}
				}
				delete cur;
			}
			else if(cur->_right == nullptr)//同上//判断是否为单叶子,若为单叶子就好办了
			{                                             
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					if (parend->_key < key)//判断是父节点的左还是右节点
					{
						parend->_right = cur->_left;
					}
					else
					{
						parend->_left = cur->_left;
					}
				}
				delete cur;
			}

为单节点:

为双节点

else   //上面全判断完还是不满足就说明要删除的节点为双叶子节点
{
	Node* replace = cur->_right;//找右节点的最小节点
	Node* replaceparent = cur;  //cur就是要删除的节点
	while (replace->_left)     //找右节点的最小节点
	{
		replaceparent = replace;
		replace = replace->_left;
	}

		swap(cur->_key,replace->_key);//交换key

	 	if (replaceparent->_left == replace) //判断是左还是右
			replaceparent->_left = replace->_right;
		else
			replaceparent->_right = replace->_right;

		delete replace; //记得删除
	}
		return true;
}

 

 

总结

搜索二叉树是一种强大而灵活的数据结构,它通过巧妙的结构设计实现了高效的数据管理。虽然在最坏情况下性能可能会下降,但通过一些优化策略(如平衡二叉搜索树)可以有效避免这种情况。在实际应用中,搜索二叉树广泛应用于各种领域,为我们提供了快速、可靠的数据处理解决方案。希望通过本文的介绍,你对搜索二叉树有了更深入的理解和认识。

在此我们可以感觉到搜索二叉树还是有很多不足的比如树在极端情况下复杂度为O(N)在此我引出下篇——AVL平衡树实现

最后祝大家生活愉快!

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值