【数据结构】二叉搜索树相关知识详细梳理

发布于:2024-09-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

1. 二叉搜索树的概念

        二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

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

例如:

 2. 二叉搜索树操作

      2.1 查找

                原理:

                1. 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
                2. 最多查找高度次,走到到空,还没找到,这个值不存在。

      2.2 插入

                原理:

                树为空,则直接新增节点,赋值给root指针。
                树不空,按二叉搜索树性质查找插入位置,插入新节点。

                例如:(左小右大)

        2.3 删除

                首先查找元素是否在二叉搜索树中,如果不存在,

                则返回, 否则要删除的结点可能分下面四种情况:

                a. 要删除的结点无孩子结点
                b. 要删除的结点只有左孩子结点
                c. 要删除的结点只有右孩子结点
                d. 要删除的结点有左、右孩子结点

                对应的处理方法:

                b.删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除。
                c.删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除。
                d.在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
           中,再来处理该结点的删除问题--替换法删除。

 

情况c与b类似,不再举例。 

说完二叉搜索树操作的原理,现在开始模拟实现:

3. 二叉搜索树模拟实现 

        声明部分:

using namespace std;
//二叉搜索树节点
template<class K, class V>
struct BSTreeNode
{
	BSTreeNode(const K& key = K(), const V& value = V());//默认构造

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

//二叉搜索树主体
template<class K, class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;
public:
	BSTree() = default;//默认构造,直接用默认生成的
	//~BSTree();//析构

	bool Insert(const K& key, const V& value);//插入数据
	Node* Find(const K& key);//查找数据
	bool Erase(const K& key);//删除数据
	
	void InOrder();//中序遍历
private:
	Node* _root = nullptr;//二叉树根节点
	void _InOrder(Node* root);//中序遍历
};

        具体实现:

template<class K, class V>
void BSTree<K, V>::_InOrder(Node* root)//中序遍历
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << root->_value << ' ';
	_InOrder(root->_right);
}

template<class K, class V>
void  BSTree<K, V>::InOrder()//中序遍历
{
	_InOrder(_root);
	cout << endl;
}

节点的构造:

//二叉树节点
template<class K, class V>
BSTreeNode<K,V>::BSTreeNode(const K& key, const V& value)//默认构造
	:_left(nullptr)
	,_right(nullptr)
	,_key(key)
	,_value(value)
{}

 

实现中序遍历是为了方便打印观察,看看再执行插入删除的操作后,二叉搜索树的性质有没有发生错误。 

        增删查:

//二叉树主体///
template<class K,class V>
bool BSTree<K, V>::Insert(const K& key, const V& value)//插入数据
{
	if (_root == nullptr)
	{
		_root = new Node(key, value);
		return true;
	}
    //先查找数据是否已经存在,存在则返回插入失败
	if (Find(key) != nullptr)
		return false;
	Node* pcur = _root;
	Node* parent = nullptr;
	while (pcur)
	{
		if (key == pcur->_key)
			return false;
		parent = pcur;
		if (key > pcur->_key)
			pcur = pcur->_right;
		else
			pcur = pcur->_left;
	}
	pcur = new Node(key, value);
	if (key > parent->_key)
		parent->_right = pcur;
	else
		parent->_left = pcur;
	return true;
}

template<class K,class V>
bool  BSTree<K, V>::Erase(const K& key)//删除数据
{
	//树为空
	if (_root == nullptr)
		return false;
	Node * pcur = _root;
	Node* parent = nullptr;
	while (pcur)
	{
		if (key == pcur->_key)
			break;
		parent = pcur;
		if (key > pcur->_key)
			pcur = pcur->_right;
		else
			pcur = pcur->_left;
	}
	//指定删除数据不存在
	if (pcur == nullptr)
		return false;
	Node* left = pcur->_left;
	Node* right = pcur->_right;
	//左右子树都为空
	if (left == nullptr && right == nullptr)
	{
		//删除节点为根节点
		if (pcur == _root)
			_root = nullptr;
		else
		{
			//删除节点不为根节点
			if (pcur->_key > parent->_key)
				parent->_right = nullptr;
			else
				parent->_left = nullptr;
		}
		delete pcur;
		return true;
	}
	//右子树为空,左子树不为空
	if (right == nullptr)
	{
		//删除节点为根节点
		if (pcur == _root)
		{
			_root = left;
			delete pcur;
			return true;
		}
		//删除节点不为根节点
		if (pcur->_key > parent->_key)
			parent->_right = left;
		else
			parent->_left = left;
		delete pcur;
		return true;
	}
	//左子树为空,右子树不为空
	if (left == nullptr)
	{
		//删除节点为根节点
		if (pcur == _root)
		{
			_root = right;
			delete pcur;
			return true;
		}
		//删除节点不为根节点
		if (pcur->_key > parent->_key)
			parent->_right = right;
		else
			parent->_left = right;
		delete pcur;
		return true;
	}
	//左右子树都不为空
	//找到左子树最大节点和删除节点“交换”
	//找右子树最小节点也可以,逻辑相反
	Node* leftmax = left;
	Node* leftmaxp = pcur;
	while (leftmax->_right)
	{
		leftmaxp = leftmax;
		leftmax = leftmax->_right;
	}
	//左子树最大节点右子树为空,左子树交给父节点
	if (leftmax->_key > leftmaxp->_key)
		leftmaxp->_right = leftmax->_left;
	else
		leftmaxp->_left = leftmax->_left;
	//用leftmax的内容覆盖pcur,这样就不会变更根节点
	//如果真的交换两个节点,就可能需要更新根节点,会很麻烦
	pcur->_key = leftmax->_key;
	pcur->_value = leftmax->_value;
	//释放原来的leftmax
	delete leftmax;
	return true;
}

template<class K, class V>
typename BSTree<K, V>::Node* BSTree<K, V>::Find(const K& key)//查找数据
{
	Node* tmp = _root;
	while (tmp)
	{
		if (key == tmp->_key)
			return tmp;
		if (key > tmp->_key)
			tmp = tmp->_right;
		else
			tmp = tmp->_left;
	}
	return nullptr;
}

        最重要的部分就是删除操作,可以结合前面的原理理解代码逻辑。 需要注意的细节是删除节点有左右孩子的情况,“替换”操作实际上是覆盖被删除节点的数据,比起真正的替换操作,覆盖操作可以不改变周围节点的连接情况,而且在删除节点是根节点的特殊情况下,删除后不用更新根节点。

 4. 二叉搜索树的应用

        二叉搜索树的应用可以非为两种模式:

        1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
        比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
        以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树。
        在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

        2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
        比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;
        再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。

        (上面模拟实现的就是简易的KV模型二叉搜索树)。

5. 二叉搜索树性能

        首先因为插入和删除操作都必须先查找,所以查找效率代表了二叉搜索树中各个操作的性能。
        对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
        但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

 

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

        可以看到,二叉搜索树的最优和最差情况性能相差非常大,为了解决二叉搜索树的退化问题,就出现了AVL树和红黑树。