首页 > 其他分享 >AVL树介绍与构建

AVL树介绍与构建

时间:2024-10-24 20:17:15浏览次数:18  
标签:Node bf parent 介绍 AVL 构建 root 节点 left

目录

AVL树的概念

二叉树的构建

平衡因子的更新

旋转

左单旋

旋转过程

左单旋代码

右单旋

旋转过程

右单旋代码

左右双旋

发生情况

抽象图

具体图

平衡因子更新

左右双旋代码

右左双旋

右左双旋旋代码

验证测试AVL树

测试成员函数

测试代码

AVL树实现代码

AVL树的删除(了解)

AVL树的性能


        在之前对map/multimap/set/multiset进行了简单的介绍,而这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现

AVL树的概念

一个高等平衡的二叉树

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


一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

        ·它的左右子树都是AVL树
        ·左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

        如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O(log n),搜索时间复杂度O(log n)

二叉树的构建

参考文章:

平衡因子的更新

平衡因子更新规则

1、parent->_bf == 0说明之前parent->_bf 是 1 或者 -1说明之前parent一边高一边低,这次插入填上矮的那一边,parent所在的字数高度不变,不需要继续向上更新


2、parent->_bf == 1 或者 -1 说明之前parent->_bf == 0,两边一样高,现在插入一边更高了,parent所在子树高度变高了,继续往上更新


3、parent->_bf == 2 或者 -2,说明之前 parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则,就地处理 -- 旋转

旋转

根据AVL树概念可以得到以下的旋转规则

旋转规则:
        1、让这颗子树左右高度差不超过1
        2、旋转过程中继续保持他是搜索树
        3、更新调整孩子节点的平衡因子
        4、让这颗子树的高度跟插入前保持一致

左单旋

抽象图 

旋转过程

        需要注意的是,这里的旋转可能是指的是 子树 或者 整棵树,注意到第四条规则,保持让这颗子树的高度跟插入前保持一致,0的话就不需要继续向上更新,但是还是1或者-1的话就需要继续向上更新平衡因子了,假如又出现2的平衡因子那么就需要继续旋转,直到根节点

左单旋代码

	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;	//父节点的右孩子
		Node* subRL = subR->_left;		//父节点的右孩子的左孩子

		parent->_right = subRL;
		if (subRL)						//如果subRL不为空,那么它的父节点更新为parent
			subRL->_parent = parent;

		Node* ppNode = parent->_parent;	//保留父节点的父节点(因为这可能是一个子树)
		subR->_left = parent;			//更新subR的左孩子
		parent->_parent = subR;			//把之前的父节点更新为subR的左孩子

		if (ppNode == nullptr)	//假如ppNode为空,说明这是正棵树
		{
			_root = subR;				//更新root节点
			_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 (_root == parent); 这种写法也是一个道理
		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;
	}

左右双旋

发生情况

抽象图

具体图

平衡因子更新

这里是b插入,c插入大同小异

左右双旋代码

	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;	//记录父亲的左子树
		Node* subLR = subL->_right;//记录父亲的左子树的右子树
		int bf = subLR->_bf;	//记录subLR节点的平衡因子,便以判断插入在b(左子树)还是c(右子树)

		//先左旋转,再右旋转
		RotateL(parent->_left);
		RotateR(parent);

		//判断插入情况并更新平衡因子
		if (bf == -1)		//subLR左子树新增
		{
			subL->_bf = 0;	//这里的平衡因子在上面旋转的时候就已经更新了,但是为了不耦合单旋代码,特地在这里再进行一次更新
			parent->_bf = 1;//这里实际更新的(如果单旋正常工作)
			subLR->_bf = 0;	//正常更新平衡因子一定会为0
		}
		else if (bf == 1)	//subLR右子树新增
		{
			subL->_bf = -1;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)	//subLR自己就是新增的节点,单折线型
		{
			subL->_bf = 0;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);	//程序出错
		}
	}

右左双旋

实际操作和左右双旋大致一样,这里是c插入

 

右左双旋旋代码

//右左双旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		//先右旋转,再左旋转
		RotateR(parent->_right);
		RotateL(parent);

		//判断插入情况并更新平衡因子
		if (bf == -1)
		{
			subR->_bf = 1;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			parent->_bf = -1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			subR->_bf = 0;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

验证测试AVL树

测试成员函数

int Height(Node* root)	//递归遍历高度
	{
		if (root == nullptr)
			return 0;

		int lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}

	bool IsBalance()	//便以外面调用
	{
		return _IsBalance(_root);
	}

	bool _IsBalance(Node* root)	//判断是否平衡,这里我们不选择通过平衡因子判断是否平衡,因为平衡因子是我们自己更新的
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)	//顺便测试平衡因子,并打印出异常地方
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		return abs(rightHeight - leftHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

测试代码

void TestAVLTree1()	//测试代码
{
	//int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	//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> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
	
	t.Inorder();

	cout << t.IsBalance() << endl;
}

void TestAVLTree2()	//测试代码,随机数的检查
{
	srand((unsigned int)time(NULL));
	const size_t N = 10000;
	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;
	}

	t.Inorder();

	cout << t.IsBalance() << endl;
}

旋转总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋


2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新

AVL树实现代码

#pragma once
#include<iostream>
#include<assert.h>
#include<time.h>
using namespace std;

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

	int _bf;	//balance factor 平衡因子

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

template<class K, class V>
struct 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->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//更新平衡因子
		while (parent)	//更新到根节点的时候停止
		{
			if (cur == parent->_left)	//左边新增,--
			{
				parent->_bf--;
			}
			else                      //右边新增,++
			{
				parent->_bf++;
			}

			//1、parent->_bf == 0说明之前parent->_bf 是 1 或者 -1
			//说明之前parent一边高一边低,这次插入填上矮的那一边,parent所在的字数高度不变,不需要继续向上更新
			//2、parent->_bf == 1 或者 -1 说明之前parent->_bf == 0,两边一样高,现在插入一边更高了
			//parent所在子树高度变高了,继续往上更新
			//3、parent->_bf == 2 或者 -2,说明之前 parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
			//就地处理 -- 旋转

			//旋转:
			//1、让这颗子树左右高度差不超过1
			//2、旋转过程中继续保持他是搜索树
			//3、更新调整孩子节点的平衡因子
			//4、让这颗子树的高度跟插入前保持一致
			if (parent->_bf == 0)	//当平衡因子为0时停止跳出循环
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			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
			{
				assert(false); //再有情况就直接断死,已经坏掉了(代码出错)
			}
		}

		return true;	//插入成功
	}

	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;	//父节点的右孩子
		Node* subRL = subR->_left;		//父节点的右孩子的左孩子

		parent->_right = subRL;
		if (subRL)						//如果subRL不为空,那么它的父节点更新为parent
			subRL->_parent = parent;

		Node* ppNode = parent->_parent;	//保留父节点的父节点(因为这可能是一个子树)
		subR->_left = parent;			//更新subR的左孩子
		parent->_parent = subR;			//把之前的父节点更新为subR的左孩子

		if (ppNode == nullptr)	//假如ppNode为空,说明这是正棵树
		{
			_root = subR;				//更新root节点
			_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 (_root == parent); 这种写法也是一个道理
		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;
	}

	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;	//记录父亲的左子树
		Node* subLR = subL->_right;//记录父亲的左子树的右子树
		int bf = subLR->_bf;	//记录subLR节点的平衡因子,便以判断插入在b(左子树)还是c(右子树)

		//先左旋转,再右旋转
		RotateL(parent->_left);
		RotateR(parent);

		//判断插入情况并更新平衡因子
		if (bf == -1)		//subLR左子树新增
		{
			subL->_bf = 0;	//这里的平衡因子在上面旋转的时候就已经更新了,但是为了不耦合单旋代码,特地在这里再进行一次更新
			parent->_bf = 1;//这里实际更新的(如果单旋正常工作)
			subLR->_bf = 0;	//正常更新平衡因子一定会为0
		}
		else if (bf == 1)	//subLR右子树新增
		{
			subL->_bf = -1;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)	//subLR自己就是新增的节点,单折线型
		{
			subL->_bf = 0;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);	//程序出错
		}
	}

	//右左双旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		//先右旋转,再左旋转
		RotateR(parent->_right);
		RotateL(parent);

		//判断插入情况并更新平衡因子
		if (bf == -1)
		{
			subR->_bf = 1;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			parent->_bf = -1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			subR->_bf = 0;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void Inorder()	//让外部调用内部的中序,因为外部不好传根节点,我们在内部调用可以解决这一点
	{
		_Inorder(_root);
	}

	void _Inorder(Node* root)	//内部的中序,对于排序好的二叉树来说,中序遍历刚好可以按照顺序遍历
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	int Height(Node* root)	//递归遍历高度
	{
		if (root == nullptr)
			return 0;

		int lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}

	bool IsBalance()	//便以外面调用
	{
		return _IsBalance(_root);
	}

	bool _IsBalance(Node* root)	//判断是否平衡,这里我们不选择通过平衡因子判断是否平衡,因为平衡因子是我们自己更新的
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)	//顺便测试平衡因子,并打印出异常地方
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		return abs(rightHeight - leftHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

private:
	Node* _root = nullptr;
};

void TestAVLTree1()	//测试代码
{
	//int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	//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> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
	
	t.Inorder();

	cout << t.IsBalance() << endl;
}

void TestAVLTree2()	//测试代码,随机数的检查
{
	srand((unsigned int)time(NULL));
	const size_t N = 10000;
	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;
	}

	t.Inorder();

	cout << t.IsBalance() << endl;
}

AVL树的删除(了解)
 

        因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置,具体实现就不是AVL树的强项了,有兴趣可以自行学习

AVL树的性能


        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。

        但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

        因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

标签:Node,bf,parent,介绍,AVL,构建,root,节点,left
From: https://blog.csdn.net/weixin_67595436/article/details/129308422

相关文章