C++_AVL树插入,查找与修改的实现(Key_Value+平衡因子+三叉链)

Posted dodamce

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++_AVL树插入,查找与修改的实现(Key_Value+平衡因子+三叉链)相关的知识,希望对你有一定的参考价值。

1.AVL树的提出

二叉搜索树虽然可以提高搜索效率,但如果数据接近有序的话搜索二叉树的效率退化为链表了。为了解决这个问题,提出了AVL树。

向平衡二叉树中插入新节点,保证每个节点的高度差的绝对值小于等于1。降低树的高度,提高搜索效率。这种树称为AVL树

平衡因子

每个节点的平衡因子=这个节点的右子树高度-这个节点的左子树高度。

注意:平衡因子不是AVL树必须的,这里实现AVL树时节点选择了添加了平衡因子

2.AVL树的节点定义

template<class Key,class Val>
struct AVLNode
{
	AVLNode<Key, Val>* _Left;
	AVLNode<Key, Val>* _Right;
	AVLNode<Key, Val>* _Parent;//记录节点的父节点,方便旋转

	int _bf;//平衡因子
	pair<Key, Val> _KV;
	AVLNode(const pair<Key, Value>& kv) :_Left(nullptr), _Right(nullptr)
		, _Parent(nullptr), _bf(0), _KV(kv)
	{}
};

3.AVL树类

template<class Key, class Val>
class AVLTree
{
public:
	typedef AVLNode<Key, Val> Node;
	AVLTree() :_root(nullptr) {}

	pair<Node*,bool> Insert(const pair<Key, Val>& kv);//向AVL树中插入节点,插入成功返回这个位置的指针和true,失败返回这个位置的指针和false

	Node* Find(const Key& key);//通过键值来查找对应键值节点,成功返回对应节点的指针,失败返回空

	void PrintTree()//中序打印AVL树
	{
		_PrintTree(_root);
	}

private:
	Node* _root;

	void _PrintTree(Node* root)
	{
		if (root == nullptr)
			return;
		_PrintTree(root->_Left);
		cout << root->_KV.first << "->" << root->_KV.second << endl;
		_PrintTree(root->_Right);
	}
	/*
	四种旋转,看下文分析,下面的四种旋转函数为类的成员函数
	*/
};

①AVL树插入节点(插入成功返回一个pair值)

向AVL树中插入节点是否旋转取决于树中的平衡因子,当平衡因子的绝对值大于1证明需要调整AVL树的高度。

更新平衡因子

插入节点后,这个节点的父节点到根节点的路径上的所有节点的平衡因子可能要改变。新插入的节点平衡因子为0,这在构造函数中就可以保证。

平衡因子=右树-左树。
所以插入节点在父节点左边,父节点的平衡因子-1,反之父节点的平衡因子+1;

如果父节点经过调整后,平衡因子为1或者-1。说明原来父节点的因子为0,这次插入改变了父节点原来的高度,还要继续向上调整平衡因子。
如果父节点经过调整后,平衡因子变为0。说明原来父节点的因子为1或-1,这次插入没有改变父节点的高度,不需要继续向上调整平衡因子。
如果父节点经过调整后,平衡因子变为2或-2。说明此时不满足AVL树特性,要通过旋转来调整高度。

当平衡因子变为2或-2时,四种旋转调整AVL树

旋转的基本准则:在旋转后这棵树还是搜索树,只是让其尽可能的保持平衡

这里将二叉树抽象为高度为h的矩形来概括情况

右单旋(新节点插入较高左子树的左侧)


右单旋的情况为插入节点的父节点的平衡因子为-1,且这个节点父节点的父节点平衡因子为-2。
如上图表现为右高左低。将a节点连接到b右节点,将原来b右子树连接到a左子树上。
注意还要调节对应节点的父指针,让a的父指针指向b,Rh的父指针指向a(如果Rh存在的话,不存在(h=0)不需要处理),b的父指针指向空
旋转后如上图,修改平衡因子的值为0。
调整根节点:上图因为旋转的是根节点,这时只要把b作为根节点即可。
当旋转的是子树时,原来的a的父节点要被保存下来。在旋转完后b的父节点要指向这个节点。
只要发生旋转一定是平衡因子从1/-1到2/-2,旋转完后平衡因子变为0。相当于从1/-1到0,子树的高度没有发生变化,所以上面的平衡因子不变。不需要再向上调整了

_右单旋代码

函数要传入平衡因子为-2的节点指针,如上图要传入节点a的指针。

void _Single_Right(Node* parent)//根据图把对应关系连接起来
{
	//记录要移动的节点
	Node* SubL = parent->_Left;
	Node* SubLR = SubL->_Right;

	//连接
	parent->_Left = SubLR;
	if (SubL->_Right != nullptr)//修改父指针
	{
		SubLR->_Parent = parent;
	}
	//连接
	SubL->_Right = parent;
	Node* GradParent = parent->_Parent;//记录这个节点的父节点,为了修改根节点
	parent->_Parent = SubL;//修改父指针
	//修改平衡因子为0
	parent->_bf = 0; SubL->_bf = 0;
	//调整根节点
	if (parent == _root)//要旋转的节点为根节点
	{
		_root = SubL;
		SubL->_Parent = GradParent;
	}
	else//要旋转的节点是子树,修改GradParent指针
	{
		if (GradParent->_Left == parent)
		{
			GradParent->_Left = SubL;
		}
		else
		{
			GradParent->_Right = SubL;
		}
		SubL->_Parent = GradParent;
	}
}

左单旋(新节点插入较高右子树的右侧)


左单旋与右单旋情况类似:观察上图:
左单旋的情况为插入节点的父节点的平衡因子为1,且这个节点父节点的父节点平衡因子为2。

将bL连接到a的右树上,a再连接到b的左树上。调整平衡因子a,b平衡因子都变为0,调整a,b节点父指针,调整树的根节点,处理子树的情况,细节和右单旋类似。

_左单旋代码

void _Single_Left(Node* parent)//左旋转
{
	//记录要移动的节点
	Node* SubR = parent->_Right;
	Node* SubRL = SubR->_Left;

	//连接
	parent->_Right = SubRL;
	if (SubRL != nullptr)
	{
		SubRL->_Parent = parent;
	}
	SubR->_Left = parent;
	Node* GradParent = parent->_Parent;//记录这个节点的父节点,为了修改根节点
	parent->_Parent = SubR;

	//修改平衡因子
	parent->_bf = 0; SubR->_bf = 0;
	//调整根节点
	if (parent == _root)
	{
		_root = SubR;
		SubR->_Parent = GradParent;
	}
	else //要旋转的节点是子树,修改GradParent指针
	{
		if (GradParent->_Left == parent)//旋转的是左子树,连接到左边
		{
			GradParent->_Left = SubR;
		}
		else
		{
			GradParent->_Right = SubR;//反之
		}
		SubR->_Parent = GradParent;
	}
}

左右双旋(新节点插入较高左子树的右侧)


由上图可以看到a节点的平衡因子为-2,b节点的平衡因子为1时要进行左右双旋。
特点为:b节点进行左旋,a节点进行右旋。
因为上面已经写过左旋右旋的代码,直接复用即可。
但是这时要分析平衡因子的变化

观察上图,相当于把c节点拆开c做根节点,c的左子树给b右树,c的右子树给a的左树。c的左子树连接b,c的右子树连接a。从而达到降低树的高度的目的。

根据上面的分析可知c在旋转后平衡因子一定为0.
如果新节点插入的位置在c的左子树上,经过旋转会到b的右子树上,b的平衡因子为0,a的平衡因子为1.
如果新节点插入的位置在c的右子树上,经过旋转会到a的左子树上,b的平衡因子为-1,a的平衡因子为0(如上图).

特殊情况:当b节点没有子树时,即这颗树只有cba这三个节点,此时a,b,c这三个节点的平衡因子都为0。

上面的这三种情况都是以c节点的平衡因子来判断的,如果c的平衡因子变为1,说明其插入到了c节点的右子树上,对应第二种情况。如果c的平衡因子变为-1,说明其插入到了c节点的左子树上,对应第一种情况。如果c作为插入的节点,此时对应的是特殊情况。

先记录旋转改变的这三个节点指针,通过判断SubLR中平衡因子来判断是那种情况,与上面的对应处理即可。

_左右双旋代码

void _Single_LeftRight(Node* parent)//左右双旋
{
	//记录节点的位置
	Node* SubL = parent->_Left;
	Node* SubLR = SubL->_Right;
	//先左旋,再右旋
	_Single_Left(parent->_Left);
	_Single_Right(parent);
	//根据SubRL判断是什么类型,修改平衡因子
	if (SubLR->_bf == 1)//插入到右子树上
	{
		SubLR->_bf = 0;
		SubL->_bf = -1;
		parent->_bf = 0;
	}
	else if (SubLR->_bf == -1)//插入到左子树上
	{
		parent->_bf = 1;
		SubL->_bf = 0;
		SubLR->_bf = 0;
	}
	else if (SubLR->_bf == 0)//特殊情况,插入后只有三个节点,三个节点的平衡因子都为0。
	{
		parent->_bf = 0;
		SubL->_bf = 0;
		SubLR->_bf = 0;
	}
	else//发生错误
	{
		assert(false);
	}
}

右左双旋(新节点插入较高右子树的左侧)


由上图可以看到a节点的平衡因子为2,b节点的平衡因子为-1时要进行右左双旋。
特点为:b节点进行右旋,a节点进行左旋。
具体细节看上图,这里不在赘述

_右左双旋代码

void _Single_RightLeft(Node* parent)//右左双旋
{
	//记录节点的位置
	Node* SubR = parent->_Right;
	Node* SubRL = SubR->_Left;
	//先右旋,再左旋
	_Single_Right(parent->_Right);
	_Single_Left(parent);
	//根据SubRL判断是什么类型,修改平衡因子
	if (SubRL->_bf == 1)//插入到右子树上
	{
		SubRL->_bf = 0;
		SubR->_bf = 0;
		parent->_bf = -1;
	}
	else if (SubRL->_bf == -1)//插入到左子树上
	{
		parent->_bf = 0;
		SubRL->_bf = 0;
		SubR->_bf = 1;
	}
	else if (SubRL->_bf == 0)//特殊情况,插入后只有三个节点,三个节点的平衡因子都为0。
	{
		parent->_bf = 0;
		SubR->_bf = 0;
		SubRL->_bf = 0;
	}
	else//发生错误
	{
		assert(false);
	}
}

AVL树插入C++实现

pair<Node*,bool> Insert(const pair<Key, Val>& kv)//向AVL树中插入节点,插入成功返回这个位置的指针和true,失败返回这个位置的指针和false
{
	//根节点为空
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return make_pair(_root, true);
	}
	//普通平衡二叉树的插入(不允许键值冗余)
	Node* parent = nullptr; Node* cur = _root;
	while (cur != nullptr)
	{
		if (kv.first > cur->_KV.first)
		{
			parent = cur;
			cur = cur->_Right;
		}
		else if (kv.first < cur->_KV.first)
		{
			parent = cur;
			cur = cur->_Left;
		}
		else//找到重复键值退出循环
		{
			return make_pair(cur, false);
		}
	}
	//cur==nullptr
	cur = new Node(kv);
	Node* NewCur = cur;//保存新插入的节点,来返回
	if (parent->_KV.first > kv.first)
	{
		cur->_Parent = parent;//链接上一个节点
		parent->_Left = cur;
	}
	else
	{
		cur->_Parent = parent;
		parent->_Right = cur;
	}
	/*------------------------------------------------ */
		//更新平衡因子(右-左)
	while (parent != nullptr)
	{
		//判断插入位置
		if (parent->_Left == cur)
		{
			parent->_bf--;
		}
		else
		{
			parent->_bf++;
		}
		if (parent->_bf == 0)
		{
			break;//说明插入后仍然满足AVL树,不需要调整了,插入结束。
		}
		else if (parent->_bf == 1 || parent->_bf == -1)//高度改变,继续向上调整
		{
			cur = parent;
			parent = parent->_Parent;//继续向上调整
		}
		else if (parent->_bf == 2 || parent->_bf == -2)//不满足AVL树特征,需要调整树结构
		{
			//旋转,对应四种不同的旋转,看节点的平衡因子
			if (parent->_bf == -2)
			{
				if (cur->_bf == -1)//右单旋
				{
					_Single_Right(parent);
				}
				else if (cur->_bf == 1)//左右双旋
				{
					_Single_LeftRight(parent);
				}
			}
			else//parent->_bf==2
			{
				if (cur->_bf == 1)//左单旋
				{
					_Single_Left(parent);
				}
				else if (cur->_bf == -1)//右左双旋
				{
					_Single_RightLeft(parent);
				}
			}
			break;
		}
		else//发生错误
		{
			assert(false);
		}
	}
	return make_pair(NewCur, true);
}

②判断是否是AVL树代码

思路:通过计算每个节点的左子树和右子树的差值的绝对值<1,并且这个差值和这个节点的平衡因子相同

二叉树节点高度=这个节点左树高度与这个节点右树高度的最大值+1。

先检查这个节点是不是符合AVL树,再递归检查其他节点

//私有:
bool _IsAVLTree(Node* _root)
{
	if (_root == nullptr)
		return true;
	int LeftHeight = _Height(_root->_Left);
	int RightHeight = _Height(_root->_Right);
	if (RightHeight - LeftHeight != _root->_bf)//发生错误
	{
		cout << "平衡因子错误" << endl;
		return false;
	}
	return abs(LeftHeight - RightHeight) < 2//检查左右子树的高度再递归检查左子树和右子树
		&& _IsAVLTree(_root->_Left) && _IsAVLTree(_root->_Right);
}

int _Height(Node* root)
{
	if (root == nullptr)
		return 0;
	int LeftTreeHeight = _Height(root->_Left);
	int RightTreeHeight = _Height(root->_Right);
	return max(LeftTreeHeight, RightTreeHeight) + 1;
}
//共有
template<class Key, class Val>
bool AVLTree<Key, Val>::IsAVLTree()
{
	return _IsAVLTree(_root);
}

③AVL树通过键值C++查找代码

与搜索二叉树查找类似,遍历树即可

Node* Find(const Key& key)//通过键值来查找对应键值节点,成功返回对应节点的指针,失败返回空
{
	Node* cur = _root;
	while (cur!=nullptr)
	{
		if (cur->_KV.first < key)
		{
			cur = cur->_Right;
		}
		else if (cur->_KV.first > key)
		{
			cur = cur->_Left;
		}
		else
		{
			return cur;
		}
	}
	return nullptr;
}

④AVL树的C析构函数

这棵树节点是由我们动态开辟的,为了防止内存泄漏,要手动写析构函数

思路:先递归销毁左子树,再递归销毁右子树,最后销毁节点。后序遍历

//私有
void _Destory(Node* root)
{
	if (root == nullptr)
		return;
	_Destory(root->_Left);
	_Destory(root->_Right);
	delete root;
}

~AVLTree()
{
	_Destory(_root);
}

⑤利用插入函数的返回值重载[]实现AVL树的修改

具体思路见博客:
利用插入函数返回值重载思路

这里直接写C++代码

Val& operator[](const Key& key)
{
	pair<Node*, bool>ret = Insert(make_pair(key, Val()));//利用插入函数的返回值
	return ret.first->_KV.second;
}

4.AVL树插入,修改C++代码

#pragma once

#include<iostream>
#include<assert.h>

using namespace std;

template<class Key,class Val>
struct AVLNode
{
	AVLNode<Key, Val>* _Left;
	AVLNode<Key, Val>* _Right;
	AVLNode<Key, Val>* _Parent;

	int _bf;//平衡因子
	pair<Key, Val> _KV;
	AVLNode(const pair<Key, Val>& kv) :_Left(nullptr), _Right(nullptr)
		, _Parent(nullptr), _bf(0), _KV(kv)
	{}
};

template<class Key, class Val>
class AVLTree
{
public:
	typedef AVLNode<Key, Val> NodeAVL树介绍与实现

AVL树介绍与实现

❤️数据结构入门❤️(2 - 2)- AVL 树

平衡二叉查找树AVL

二叉查找树,AVL,红黑树的Python实现

AVL树/红黑树介绍及插入操作实现