AVLTree手撕AVL树


title: 手撕AVL树
date: 2024-08-12 10:55:47
tags:
cover: https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/202409230910780.png


原文及更多原创文章见我的网站: www.supdriver.top

AVL树的概念

二叉搜索树的不足

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化单支树,查找元素相当于在顺序表中搜索元素,效率低下。

AVL树的提出

两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即构建一颗绝对的平衡搜索二叉树,即可降低树的高度,从而减少平均搜索长度。

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

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

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

封装AVL树

头文件

#include <utility>
#include <cassert>

封装AVL树的节点

  • 这里采用键值(KV)类型的二叉树节点,使其泛用性更高
  • 并增加指向父节点的指针构造三叉链表,来简化对二叉树的调整,但代价是维护成本变高(多了一个指针要维护)。
  • 引入平衡因子:
    • -1表示左子树比右子树高1左>右
    • 0表示左右子树等高 左=右
    • 1表示右子树比左子树高1左<右
#include <utility>
#include <iostream>
#include <cassert>

template <class K,class V>
struct AVLTreeNode
{
	typedef AVLTreeNode<K, V> Node;//使用typedef简化代码
	Node* _left;                   
	Node* _right;
	Node* _parent;
	std::pair<K, V> _kv; //节点储存的键值对
	int _bf;// ballance factor 平衡因子

	//构造函数,但不提供无参的默认构造函数
	AVLTreeNode(const std::pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

规定AVLTree类的框架

首先确定成员变量,这里仅用_root指向二叉树,具体的维护由成员函数完成

代码简化方面,使用

typedef AVLTreeNode<K, V> Node;
typedef std::pair<K,V> PKV;

简化代码

然后是准备实现的成员函数

公共接口

  • Insert插入节点
  • Inorder前序遍历打印节点,用于debug
  • IsBalance检测是否平衡
  • Height获取子树高度

私有接口

  • RotateL向左旋转子树,用于维护平衡
  • RotateR向右旋转子树,用于维护平衡
  • RotateLR向左再向右旋转子树,用于维护平衡
  • RotateRL向右再向左旋转子树,用于维护平衡

实现AVL树的核心就是实现这四个旋转操作,并再在insert中回调。所以Insert函数我们放到最后再实现

默认构造函数

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
	typedef std::pair<K, V> PKV;
public:
	AVLTree() :_root(nullptr) {}//默认构造函数

private:
	Node* _root;
};

Inorder函数

公共的Inorder接口为无参函数,内部调用另外定义的_Inorder子函数

public:
	AVLTree() :_root(nullptr) {}//默认构造函数

	void Inorder()
	{
		_Inorder(_root);
		std::cout<<"nullptr" << std::endl;
	}

protected:
	void _Inorder(Node* root)
	{
		if (root == nullptr) return;
		_Inorder(root->_left);
		std::cout << root->_kv.second << "->";
		_Inorder(root->_right);
	}

Height函数

同上,使用_Height子函数

public:
	int Height()
	{
		return _Height(_root);
	}
private:
	int _Height(Node* root)
	{
		if (root == nullptr) return 0;

		int LHeight = _Height(root->_left);
		int RHeight = _Height(root->_right);
		if (LHeight > RHeight) return LHeight + 1;
		else return RHeight + 1; //将左右子树相等的情况合并在这里
	}

IsBalance函数

同上,使用_Balance子函数

这里要较为严格,可靠地地判定平衡,而平衡因子由我们自己维护,作为判定的依据,并不可靠,相比之下,计算左右子树的高度差更可靠,所以这里回调_Height函数

public:
	bool IsBalance()
	{
		return _IsBalance(_root);
	}
private:
	bool _IsBalance(Node* root)
	{
		if (root == nullptr) return true;
		bool left = _IsBalance(root->_left);
		bool right = _IsBalance(root-> - _right);

		if (left == false || right == false) return false;//先判定左右子树

		int LHeight = _Height(root->_left);
		int RHeight = _Height(root->_right);

		if (LHeight - RHeight >= 2 || LHeight - RHeight <= -2)return false;//最后判定根
		else return true;
	}

Insert函数

Insert函数主要按步骤实现以下功能

  1. 插入节点:先按照二叉搜索树的规则将节点插入到AVL树中
  2. 维护平衡:新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树

插入节点

具体插入方式与平衡搜索二叉树基本相同,但是还要额外维护_parent指针

	bool Insert(const PKV& kv)
	{
		Cmp cmp;//仿函数实例化一个对象
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		//寻找插入位置
		while (cur)
		{
			if (cur->_kv.first == kv.first)
			{
				//该键下的节点已存在,发生冲突
				return false;
			}
			else if (cmp(cur->_kv.first, kv.first))//根比节点"大"
			{
				parent = cur;
				cur = cur->_left;
			}
			else //根比节点小
			{
				parent = cur;
				cur = cur->_right;
			}
		}
		
		//开始插入
		Node* node = new Node(kv);

		if (cmp(parent->_kv.first, kv.first))//应该插入左边
		{
			parent->_left = node;
			node->_parent = parent;
			parent->_bf -= 1;//_bf越小,左子树越高
		}
		else //插入右子树
		{
			parent->_right = node;
			node->_parent = parent;
			parent->_bf += 1;//_bf越大,右子树越高
		}
		//插入完成,准备开始维护平衡
		//.....
		
	}

维护平衡

插入节点的所有父级子树的高度都有可能受到其影响,所以要一路向上递归维护,直至某棵子树的高度不变,或者到达根节点

下面我们一一枚举所有情况,并逐一解决

  • 父节点_bf == 0
    • 右子树高度增加: _bf = 1,且父节点高度增加,需继续向上调整
    • 左子树高度增加: _bf = -1,且父节点高度增加,需继续向上调整
  • 父节点_bf == -1
    • 右子树高度增加: _bf = 0,父节点高度不变,停止调整
    • 左子树高度增加: _bf = -2,平衡被打破,需要旋转调整, 下文详细讨论
  • 父节点_bf == 1
    • 右子树高度增加: _bf = 2,平衡被打破,需要旋转调整, 下文详细讨论
    • 左子树高度增加: _bf = 0,父节点高度不变,停止调整

可以看到,需要调整调整的情况有两大方向,接下来详细介绍调整方法

RotateR :_bf == -2且子树的左子树超高

根据约定,_bf == -2表示左子树比右子树高2,所以我们要先通过向右旋转操作减少左子树高度

如图所示,通过向右旋转,成功地降低了左子树的高度,而根据左子树的平衡因子,又可以分为以下两种情况

  • -1:旋转后平衡性得到维护,但整棵树的高度由h+2降低到了h-1,总高度-1,与原本的高度+1相抵消,停止调整
  • 0:非法情况,不可能左右子树同时超高
  • 1:不存在的情况,因为破坏了左子树超高的前提

以上分析可知,一次向右旋转已经可以解决两种情况了,所以我们着手实现RotateR

如上图所示,我们要对树里的父子关系作出调整,以此改变高度。

然后除了图中的内容,我们还要维护一系列_bf,指针之类的内容

private:
	void RotateR(Node* parent)
	{
		if (parent == nullptr) return;
		Node* child = parent->_left;

		//维护树的结构
		parent->_left = child->_right;//过继子树的右子树
		child->_right = parent;//重构树结构
		Node* grandParent = parent->_parent;//维护祖父节点
		child->_parent = grandParent;//维护child
		parent->_parent = child;
		if (grandParent)//如果不是根节点
		{
			//维护根节点的父节点的子树
			if (parent == grandParent->_left) grandParent->_left = child;
			else grandParent->_right = child;
		}
		
		//维护平衡因子
		if (child->_bf == -1)
		{
			child->_bf = 0;
			parent->_bf = 0;
		}
		else if (child->_bf == 0)
		{
			assert(false);//不存在的情况
		}
		else
		{
			assert(false);//需要多次旋转时不应该调用该函数
		}

		if (parent == _root)
		{
			_root = child;//更新根节点
		}
	}
RotateL:_bf == 2且子树的右子树超高

根据约定,_bf == -2表示右子树比左子树高2,所以我们要先通过向左旋转操作减少右子树高度

如图所示,通过向左旋转,成功地降低了右子树的高度,而根据右子树的平衡因子,又可以分为以下三种情况

  • -1:旋转后平衡性得到维护,但整棵树的高度由h+2降低到了h-1,总高度-1,需继续向上调整
  • 0:非法情况,不可能左右子树同时超高
  • 1:不存在的情况,因为破坏了子树的右子树超高的前提

以上分析可知,一次向左旋转已经可以解决两种情况了,所以我们先实现RotateL
RotateL一样,我们要对树里的父子关系作出调整,以此改变高度。

然后除了图中的内容,我们还要维护一系列_bf,指针之类的内容

private:
	void RotateL(Node* parent)
	{
		if (parent == nullptr) return;
		Node* child = parent->_right;
		//维护树结构
		parent->_right = child->_left;//过继子树
		child->_left = parent;//重构树结构
		Node* grandParent = parent->_parent;//维护祖父节点
		child->_parent = grandParent;//维护_parent
		parent->_parent = child;//维护_parent
		if (grandParent)//如果不是根节点
		{
			//维护祖父节点的子树
			if (parent == grandParent->_left) grandParent->_left = child;
			else grandParent->_right = child;
		}

		//维护平衡因子
		if (child->_bf == 1)
		{
			child->_bf = 0;
			parent->_bf = 0;
		}
		else if (child->_bf == 0)
		{
			assert(false);//不存在的情况
		}
		else
		{
			assert(false);//需要多次旋转时不应该调用该函数
		}
		if (parent == _root)
		{
			_root = child;//更新根节点
		}
	}
RotateRL:_bf == -2且子树的右子树超高

当出现该情况时,单纯的向右旋转并不顶用,效果如下图

所以说我们要先调整左子树的平衡因子(左子树向左旋转),再最后进行向右旋转

如上图,通过两次旋转,完成了整体的平衡因子的维护

高度变化:整棵树的高度由h+3变成了h+2,高度减少了1,与原本的高度+1向抵消,停止调整

private:
	void RotateLR(Node* parent)
	{
		if (parent == nullptr) return;//错误情况
		Node* Lchild = parent->_left;
		if (Lchild == nullptr) return;//错误情况
		Node* Rchild = Lchild->_right;

		//先旋转左子树

		//维护树结构
		Lchild->_right = Rchild->_left;
		Rchild->_left = Lchild;
		Lchild->_parent = Rchild;
		parent->_left = Rchild;
		Rchild->_parent = parent;

		//维护_bf
		if (Lchild->_bf == 1)
		{
			Lchild->_bf = -1;
		}
		else
		{
			//_bf == -1 或 0都是非法的
			assert(false);
		}

		//再向右旋转
		Node* child = parent->_left;
		
		//维护树结构
		parent->_left = child->_right;
		child->_right = parent;

		Node* grandParent = parent->_parent;
		child->_parent = grandParent;
		parent->_parent = child;

		if (grandParent)//不是根节点
		{
			if (parent == grandParent->_left)grandParent->_left = child;
			else grandParent->_right = child;
		}

		//维护_bf
		parent->_bf = -1;
		child->_bf = 0;
		
		if (parent == _root)
		{
			_root = child;//更新根节点
		}
	}
RotateLR:bf == 2且子树的左子树超高

具体情况与上一个接口相同,一次旋转无法完成任务,需要先向右旋转右子树调节平衡因子,再向左旋转,完成平衡的维护

高度变化:整棵树的高度由h+3变成了h+2,高度减少了1

private:
	void RotateRL(Node* parent)
	{
		//先向右旋转子树
		Node* Rchild = parent->_right;
		Node* Lchild = Rchild->_left;

		//维护树结构
		Rchild->_left = Lchild->_right;
		Lchild->_right = Rchild;
		Rchild->_parent = Lchild;
		Lchild->_parent = parent;
		parent->_right = Lchild;

		//维护_bf
		if (Rchild->_bf == -1)
		{
			Rchild->_bf = 0;
		}
		else
		{
			assert(false);//非法的情况
		}

		//再向左旋转
		Node* child = parent->_right;

		parent->_right = child->_left;//过继左子树
		child->_left = parent;
		parent->_parent = child;
		Node* grandParent = parent->_parent;
		child->_parent = grandParent;
		
		if (grandParent)//如果不是根节点
		{
			if (parent == grandParent->_left)grandParent->_left = child;
			else grandParent->_right = child;
		}

		//维护_bf
		parent->_bf = -1;
		child->_bf = 0;

		if (parent == _root)
		{
			_root = child;//更新根节点
		}
	}
完成Insert中调整二叉树的代码
public:
	bool Insert(const PKV& kv)
	{
		Cmp cmp;//仿函数实例化一个对象
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		//寻找插入位置
		while (cur)
		{
			if (cur->_kv.first == kv.first)
			{
				//该键下的节点已存在,发生冲突
				return false;
			}
			else if (cmp(cur->_kv.first, kv.first))//根比节点"大"
			{
				parent = cur;
				cur = cur->_left;
			}
			else //根比节点小
			{
				parent = cur;
				cur = cur->_right;
			}
		}

		//开始插入
		Node* node = new Node(kv);
		if (cmp(parent->_kv.first, kv.first))//应该插入左边
		{
			parent->_left = node;
			node->_parent = parent;
			parent->_bf -= 1;//_bf越小,左子树越高
		}
		else //插入右子树
		{
			parent->_right = node;
			node->_parent = parent;
			parent->_bf += 1;//_bf越大,右子树越高
		}
		//插入完成,准备开始维护平衡
		while (true)
		{
			if (parent->_bf == 0)break;
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				//子树高度增加
				node = parent;
				parent = parent->_parent;
				if (parent == nullptr)break;//走到根节点了
				if (node == parent->_left)
				{
					parent->_bf -= 1;
				}
				else if (node == parent->_right)
				{
					parent->_bf += 1;
				}
				else
				{
					assert(false);//非法情况
				}
			}
			else if (parent->_bf == -2)
			{
				if (parent->_left->_bf == 0)
				{
					assert(false);//非法情况,不可能左右子树同时超高
				}
				else if (parent->_left->_bf == -1)
				{
					//子树的左子树超高
					RotateR(parent);
					break;
				}
				else if (parent->_left->_bf == 1)
				{
					//子树的右子树超高
					RotateLR(parent);
				}
			}
			else if (parent->_bf == 2)
			{
				if (parent->_right->_bf == 0)
				{
					assert(false);//非法情况,不可能左右子树同时超高
				}
				else if (parent->_right->_bf == 1)
				{
					//子树的右子树超高
					RotateL(parent);
					break;
				}
				else if (parent->_right->_bf == -1)
				{
					//子树的左子树超高
					RotateRL(parent);
					break;
				}
			}
			else
			{
				assert(false);//非法情况
			}
		}
		return true;
	}

测试

我们来写个简单的main函数测试一下我们封装的AVL树

main.cpp

#include "AVLTree.hpp"
#include <iostream>
using namespace std;
int main()
{
	AVLTree<int,int> avt;//实例化一个对象
	for (int i = 0; i < 100; ++i)//准备100个数据
	{
		avt.Insert({ i,i });//插入数据
	}
	cout <<"Height " << avt.Height() << endl;   //检查高度
	cout << "IsBalance " << avt.IsBalance() << endl; //检测平衡
	avt.Inorder();//检测搜索树的特性
	return 0;
}

我们来看看结果

可以看到,100个数据下,而且还是按顺序插入,AVL树的高度只有7,很好得提高了搜索效率

AVL树的性能

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

改造成 红黑树

目前的AVL树只是实现了最基本的功能,增删查改只支持了,剩下的内容可自行完善,或者跟随作者的脚步,将AVL树改成红黑树,增加其修改的性能,并进一步完善增删查改的功能,以及封装迭代器等,最后将红黑树封装成我们自己的set类和map

戳我前往红黑树篇🔗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值