【C++进阶篇】AVL树的实现(赋源码)

一. AVL树简介

1.1 基本概念

AVL树(Adelson-Velsky and Landis Tree)是计算机科学中最早发明的自平衡二叉搜索树(BST),由苏联数学家G. M. Adelson-Velsky和E. M. Landis于1962年提出。其核心特性是:

  • 平衡性:任意节点的左右子树高度差绝对值不超过1(即平衡因子为-1、0或1)。
  • 自平衡机制:通过树旋转(左旋、右旋、左右双旋、右左双旋)动态调整结构,确保插入、删除操作后树的高度保持对数级别(O(log n))。
  • 时间复杂度:所有操作(查找、插入、删除)的时间复杂度均稳定在O(log n),避免了普通BST在极端情况下退化为链表(时间复杂度O(n))的问题。

1.2 AVL树的意义

  1. 解决BST的退化问题

BST在数据有序插入时可能退化为链表,导致操作效率骤降。AVL树通过强制平衡,确保树的高度始终可控,从而维持高效的动态操作性能。
2. 性能稳定性

在频繁插入、删除的场景中,AVL树的严格平衡约束使其性能波动远小于其他自平衡树(如红黑树),尤其适合对响应时间敏感的实时系统。
3. 算法基础

AVL树是理解更复杂平衡树(如红黑树、B树)的基石,其旋转操作和平衡因子设计思想被广泛借鉴。

1.3 AVL树的应用场景

  1. 数据库索引
  • 作为索引结构,AVL树支持高效的查询、插入和删除操作,确保数据库在处理动态数据时保持快速响应。
  • 例如,MySQL的InnoDB引擎早期版本曾使用AVL树管理索引。
  1. 文件系统与内存管理
  • 文件系统通过AVL树管理目录和元数据,提升文件查找效率。
  • 操作系统用AVL树管理空闲内存块,实现快速分配与释放。
  1. 网络路由与实时系统
  • 路由表维护:AVL树快速更新路由信息,优化数据传输路径。
  • 事件调度:高效管理定时任务,确保事件按预定时间触发。
  1. 词典与拼写检查
  • 存储单词列表时,AVL树支持动态更新,适用于需要频繁增删词汇的场景。
  1. 符号表与动态集合
  • 实现集合、映射等数据结构,保证插入、删除、查找操作的高效性。

1.4 AVL树的前景

  1. 严格平衡需求的场景
    在需要绝对低延迟的金融交易系统、高频交易平台中,AVL树的稳定性能仍具优势。
  2. 与现代技术的结合
  • 内存优化:通过压缩节点存储或缓存友好设计,减少空间开销。
  • 并行计算:结合无锁数据结构,提升多线程环境下的并发性能。
  1. 教育与研究价值
    AVL树作为平衡树的经典案例,仍是算法教学和研究的重点,其设计思想持续启发新数据结构的创新。
  2. 替代方案的局限性
    尽管红黑树在插入/删除次数较少的场景中更高效,但AVL树在查询密集型任务中仍具竞争力。未来,随着硬件性能提升和算法优化,AVL树的应用场景有望进一步扩展。

1.5 总结:

AVL树通过严格的平衡约束和高效的旋转机制,为需要频繁动态更新的场景提供了可靠的性能保障。尽管存在实现复杂度和旋转开销的挑战,但其在大规模数据管理、实时系统及教育领域的应用潜力仍不可忽视。随着技术发展,AVL树或将在性能优化与场景适配中迎来新的发展机遇。

二. AVL树的实现

2.1 AVL树的结构

//AVL树节点结构
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;//平衡因子

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

注意:AVL树插入的是pair类型结构数据。

2.2 插入

插入的过程还是按照二搜索树的规则进行插入,插入之后进行平衡因子的更新,
更新规则:

  • 平衡因子=右子树高度-左子树高度
  • 只有子树高度变化才会影响当前节点的平衡因子
  • 插入节点后,会增加高度,新增节点在parent右子树,parent平衡因子++,新增在parent左子树,平衡因子–
  • parent所在子树的高度是否变化决定是否需要向上更新
  1. 插入节点后,如果parent平衡因子是0,说明是在parent的本来就是低的子树插入,不影响parent的父节点的平衡因子,直接跳出循环即可。下面以图来展示过程,更清楚点,毕竟有图才有原貌。
    在这里插入图片描述

通过上图可以看出当前增加13这个节点后父亲节点10的平衡因子由1变成0,10对应的父亲节点并不需要更新它为-1,平衡因子合理的取值为-1,0,1。

  1. 插入节点后,如果parent平衡因子是1或-1,说明是在parent之前平衡因子就是0,增加节点后子树高度变高或变低需要继续向上跟新,因为所在节点的节点的子树都发生变化。下面以图来展示过程,更清楚点,毕竟有图才有原貌。
    在这里插入图片描述

通过上图可以看出新增节点16后,parent的平衡因子由0变为1,parent的父节点(10)的平衡因子由0变为1,所以需要继续向上更新。
3. 插入节点后,如果parent平衡因子是2或-2,说明是在parent之前平衡因子就是1或-1,增加节点后在子树高度变高本来就高的子树继续增加高度,导致不平衡,需要旋转处理。
在这里插入图片描述
下面将详细使用特定的场景来讲述如何正确使用旋转,使子树高度平衡。
通过上述分析伪代码如下(缺少旋转处理代码):

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;

	//更新平衡因子
	while (parent)
	{
		if (parent->_left == cur)
		{
			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 || parent->_bf == -2)
		{
			//旋转处理
			break;
		}
		else
		{
			assert(false);
		}
	}

	return true;
}

2.2.1 右单旋场景

旋转原则:

  1. 保持搜索树的规则
  2. 让旋转的树从不满足变成平衡,其次降低树的高度。

场景图:
在这里插入图片描述

  • 过程:
  1. 用parent的左孩子指向subL,用subL的右孩子指向subLR
  2. 同时跟新三个节点的父亲,subL的父节点指向parent的父节点,subLR的父节点指向parent,parent的父节点指向subL,需额外注意subLR可能为空,指向之前判空。
  3. subL的右孩子指向parent,parent的左孩子指向subLR。
  4. 最后更新平衡因子即可。

伪代码如下:

	//右单旋
	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)
		if (parent == _root)//如果根节点就是parent,subL就是根节点,直接进行赋值即可
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}

		subL->_bf = 0;
		parent->_bf = 0;
	}

何时进行右单旋,当parent的平衡因子为-2且左孩子平衡因子为-1时,进行右单旋即可。

2.2.2 左单旋场景

场景图:
在这里插入图片描述

  • 过程:
  1. 用parent的右孩子指向subR,用subR的左孩子指向subRL
  2. 同时跟新三个节点的父亲,subR的父节点指向parent的父节点,subRL的父节点指向parent,parent的父节点指向subR,需额外注意subRL可能为空,指向之前进行判空即可。
  3. subR的左孩子指向parent,parent的右孩子指向subRL。
  4. 判断parent是否是根节点,如果是直接让subR成为新的根节点;否则将subR的父节点指向parent之前的父节点,如果parent是parent父节点的左孩子,则将parent父节点左孩子指向subR,否则右孩子指向subR。
  5. 最后更新平衡因子即可,跟新完后直接跳出循环即可,旋转后该树已经是平衡得了。

伪代码如下:

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	Node* ppNode = parent->_parent;

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

	if (parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent)
		{
			ppNode->_left = subR;
		}
		else
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}

	subR->_bf = 0;
	parent->_bf = 0;
}

何时进行左单旋,当parent的平衡因子为2且左孩子平衡因子为1时,进行左单旋即可。

2.2.3 左右双旋场景

场景图如下:

  • 场景1:
    在这里插入图片描述

新插入的节点为subLR的左孩子,parent,subL,subLR成折线型基本上以双旋进行解决该不平衡问题。现以parent的左孩子进行左单旋,然后再以parent节点进行右单旋即可,咱们直接调用接口即可,最后跟新平衡因子,以最初subLR的平衡因子进行判断更新,这步在代码中会体现出来。
在这里插入图片描述

再将上图以10节点进行右单旋即可。如下图:
在这里插入图片描述

  • 场景2:
    在这里插入图片描述

过程与上述一致,就是平衡因子的更新不同。

  • 场景3:
    在这里插入图片描述

旋转过程与上述一致,唯一不同是平衡因子的更新不同。

  • 伪代码如下:
//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

	RotateL(parent->_left);
	RotateR(parent);

	if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

何时进行左右双旋,当parent的平衡因子为-2且左孩子平衡因子为-1时,进行左右双旋即可。

2.2.4 右左双旋场景

场景图:

  • 场景1:
    在这里插入图片描述
    新插入的节点为subRL的左孩子,parent,subL,subLR成折线型基本上以双旋进行解决该不平衡问题。现以parent的右孩子进行右单旋,然后再以parent节点进行左单旋即可,咱们直接调用接口即可,最后跟新平衡因子,以最初subRL的平衡因子进行判断更新,这步在代码中会体现出来。

  • 以parent的右孩子(15这个节点)进行右单旋后的图解:
    在这里插入图片描述

  • 场景2:
    在这里插入图片描述
    过程与上述一致,就是平衡因子的更新不同。

  • 场景3:
    在这里插入图片描述
    过程与上述一致,就是平衡因子的更新不同。
    如何区别不同场景下平衡因子的准确跟新,通过最初subRL的平衡因子判断即可。

  • 伪代码如下:

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

	RotateR(parent->_right);
	RotateL(parent);

	if (bf == -1)
	{
		subRL->_bf = 0;
		subR->_bf = 1;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == 0)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);//其他的直接断言报错
	}
}

何时进行右左双旋,当parent的平衡因子为2且左孩子平衡因子为-1时,进行右左双旋即可。

2.3 查找

查找过程与二叉搜索树相同,不同的是返回值,因为它是key/value结构,需要通过key修改对应value的值,伪代码如下:

Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < key)
		{
			cur = cur->_right;
		}
		else if (cur->_kv.first > key)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}

	return nullptr;
}

2.4 AVL树平衡检测

递归检查每个树左右高度差同时判断平衡因子是否异常即可,递归该过程即可。

  • 求每个节点左右子树高度差
int _Height(Node* root)
{
	if (root == nullptr)
		return 0;
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

检查树是否平衡

bool _IsBalanceTree(Node* root)
{
	// 空树也是AVL树
	if (nullptr == root)
		return true;

	// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;

	// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
	// pRoot平衡因子的绝对值超过1,则一定不是AVL树
	if (abs(diff) >= 2)
	{
		cout << root->_kv.first << "高度差异常" << endl;
		return false;
	}

	if (root->_bf != diff)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}

	// pRoot的左和右如果都是AVL树,则该树一定是AVL树
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

对上述代码进行测试:

Test.cpp

#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;
#include"AVLTree.h"

// 测试代码
void TestAVLTree1()
{
	AVLTree<int, int> t;
	// 常规的测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		/*if (e == 14)
		{
			int x = 0;
		}*/

		t.Insert({ e, e });
		cout << "Insert:" << e << "->";
		cout << t.IsBalanceTree() << endl;
	}

	t.InOrder();
	cout << t.IsBalanceTree() << endl;
}

// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestAVLTree2()
{
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}

	size_t begin2 = clock();
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();

	cout << t.IsBalanceTree() << endl;

	cout << "Insert:" << end2 - begin2 << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;

	size_t begin1 = clock();
	// 确定在的值
	/*for (auto e : v)
	{
		t.Find(e);
	}*/
	// 随机值
	for (size_t i = 0; i < N; i++)
	{
		t.Find((rand() + i));
	}

	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}

int main()
{
	//TestAVLTree1();
	TestAVLTree2();
	return 0;
}

该代码主要用于验证AVL树实现的正确性和性能,既包含边界条件测试(双旋转场景),也包含大规模压力测试,能够全面评估AVL树实现的质量。

三. 最后

本文系统阐述了AVL树的原理与实现,涵盖其自平衡特性、应用场景及代码验证。AVL树通过严格的平衡因子(-1/0/1)约束和四类旋转操作(单旋/双旋)维持O(log n)时间复杂度,适用于数据库索引、实时系统等对性能稳定性要求高的场景。实现部分详细解析了节点结构、插入时的平衡因子更新策略及四种旋转场景,并提供了查找和递归平衡检测方法。测试代码通过边界用例和百万级数据压力测试,验证了实现的正确性与效率,展现了AVL树在动态数据管理中的可靠性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值