当前位置: 首页 > news >正文

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

解密AVL树:让二叉搜索树永远保持平衡

  • 一. AVL树简介
    • 1.1 基本概念
    • 1.2 AVL树的意义
    • 1.3 AVL树的应用场景
    • 1.4 AVL树的前景
    • 1.5 总结:
  • 二. AVL树的实现
    • 2.1 AVL树的结构
    • 2.2 插入
      • 2.2.1 右单旋场景
      • 2.2.2 左单旋场景
      • 2.2.3 左右双旋场景
      • 2.2.4 右左双旋场景
    • 2.3 查找
    • 2.4 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树在动态数据管理中的可靠性。

相关文章:

  • 元注解(Meta-Annotations)详解
  • 双条件拆分工作表,一键生成独立工作簿-Excel易用宝
  • Python将Excel单元格某一范围生成—截图(进阶版—带样式+批量+多级表头)
  • reserve学习笔记(花指令)
  • W3电力线载波通信技术
  • 项目删除了,为什么vscode中的git还是存在未提交记录,应该怎么删除掉
  • 系统安全应用
  • 系统安全及应用深度笔记
  • Android13 以太网(YT8531)
  • MetaERP:开启企业数字化管理新时代
  • 2025年渗透测试面试题总结-各厂商二面试题01(题目+回答)
  • 智能呼叫中心系统的功能
  • 设计模式-面试题
  • 用Caffeine和自定义注解+AOP优雅实现本地防抖接口限流
  • 基于RT-Thread的STM32F4开发第五讲——软件模拟I2C
  • spring boot 注解 @bean
  • 解决 uv run 时 ModuleNotFoundError: No module named ‘anthropic‘ 的完整指南
  • HTTPS、SSL证书是啥?网站“安全小锁”的入门科普
  • megatron——EP并行
  • 【idea 报错:java: 非法字符: ‘\ufeff‘】
  • 国家统计局:4月社会消费品零售总额同比增长5.1%
  • 宫崎骏的折返点
  • 经济日报金观平:促进信贷资金畅达小微企业
  • 全国多家健身房女性月卡延长,补足因月经期耽误的健身时间
  • 特朗普政府涉税改法案遭众议院预算委员会否决
  • 刘小涛任江苏省委副书记