C++学习记录——이십일 AVL树


1、了解AVL树

如果数据有序或接近有序,二叉搜索树将退化为单支数,查找元素相当于在顺序表中搜索元素,效率低下。因此有两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的办法:当向二叉搜索树插入新节点后,如果能保证每个结点的左右子树高度差的绝对值不超过1,即可降低树的高度,减少平均搜索长度。

AVL树也叫高度平衡搜索树。左右子树高度差叫平衡因子。平衡因子只是AVL树实现的一种方式,还有别的方式。在平衡因子方式中,如果平衡因子是-1,0,1,那么就是AVL树,不是就需要调整。

2、模拟实现

博客末尾有整体代码的链接

结构

template<class K, class V>
struct AVLTreeNode
{
    
    
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{
    
    }
};

Insert

template<class K, class V>
class AVLTree
{
    
    
	typedef AVLTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
    
    
		if (_root == nullptr)
		{
    
    
			_root = new Node(kv);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
    
    
			if (cur->_kv.first < kv.first)
			{
    
    
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
    
    
				parent = cur;
				cur = cur->_left;
			}
			else
			{
    
    
				return false;
			}
		}
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
		{
    
    
			parent->_left = cur;
		}
		else
		{
    
    
			parent->_right = cur;
		}
		cur->_parent = parent;//已经插入好了,关系也都搞清楚了,cur就是新节点,parent就是它的父节点
		return true;
	}
private:
	Node* _root = nullptr;
};

这是一个基本的AVL树的代码,bf就是平衡因子。在此基础上开始加功能。

如何写?假如这个新结点插入在左子树最下边一个结点的后面,它有三个可能,在最下结点的左右两边,或者和这个结点同一层。插入完成后,平衡因子要更新,更新完后就得查看bf是否还是符号要求,如果绝对值大于1,平衡结构收到影响,就需要旋转处理。

插入结点后这个结点只会影响它的部分祖先,也就是那一个子树里的某些结点。这时候就可以利用parent来访问祖先。

如果新结点在父节点右边,父节点的因子++;如果在左边,因子–。更新完后父结点的平衡因子如果等于1或者-1,说明高度变了,要更新爷爷结点的因子。

从cur和parent现在的位置出发,cur新增结点当然是0,假如在parent右边,parent的因子变为1,cur来到parent位置,parent来到上面的结点位置,假如原parent所在结点在父结点的右侧,那么因为在右侧所以要++,现在新parent的位置就会变成2,这就出来另一种情况了,更新完后新parent的因子变为2,或者可能出现-2,这时候parent所在的子树不平衡,就需要处理一遍,也就是旋转。

如果插入新结点后,parent的因子是0,就不需要往上调整了。说明这时候parent原本是1或者-1,插入之后就高度不变,就可以插入结束了,这时候parent左右子节点都有,所以高度不需要调整。

所以就是

parent的bf绝对值是1,那就需要往上更新
parent的bf是0,那就插入结束
parent的bf绝对值大于1,那就旋转

可能会出现这种情况,一路更新到根节点。

在上面的insert函数里,我们继续这样写

		while (parent)
		{
    
    
			if (cur == parent->_right)
			{
    
    
				parent->_bf++;
			}
			else
			{
    
    
				parent->_bf--;
			}
			//比较因子
			if (parent->_bf == 1 || parent->_bf == -1)
			{
    
    
				//继续更新
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (parent->_bf == 0)
			{
    
    
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
    
    
				//需要旋转处理
			}
			else
			{
    
    
				//为了防止出现更多绝对值大于1的情况
				assert(false);
			}
		}

接下来就是旋转

3、旋转

1、左单旋(右边高,插入在右边)

在这里插入图片描述

abc都是高度为h的子树。比如h == 2,就可以有两个子树或一个左子树或一个右子树的3种情况。c一定是有两个子树的,而ab则是三种情况中任意一种。

h == 1

在这里插入图片描述

在最底层最右边,也就是c,无论插入在左还是右,c的因子都会变化,对于60这个结点来说,它的右子树这里插入新结点,所以它+1,而30也是右子树插入了一个新节点而变为2。

h == 0,abc位置没有结点,插入后往上更新因子,各因子的变化如同上一段。

h == 2,c有四个位置可插入,无论哪一个,都会让30结点的因子变为2。如果c结点不是有两个子树,就不会让30这个结点变为2。假如c有一棵子树,那么再插入一个节点后,c那里就需要旋转了,也就不会影响上面的节点。在这种情况下,ab总共有9种组合,c可以有4个变化,所以有36个组合。

h == 0和1,各有2个组合,总共40个组合。无论怎么样,都是左单旋。最后都要变成第一个图第三个模样。

b变成30的右边,30变成60的左边,60变成整棵树的根。

旋转的原则是保持搜索树,并且左右均衡,降低整棵树的高度

2、右单旋(左边高,插入在左边)

在这里插入图片描述

在这里插入图片描述

了解左单旋后,右单旋就会更容易理解。

b变成60的左边,60变为30的右边,30成为整棵树的根。

右单旋情况分析和左单旋一样。

调用的条件

			//前面这几个判断,是在找因子是2的那个节点
			//代入情景想一下,因子是1或-1就向上走,再次循环
			//整棵树都是1、-1、0那就没问题,找到2,那也就进入了这个判断
			//这时候问题parent就是问题节点,cur就是它的子节点,下面就开始了判断
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
    
    
				//旋转
				if (parent->_bf == 2 && cur->_bf == 1)//右边高,左单旋
				{
    
    
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)//左边高,右单旋
				{
    
    
					RotateR(parent);
				}
			}

旋转代码(放在private里,单旋实现的详细情况放在代码实现中)

	void RotateL(Node* parent)//左单旋
	{
    
    
	     //画图来理清这些关系
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		//前置设定好后,开始旋转。要考虑清除,每个结点有三个指针的,三叉链
		//subRL可能为空
		//旋转的不一定是整棵树
		parent->_right = subRL;
		if (subRL)//如果是空,也就不管了,因为也就是说根本没有这个节点,这里被初始化为空,所以就直接指向空了
			subRL->_parent = parent;
		Node* ppnode = parent->_parent;//先拿走父节点的父节点,也就是要改变的部分的根,因为下面要改变根
		subR->_left = parent;
		parent->_parent = subR;
		//前面这些已经改了很多关系了
		//总体来说,除了原parent的父节点还指向这个parent,其他关系都已经调整好了,所以只剩这个爷节点的关系了
		if (ppnode == nullptr)//到这里判断是不是根节点,是根节点就直接让定义新的根就可以了
		{
    
    
			_root = subR;
			_root->_parent = nullptr;
		}
		else//不是就调整爷节点的指针指向新根
		{
    
    
			if (ppnode->_left == parent)
			{
    
    
				ppnode->_left = subR;
			}
			else
				ppnode->_right = subR;
			subR->_parent == ppnode;
		}
		parent->_bf = subR->_bf = 0;
	}

    //左单旋详细说了关系调整,右单旋也是一样,画图就清楚了
	void RotateR(Node* parent)//右单旋
	{
    
    
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		Node* ppnode = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (ppnode == nullptr)
		{
    
    
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
    
    
			if (ppnode->_left == parent)
			{
    
    
				ppnode->_left = subL;
			}
			else
				ppnode->_right = subL;
			subL->_parent = ppnode;
		}
		subL->_bf = parent->_bf = 0;
	}

3、双旋(先左后右)

h == 0,abcd都不存在,60结点也不存在,60这个结点就相当于新增。此时30因子是1,90因子是-2。

在这里插入图片描述

上图为h == 1,60的左右子树插入结点后都需要旋转。

在这里插入图片描述

h == 2。bc是1,都是1个结点,ad则是三种情况的一种,b或者c的任意一个孩子位置插入结点都会引发旋转,这样总共36种,加上上面的,总共40种。

这时候一个单旋不能解决问题,需要两个单旋。不论b还是c插入,30和60都需要进行左旋一次,左旋后会发现,二叉树一边高,60和30都在90的左子树上,所以需要从90开始右旋。

	void RotateLR(Node* parent)
	{
    
    
		RotateL(parent->_left);
		RotateR(parent);
	}

双选控制因子比较麻烦。b位置插入的话,60原始因子是-1,最后30 60 90对应的因子分别是001;如果是c插入,60原始因子是1,就是-100;h == 0的时候,两种情况分别是1 0 -2,0 0 0。上图是b插入,下图是c插入。这里还是要画图理解。

在这里插入图片描述

	void RotateLR(Node* parent)
	{
    
    
		//parent为90,subL是30,subLR是60
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 1)
		{
    
    
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == -1)
		{
    
    
			parent->_bf = 1;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else if(bf == 0)
		{
    
    
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
    
    
			assert(false);
		}
	}

4、双旋(先右后左)

h == 0,60就是新增结点。

h == 1,60下面没有子树,30有一个左结点,90有一个右结点,那么在60左右新增结点都会引发旋转

h == 2,那么a和d就有三种情况,60有两个子结点bc,bc可以新增4个结点。

有三种情况会引发双旋,c插入,c高度变化为h;b插入,b高度变化为h;60本身是新增点。要判断是哪个情况就判断60的因子,也就是1,-1,0

	void RotateRL(Node* parent)
	{
    
    
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == 1)
		{
    
    
			parent->_bf = -1;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else if (bf == -1)
		{
    
    
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 0)
		{
    
    
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
    
    
			assert(false);  
		}
	}

进入旋转前的判断条件

		while (parent)
		{
    
    
			if (cur == parent->_right)
			{
    
    
				parent->_bf++;
			}
			else
			{
    
    
				parent->_bf--;
			}
			if (parent->_bf == 1 || parent->_bf == -1)
			{
    
    
				//继续更新
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (parent->_bf == 0)
			{
    
    
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
    
    
				//需要旋转处理
				if (parent->_bf == 2 && cur->_bf == 1)
				{
    
    
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
    
    
					RotateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
    
    
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
    
    
					RotateRL(parent);
				}
				else
				{
    
    
					assert(false);
				}
				break;
			}
			else
			{
    
    
				//为了防止出现更多绝对值大于1的情况
				assert(false);
			}
		}

测试代码:

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

	void _InOrder(Node* root)
	{
    
    
		if (root == nullptr)
		{
    
    
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
	
void Test_AVLTree1()
{
    
    
	int a[] = {
    
     16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t1;
	for (auto e : a)
	{
    
    
		t1.Insert(make_pair(e, e));
	}
	t1.InOrder();
}

4、检查平衡

这里检查高度即可,平衡因子因为是我们自己定的,用来判断是否是平衡二叉树有些不妥。

	int _High(Node* root)
	{
    
    
		if (root == NULL) return 0;
		int leftH = _High(root->_left);
		int rightH = _High(root->_lright);
		return leftH > rightH ? leftH + 1 : rightH + 1;
	}

	bool _IsBalance(Node* root)
	{
    
    
		if (root == NULL) return true;
		int leftH = _High(root->_left);
		int rightH = _High(root->_right);
		//所有子树都要检查,所以除了根结点的左右子树,还有剩下结点的
		return abs(leftH - rightH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
	
	bool IsBalance()
	{
    
    
		return _IsBalance(_root);
	}

	int Height()
	{
    
    
		return _Height(_root);
	}

两个测试用例
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

虽然树正常,但是平衡因子的数值可能不对劲。比如一个结点和它的右子树的因子都是0,这时候再次插入就会引发因子数值的错误。这样改

	bool _IsBalance(Node* root)
	{
    
    
		if (root == NULL) return true;
		int leftH = _High(root->_left);
		int rightH = _High(root->_right);
		if (rightH - leftH != root->_bf)
		{
    
    
			cout << root->_kv.first << "节点平衡因子异常" << endl;
			return false;
		}
		//所有子树都要检查,所以除了根结点的左右子树,还有剩下结点的
		return abs(leftH - rightH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
void Test_AVLTree1()
{
    
    
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = {
    
     4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int, int> t1;
	for (auto e : a)
	{
    
    
		t1.Insert(make_pair(e, e));
		cout << e << "" << t1.IsBalance() << endl;//每插入一次就打印看看谁插入出了问题
	}
	t1.InOrder();
	cout << t1.IsBalance() << endl;
}

把LR的函数中调整因子的代码屏蔽掉,第二个测试用例就会出现6节点异常。

5、测试性能(随机数)

void Test_AVLTree2()
{
    
    
	srand(time(0));
	const size_t N = 100000;
	AVLTree<int, int> t;
	for (size_t i = 0; i < N; ++i)
	{
    
    
		size_t x = rand();
		t.Insert(make_pair(x, x));
	}
	cout << t.IsBalance() << endl;
}

AVL树的性能和满二叉树差不多,高度是logn,增删查改是logn,插入10亿个结点需要最多查找30次。

6、删除

整体和插入呈现相反的思路。比如删除右边子节点,父节点因子-1,删除后如果父节点是-1就不需要调整,如果是0,那么就需要调整。

下一篇是红黑树。

AVL树

结束。

猜你喜欢

转载自blog.csdn.net/kongqizyd146/article/details/130372058