二叉搜索树

本文详细介绍了二叉搜索树的概念、K模型与KV模型的区别,以及查找、插入、中序遍历和删除操作的实现原理和注意事项。同时,对性能进行了分析,讨论了理想和最差情况下的性能差异。
摘要由CSDN通过智能技术生成

一、概念

  • 二叉搜索树又称为二叉排序树,它可以是一棵空树,也可以不为空。当它不为空时具有以下性质。
  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
  • 它的左右子树也都为二叉搜索树。

二、K模型与KV模型

  • K模型即只有key作为关键码,在二叉搜索树结构中只需要存储Key的值即可,关键码即为需要搜索到的值。
  • KV模型则是每一个关键码key,都有与之对应的Value值,即<Key, Value>键值对。在二叉搜索树结构中需要存储Key和Value的值。

三、整体框架

1、代码

template<class K, class V>
class BSTreeNode
{
public:
	BSTreeNode(const K& key = K(), const V& val = V())
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
		,_val(val)
	{}

	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;
	V _val;
};

template<class K, class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;
	Node* _root = nullptr;
};

2、实现原理

  • 此处实现的二叉搜索树是Key/Val模型的。
  • 因为K和V的类型可能根据需要会有多种,所以,可以将二叉树搜索树类和节点类写成模板。
  • 因为二叉搜索树实际上也是一棵树,所以它有左右节点,用两个节点类指针_left和_right分别指向本节点的左右子节点;因为这棵树是Key/Val模型的,所以分别用_key和_val保存各自的数据。
  • 在节点类模板中,实现一个带有缺省值的构造函数,方便下面创建节点。因为构造函数的形参不会改变,所以加上const修饰和用引用。又因为K和V可能是自定义类型,为了避免浅拷贝的情况发生,缺省值需要写为上方代码中的样子,即匿名对象。因为创建节点时,不知道左右节点应该指向什么,所以在初始化列表中将_left和_right都初始化为空。
  • 在二叉树搜索树模板类BSTree中,节点类类型的成员变量_root 的作用是保存二叉搜索树的根节点。而二叉搜索树的各种操作函数将在这个模板类中实现,但因为在模板类外需要用到,所以它们需要用public限定符限定。

四、查找操作

1、操作

  • 从根节点开始比较,如果欲查找的值比根节点的值大则往右子树查找,比根节点的值小则往左子树查找。
  • 最多查找二叉搜索树高度次,当查找到的节点为空节点,即还没查找到欲查找的值时,说明欲查找的值不存在于这棵二叉搜索树中,此时返回空指针。

2、代码

Node* 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 cur;
	}
	return nullptr;
}

3、实现原理

  • 因为此处实现的二叉搜索树是Key/Val模型的,所以查找时只需用key进行查找,当用key查找到节点时,val值也找到了。
  • 因为查找时需要从树的根节点开始查找,所以用一个节点类型的变量保存根节点_root的值,然后用循环进行查找。当查找到时,直接返回该节点,否则,即为查找不到,直接返回空指针。

五、插入操作

1、操作

  • 当二叉搜索树为空时,直接新增节点,并将该节点赋值给根节点指针_root。
  • 当二叉搜索树不空时,先按二叉搜索树的性质查找插入位置,再在查找到的插入位置插入新节点。

2、代码

bool Insert(const K& key, const V& value)
{
	if (_root == nullptr)
	{
		_root = new Node(key, value);
		return true;
	}
	Node* parent = _root;
	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
		{
			return false;
		}
	}

	if (parent->_key > key)
		parent->_left = new Node(key, value);
	else
		parent->_right = new Node(key, value);
	return true;
}

3、实现原理

  • 因为此处实现的二叉搜索树是Key/Val模型的,所以,函数的形参需要有两个,即key和value。
  • 当_root 为空时,说明该树是空树,直接创建节点后返回真,但注意,需对_root进行修改而不是直接返回创建的Node节点。
  • 如果_root 不为空,说明二叉搜索树不为空。此时,用两个节点指针变量cur 和parent 都保存根节点的值,作用为查找欲插入节点的位置和它的父节点。然后进行查找,如果为空,则进行插入操作。否则,说明二叉搜索树中已经存在该值的节点,则直接返回false。
  • 进行插入节点时,需要用parent 节点的值与key值进行判断以确定插入的位置,而不能用 parent->_left == cur 作为判断条件,因为上面的循环结束后,cur为空,如果parent的左右节点都为空,就会判断插入的位置为左节点,而不论key的大小。这样插入后,该二叉搜索树有可能不是二叉搜索树了。

六、中序遍历

1、代码

void InOrder()
{
	_InOrder(_root);
	cout << endl;
}

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

2、实现原理

  • 因为在调用二叉搜索树的中序遍历函数时,我们不会传递根节点作为实参,而是直接调用。所以,实现两个函数,使调用中序遍历函数时,不需要传递实参。
  • 此处采用的是递归实现的,所以只需要输出root节点的内容即可。

七、删除操作

1、操作

  • 查找欲删除的元素是否存在于二叉搜索树中,如果不存在,则返回false,否则要删除的结点可以分为下面四种情况:
  • 【1】:要删除的结点无孩子节点。
  • 【2】:要删除的结点只有左孩子节点。
  • 【3】:要删除的结点只有右孩子节点。
  • 【4】:要删除的结点有左、右孩子节点。
  • 真正的删除过程为:
  • 【1】与【2】:删除该结点且使欲删除节点的父结点指向欲删除节点的指针指向被删除节点的左孩子节点。
  • 【1】与【3】:删除该结点且使欲删除节点的父结点指向欲删除节点的指针指向被删除节点的右孩子节点。
  • 【4】:在欲删除节点的右子树中寻找值最小的节点或者在左子树中寻找值最大的节点,用寻找到的节点的值填补到欲删除的节点中,再删除(释放)寻找到的节点。

2、代码

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* del = _root;
	while (del)
	{
		if (del->_key > key)
		{
			parent = del;
			del = del->_left;
		}
		else if (del->_key < key)
		{
			parent = del;
			del = del->_right;
		}
		else
		{
			break;
		}
	}
	if (del == nullptr)
		return false;
	if (del->_left == nullptr)
	{
		if (parent == nullptr)
		{
			_root = del->_right;
		}
		else
		{
			if (parent->_left == del)
				parent->_left = del->_right;
			else
				parent->_right = del->_right;
		}

		delete del;
	}
	else if (del->_right == nullptr)
	{
		if (parent == nullptr)
		{
			_root = del->_left;
		}
		else
		{
			if (parent->_left == del)
				parent->_left = del->_left;
			else
				parent->_right = del->_left;
		}

		delete del;
	}
	else
	{
		Node* pMaxLeft = del;
		Node* maxLeft = del->_left;
		while (maxLeft->_right)
		{
			pMaxLeft = maxLeft;
			maxLeft = maxLeft->_right;
		}
		swap(del->_key, maxLeft->_key);
		swap(del->_val, maxLeft->_val);
		
		if (pMaxLeft->_left == maxLeft)
			pMaxLeft->_left = maxLeft->_left;
		else
			pMaxLeft->_right = maxLeft->_left;
		delete maxLeft;
	}
	return true;
}

3、实现原理图

在这里插入图片描述

4、注意

  • 节点类指针parent不能初始化为_root,因为当要删除的节点是根节点时,parent指向的就应该是空指针。
  • 当左节点或者右节点为空时都需要对parent节点进行判断,因为可能欲删除的节点是根节点。

八、性能分析

  • 在二叉搜索树中进行插入或者删除操作时,都必须先通过Key查找欲插入或者删除的节点位置,即查找的效率代表了二叉搜索树中各个操作的性能。
  • 对有n个结点的二叉搜索树,若每个元素查找的概率都相等,则二叉搜索树平均查找长度是节点在二叉搜索树的深度的函数,即节点越深,比较的次数就越多。
  • 对于同一个关键码集合,如果各关键码插入的次序不同,可能得到的二叉搜索树的结构会有不同。
  • 最优情况,二叉搜索树为完全二叉树或者接近完全二叉树,其平均比较次数为logN。
  • 最差情况,二叉搜索树退化为单支树或者类似单支树,其平均比较次数为N。

九、整体代码

template<class K, class V>
class BSTreeNode
{
public:
	BSTreeNode(const K& key = K(), const V& val = V())
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
		,_val(val)
	{}

	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;
	V _val;
};


template<class K, class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;
public:
	bool Insert(const K& key, const V& value)
	{
		if (_root == nullptr)
		{
			_root = new Node(key, value);
			return true;
		}
		Node* parent = _root;
		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
			{
				return false;
			}
		}

		if (parent->_key > key)
			parent->_left = new Node(key, value);
		else
			parent->_right = new Node(key, value);
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	Node* 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 cur;
		}
		return nullptr;
	}

	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* del = _root;
		while (del)
		{
			if (del->_key > key)
			{
				parent = del;
				del = del->_left;
			}
			else if (del->_key < key)
			{
				parent = del;
				del = del->_right;
			}
			else
			{
				break;
			}
		}
		if (del == nullptr)
			return false;
		if (del->_left == nullptr)
		{
			if (parent == nullptr)
			{
				_root = del->_right;
			}
			else
			{
				if (parent->_left == del)
					parent->_left = del->_right;
				else
					parent->_right = del->_right;
			}

			delete del;
		}
		else if (del->_right == nullptr)
		{
			if (parent == nullptr)
			{
				_root = del->_left;
			}
			else
			{
				if (parent->_left == del)
					parent->_left = del->_left;
				else
					parent->_right = del->_left;
			}

			delete del;
		}
		else
		{
			Node* pMaxLeft = del;
			Node* maxLeft = del->_left;
			while (maxLeft->_right)
			{
				pMaxLeft = maxLeft;
				maxLeft = maxLeft->_right;
			}
			swap(del->_key, maxLeft->_key);
			swap(del->_val, maxLeft->_val);
			
			if (pMaxLeft->_left == maxLeft)
				pMaxLeft->_left = maxLeft->_left;
			else
				pMaxLeft->_right = maxLeft->_left;
			delete maxLeft;
		}
		return true;
	}

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

	Node* _root = nullptr;
};

本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请务必点赞、收藏加关注💕💕💕

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值