C++:AVL树保姆级详解

目录

AVL树概念

AVL树的性质

AVL树节点定义

AVL树的构造函数和拷贝函数

构造函数

拷贝构造

AVL树的插入(灰常重要)

更新平衡因子

什么时候向上调整平衡因子

如何向上调整平衡因子

旋转

左单旋

右左单旋

右单旋

左右单旋

AVL树的验证

验证是否是二叉搜索树

这个比较简单,只需要使用一次中序遍历,打印出来如果是有序的那么就是二叉搜索树;

验证平衡因子 

AVL树的删除

AVL树的性能 

AVL树实现完整代码


AVL树概念

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

efd5c9e3993848b5b9f7655793ff7eac.png 当搜索二叉树退化成一条链的时候,搜索的时间复杂度将会变成O(N),效率就会变的很低;说到底产生这的原因就是在二叉树的高度上没有要求;AVL树的实现解决了这个问题;

AVL树的性质

1.AVL树的左右子树也是AVL树;
2.AVL数中各个节点的平衡因子(_bf)(默认为右子树高度-左子树高度)的差值的绝对值不能超过1;
解释:AVL树的节点还会存储平衡因子;平衡因子是调节AVL树高度的标准;平衡因子=右子树高度-左子树高度;因为AVL树规定就是左右子树高度差不能超过1;所以平衡因子的绝对值不能超过1;如果平衡因子是2,就需要马上进行旋转;

AVL树节点定义

为了方便多种类型的使用,所以我们通常是使用模版;

template<class T>
struct BTNode
{
	struct BTNode<T>* _parent;//父节点
	struct BTNode<T>* _left;//左孩子节点
	struct BTNode<T>* _right;//右孩子节点
	T _data;//数据值
	int _bf = 0;//平衡因子  
	//构造函数
	BTNode(T data = T())//构造函数初始化
		:_parent(nullptr), _left(nullptr), _right(nullptr), _data(data)
	{}
};

左右节点都没问题,相比二叉搜索树多了个父节点;父节点是用来维护子树和根的关系的,后面会解释;
因为新建立的节点左右子树高度差为0;所以平衡因子初始化为0;
AVL树每次插入一个新节都需要调整平衡因子,平衡因子决定是否需要发生旋转;所以_bf(平衡因子),和父节点是十分重要的; 

AVL树的构造函数和拷贝函数

首先我们先确定AVLTree的成员变量;AVL树的是一个二叉树,所以我们只需要定义一个节点指针即可;

	template<class T>
	class AVLTree
	{
		typedef BTNode<T>  node;//取别名,方便使用
	private:
		node* _root = nullptr;  
    }

构造函数

	template <class T>
	class AVLTree
	{
		typedef BTNode<T>   node;
	public:
		AVLTree()
			:_node(nullptr)
		{}
    private:
    node* _node=nullptr;
    }

构造函数只需要把_node初始化为nullptr即可; 

拷贝构造

	template <class T>
	class AVLTree
	{
		typedef BTNode<T>   node;
	public:
		AVLTree(AVLTree<T>& tree)
			:_node(nullptr)
		{
			_node = copy(tree->_node);
		}
    private:
	//拷贝二叉树
	node* copy(const node* tree)
	{
		if (tree == nullptr)return nullptr;
		node* newnode = new node(tree->_data);
		newnode->_left = copy(tree->_left);
		newnode->_right = copy(tree->_right);
		return newnode;
	}
    node* _node=nullptr;
    }

拷贝构造函数的参数是是类;而拷贝树的参数是根节点指针;所以为了更加的清晰的实现;我们可以把copy函数和拷贝构造分开;将copy定义为私有,就能实现完美封装;
注意:copy的参数是指针,不能使用缺省值;

AVL树的插入(灰常重要)

AVL树的插入是在二叉搜索树的基础上添加了平衡因子的更新以及根据平衡因子实现对应的旋转;因此前一段代码两者是相同的;

//插入数据(需要判断是否插入成功)
bool  insert(const T& data)
{
	if (_node == nullptr)
	{
		_node = new node(data);
		return true;
	}
	node* cur = _node;
	node* parent = _node->_parent;
	while (cur)
	{
		if (data < cur->_data)//如果数据小就走左边
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (data > cur->_data)
		{
			parent = cur;
			cur = cur->_right;
		}
		else//等于节点的值
		{
			return false; //插入失败
		}
	}
	//这个时候cur为空
	cur = new node(data);   //开辟节点
	cur->_parent = parent;
	//判断是parent的左边还是右边
	if (data < parent->_data)
	{
		parent->_left = cur;
	}
	else
		parent->_right = cur;

	//更新平衡因子
	while (parent)//直到更新到根节点
	{
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		else
			parent->_bf++;
		//判断是否需要调整
		if (parent->_bf == 0)//说明不需要调整
		{
			break;
		}
		else  if (parent->_bf == 1 || parent->_bf == -1)//说明左边高
		{
			cur = parent;
			parent = parent->_parent;
		}
		else
		{
			if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);
			if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);
			if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);
			if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);
		}
	}
	return true;   
}

插入的位置在博客CSDN中,需要的可以看;下面来解释下什么时候更新平衡因子和如何更新平衡因子;

更新平衡因子

什么时候向上调整平衡因子

f54e26c619bd446893652e023eb65037.png

AVL树的最初形态就是高度为1和高度为2;在这两个高度中是不可能会出现向上调整的情况的;

下面我们列出继续向下插入的情况(因为二叉树具有对称性所以,我们只研究一侧规律即可)
这里我画的是右侧的情况
1806f30083ea4b2889d86de5eaf72f1a.png
总结:parent和cur的第一次更新不叫向上调整;第一次parent更新后的平衡因子数决定的了是否需要向上调整;
如果第一次更新后的parent的_bf=1或-1,说明parent子树更新前的_bf是0;插入新节点就是唯一的自孩子,parent子树的高度改变了,就会影响到parent之上的节点的平衡因子,这个时候就需要向上调整;
如果第一次更新后的parent的_bf=0;说明parent子树更新前_bf,不是1就是-1,插入的新节点是另一个叶节点的兄弟,并没有改变parent树的高度,这个时候不需要向上调整平衡因子;

如何向上调整平衡因子

逻辑:如果我(cur)是你(parent)的左子树,那么你的bf就-- ,如果我是你的右子树,那么你的bf就++(因为平衡因子的大小=右子树的高度-左子树的高度。)parent和cur每指向一个节点就会触发更新平衡因子的信号,而决定_bf怎么改变的动机就是cur是parent的左节点还是右节点;

//更新平衡因子
while (parent)//直到更新到根节点
{
	if (cur == parent->_left)//更新平衡因子的规则
	{
		parent->_bf--;
	}
	else
		parent->_bf++;
	//判断是否需要调整
	if (parent->_bf == 0)//说明不需要调整
	{
		break;
	}
	else  if (parent->_bf == 1 || parent->_bf == -1)//需要向上调整
    {
		cur = parent;/让parent和cur向上移动,触发更新平衡因子的信号
		parent = parent->_parent;
	}
}

一旦向上调整就会调整到根节点,所以循环的结束条件就是parent不为空;(根节点的parent为nullptr);

但是呢,在一路向上更新的_bf的同时,会不会出现_bf等于2的情况呢?我们来看个例子:
fed4054eb8f649b3a8f1de13b339d286.png
我们每次调整的都是parent的平衡因子,平衡因子出现时,一定是parent的_bf=2;这个时候如果要更新平衡因子,就需要对parent和cur进行旋转;旋转后面会解释;
完善下更新平衡因子代码:

	//更新平衡因子
while (parent)//直到更新到根节点
{
	if (cur == parent->_left)//更新平衡因子的规则
	{
		parent->_bf--;
	}
	else
		parent->_bf++;
	//判断是否需要调整
	if (parent->_bf == 0)//说明不需要调整
	{
		break;
	}
	else  if (parent->_bf == 1 || parent->_bf == -1)//需要向上调整
    {
		cur = parent;/让parent和cur向上移动,触发更新平衡因子的信号
		parent = parent->_parent;
	}
	else
		{
               //根据parent和cur的平衡因子旋转
			if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);
			else if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);
			else if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);
			else if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);
			else
				assert(false);   
            break;    
		}
	}
	return true;   
}

注意:因为旋转后就已经是平衡的,所以只要发生了任意情况的旋转后,就不需要在向上调整了;直接break; 

旋转

二叉树具有对称性,所以只研究一侧;所以下面我详细讲的都是右侧的情况,左侧的类似,后面直接给出结果;

左单旋

什么时候左单旋呢?当subR在parent的右子树上,新节点在subR的右子树上时,也就是两个节点都在上一个关键节点(parent,subR)的右子树上;所以右旋的口诀是右右(左)单旋;

下面是单旋转的图示:

b496c6dcf99a46658fa7f7f9add0e6d6.png根据图画,一目了然;
单旋的详细操作
其实从上图我们清晰的发现,关键节点只有四个(parent,subR,subRL,新节点);为了保持平衡树的特性,旋转过程中会发生节点的断裂,左旋中parent会成为subR的左孩子,而subR的左孩子会成为parent的右孩子,这也是为什么要给subRL命名的原因(这个节点是需要断裂重连的);

旋转完毕后我们需要重新更新这些个节点的平衡因子;从上图我们可以发现单旋过后2个节点的平衡因子(parent,subR)都是0;
注意subRL也可能是一个子树,所以不一定平衡因子是0;所以这个不需要管;

下面是左单旋的代码实现:

//左单旋
void RotateL(node* parent)
{
	node* subR = parent->_right;
	node* subRL = subR->_left;
	//需要记录parent的父节点
	node* pparent = parent->_parent;//旋转后需要与parent上面的节点重连,所以需要先标记parent的父节点

	//更新各节点的指向
	if (subRL)subRL->_parent = parent;
	parent->_right = subRL;

	subR->_left = parent;
	parent->_parent = subR;

	if (pparent == nullptr)
	{
		_node = subR;
		_node->_parent = nullptr;
	}
	else
	{
		if (parent == pparent->_left)
		{
			pparent->_left = subR;
			subR->_parent = pparent;
		}
		else
		{
			pparent->_right = subR;
			subR->_parent = pparent;
		}
	}
	parent->_bf = subR->_bf = 0;
}
右左单旋

在有些情况三个关键节点并不在同一侧的子树上;可能是subR在parent的右子树上,而新节点在subR的左子树上;这中情况不可以仅仅使用右单旋完成旋转;需要先右旋,再左旋;

下面是旋转的情况图示:
e4692ee1198d48658a4ee2dc82cf341f.png

 这里的关键就是判断旋转的方式;我是根据位置判断的,8079efbbeced4d8190126b9aa2542e1e.png如果是这个样子,那就是右左旋,先对parent->_right(subR)右旋;在对subR左旋:
关键节点(parent,subR,subRL)变化过程:e493a966f2d4460a8661b66ff171a405.png

下面是代码实现:

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

	RotateR(subR);         
	RotateL(parent);    

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

下面的右单旋和左右单旋不在详细介绍了;直接给出代码实现;

右单旋
	//右单旋
	void RotateR(node* parent)   
	{
		node* subL = parent->_left;
		node* subLR = parent->_right;
		node* pparent = parent->_parent;

		//考虑两点:谁指向我,我指向谁
		parent->_left = subLR;
		if (subLR)subLR->_parent = parent;

		parent->_parent = subL;
		subL->_right = parent;

		//判断 祖父节点
		if (pparent == nullptr)
		{
			_node = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parent == pparent->_left)
			{
				pparent->_left = subL;
			}
			else
				pparent->_right = subL;
			subL->_parent = pparent;
		}

		parent->_bf = subL->_bf = 0;
	}
左右单旋
//左右单旋
void RotateLR(node* parent)
{
	node* subL = parent->_left;
	node* subLR = subL->_right;
	int bf = subLR->_bf;

	//先右再左
	RotateR(subL);
	RotateL(parent);

	if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
		assert(false);
}
void print()
{
	_print(_node);
	cout << endl;
}

到此为止,insert的实现就完成了;

AVL树的验证

要验证是否是AVL树,需要验证是否是二叉搜索树,还有平衡因子是否正确;

验证是否是二叉搜索树

这个比较简单,只需要使用一次中序遍历,打印出来如果是有序的那么就是二叉搜索树;

//打印
void _print(node* node)
{
	if (node == nullptr)return;

	_print(node->_left);
	cout << node->_data << " ";
	_print(node->_right);
}

验证平衡因子 

需要验证每个节点的平衡因子是否是等于右子树和左子树之差;以及平衡因子的绝对值是否小于2;

	bool is_balance()
	{
		return balance(_node);     
	}
private:
	bool balance(node* root)  
	{
		if (root == nullptr)return true;
		//分别求出左右子树的高度
		int left_height = _height(root->_left);   
		int right_height = _height(root->_right);
		if (right_height - left_height != root->_bf)
		{
			cout << "平衡因子异常" << endl;   
               return false;
		}
		return abs(right_height - left_height) < 2 && balance(root->_left) && balance(root->_right);           
	}

AVL树的删除

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。具体实现学生们可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版.

AVL树的性能 

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即log2(n)。但是如果要对AVL树做一些结构修改的操
作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,
有可能一直要让旋转持续到根的位置。因此: 如果需要一种查询高效且有序的数据结构,而且数
据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

AVL树实现完整代码

#pragma once

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<algorithm>
#include<vector>
#include<assert.h>   
using namespace std;

namespace bit
{
	template<class T>
	struct BTNode
	{
		struct BTNode<T>* _parent;
		struct BTNode<T>* _left;
		struct BTNode<T>* _right;
		T _data;
		int _bf = 0;//平衡因子  
		//构造函数
		BTNode(T data = T())
			:_parent(nullptr), _left(nullptr), _right(nullptr), _data(data)
		{}
	};


	template <class T>
	class AVLTree
	{
		typedef BTNode<T>   node;
	public:
		AVLTree()
			:_node(nullptr)
		{}
		AVLTree(vector<T>& arr)
		{
			for (auto x : arr)
				insert(x);
		}
		//拷贝构造
		AVLTree(AVLTree<T>& tree)
			:_node(nullptr)
		{
			_node = copy(tree->_node);
		}
		//插入数据(需要判断是否插入成功)
		bool  insert(const T& data)
		{
			if (_node == nullptr)
			{
				_node = new node(data);
				return true;
			}
			node* cur = _node;
			node* parent = _node->_parent;
			while (cur)
			{
				if (data < cur->_data)//如果数据小就走左边
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (data > cur->_data)
				{
					parent = cur;
					cur = cur->_right;
				}
				else//等于节点的值
				{
					return false; //插入失败
				}
			}
			//这个时候cur为空
			cur = new node(data);   //开辟节点
			cur->_parent = parent;
			//判断是parent的左边还是右边
			if (data < parent->_data)
			{
				parent->_left = cur;
			}
			else
				parent->_right = cur;

			//更新平衡因子
			while (parent)//直到更新到根节点
			{
				if (cur == parent->_left)
				{
					parent->_bf--;
				}
				else
					parent->_bf++;
				//判断是否需要调整
				if (parent->_bf == 0)//说明不需要调整
				{
					break;
				}
				else  if (parent->_bf == 1 || parent->_bf == -1)//说明左边高
				{
					cur = parent;
					parent = parent->_parent;
				}
				else
				{
					if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);
					else if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);
					else if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);
					else if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);
					else
						assert(false);   
					break;   
				}
			}
			return true;   
		}
		//左单旋
		void RotateL(node* parent)
		{
			node* subR = parent->_right;
			node* subRL = subR->_left;
			//需要记录parent的父节点
			node* pparent = parent->_parent;

			//更新各节点的指向
			if (subRL)subRL->_parent = parent;
			parent->_right = subRL;

			subR->_left = parent;
			parent->_parent = subR;

			if (pparent == nullptr)
			{
				_node = subR;
				_node->_parent = nullptr;
			}
			else
			{
				if (parent == pparent->_left)
				{
					pparent->_left = subR;
					subR->_parent = pparent;
				}
				else
				{
					pparent->_right = subR;
					subR->_parent = pparent;
				}
			}
			parent->_bf = subR->_bf = 0;
		}
		//右单旋
		void RotateR(node* parent)   
		{
			node* subL = parent->_left;
			node* subLR = parent->_right;
			node* pparent = parent->_parent;

			//考虑两点:谁指向我,我指向谁
			parent->_left = subLR;
			if (subLR)subLR->_parent = parent;

			parent->_parent = subL;
			subL->_right = parent;

			//判断 祖父节点
			if (pparent == nullptr)
			{
				_node = subL;
				subL->_parent = nullptr;
			}
			else
			{
				if (parent == pparent->_left)
				{
					pparent->_left = subL;
				}
				else
					pparent->_right = subL;
				subL->_parent = pparent;
			}

			parent->_bf = subL->_bf = 0;
		}
		//右左旋
		void RotateRL(node* parent)
		{
			node* subR = parent->_right;
			node* subRL = subR->_left;
			int bf = subRL->_bf;

			RotateR(subR);         
			RotateL(parent);    

			//三种情况更新平衡因子
			if (bf == 0)
			{
				parent->_bf = 0;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == 1)
			{
				parent->_bf = -1;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 0;
				subR->_bf = 1;
				subRL->_bf = 0;
			}
			else
				assert(false);
		}
		//左右单旋
		void RotateLR(node* parent)
		{
			node* subL = parent->_left;
			node* subLR = subL->_right;
			int bf = subLR->_bf;

			//先右再左
			RotateR(subL);
			RotateL(parent);

			if (bf == 0)
			{
				parent->_bf = 0;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			else if (bf == 1)
			{
				parent->_bf = 0;
				subL->_bf = -1;
				subLR->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 1;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			else
				assert(false);
		}
		void print()
		{
			_print(_node);
			cout << endl;
		}
		bool is_balance()
		{
			return balance(_node);     
		}
	private:
		bool balance(node* root)  
		{
			if (root == nullptr)return true;
			//分别求出左右子树的高度
			int left_height = _height(root->_left);   
			int right_height = _height(root->_right);
			if (right_height - left_height != root->_bf)
			{
				cout << "平衡因子异常" << endl;   
			}
			return abs(right_height - left_height) < 2 && balance(root->_left) && balance(root->_right);           
		}
		//求出二叉树的高度
		size_t _height(node* root)   
		{
			if (root == nullptr)return 0;
			int h1 = _height(root->_left);  
			int h2 = _height(root->_right);   
			return max(h1,h2)+1;            
		}
		//拷贝二叉树
		node* copy(const node* tree)
		{
			if (tree == nullptr)return nullptr;
			node* newnode = new node(tree->_data);
			newnode->_left = copy(tree->_left);
			newnode->_right = copy(tree->_right);
			return newnode;
		}
		//打印
		void _print(node* node)
		{
			if (node == nullptr)return;

			_print(node->_left);
			cout << node->_data << " ";
			_print(node->_right);
		}

		node* _node = nullptr;
	};
}

95816cc736af4c42a5560b8427fa03ab.jpeg

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值