【C++高阶】精通AVL树:全面剖析与深度学习

Zfox_ 2024-07-22 14:35:05 阅读 58

目录

🚀 前言一: 🔥 AVL树的性质二: 🔥 AVL树节点的定义三: 🔥 AVL树的插入四: 🔥 AVL树的平衡调整(附动图) 五:🔥 AVL树的验证5.1 验证有序5.2 验证平衡

六: 🔥 AVL树的删除七: 🔥AVL树的性能和完整代码

🚀 前言

AVL树,又称为平衡二叉树,它基于二叉搜索树并通过平衡而得到。

在前面的学习中我们提到,二叉搜索树可以提高搜索数据的效率,但在数据有序的情况下会退化为单支树,此时在树中查找元素就得遍历一整个分支,时间复杂度也会退化至O(N)。

如果有一种算法,可以使二叉搜索树时刻保持左右子树的平衡,就可以避免这种最坏情况。

因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了AVL树,它不仅解决了二叉搜索树在数据插入和删除时可能产生的失衡问题,更通过旋转操作,使得树的高度始终保持在一个相对较低的水平,从而保证了搜索的高效性。

一: 🔥 AVL树的性质

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

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

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在

O

(

l

o

g

2

n

)

O(log_2 n)

O(log2​n),搜索时间复杂度 O(

l

o

g

2

n

log_2 n

log2​n)

二: 🔥 AVL树节点的定义

AVL树节点的定义通常包含以下几个关键部分:

1.基本元素

在调整失衡的AVL树时,我们需要频繁的访问父节点,所以在AVL树中我们需要使用三叉链,因此AVL树的节点除了包含左右子节点的指针,还需要一个指向父节点的指针。

_left:指向节点的左子节点的指针。 _right:指向节点的右子节点的指针。_parent:指向节点的父节点的指针。 _kv:一个结构体或配对(pair),包含节点的键值(key)和值(value)。这取决于AVL树的具体用途,可能只包含键或包含键值对。

2. 平衡因子(_bf)

一个整数,表示节点左子树和右子树的高度差。AVL树的性质要求任何节点的平衡因子的绝对值不超过1(-1, 0, 1)。 如果左子树比右子树高一层,那么平衡因子就为-1;如果左右子树一样高,平衡因子就为0;如果右子树比左子树高一层,那么平衡因子就为1,这三种情况下AVL树的性质都没有被打破。 按照这个规则,如果平衡因子为-2、2或其他值,则说明左右子树已经失衡,性质被打破。

另外需要说明一下,本文中,我们使用key / value模型的AVL树

AVL树节点的定义:

<code>template<class K,class V>

struct AVLTreeNode

{

AVLTreeNode<K, V>* _left;

AVLTreeNode<K, V>* _right;

AVLTreeNode<K, V>* _parent;

pair<K, V> _kv; //第一个数据存储key,第二个数据存储value

int _bf; //平衡因子(balance factor)

AVLTreeNode(const pair<const K, V>& kv)

:_left(nullptr)

,_right(nullptr)

,_parent(nullptr)

,_kv(kv)

,_bf(0) //新节点左右都为空,平衡因子为0

{ }

};

三: 🔥 AVL树的插入

🌈 AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

按照二叉搜索树的方式插入新节点树的平衡调整以及调整节点的平衡因子

新节点插入后,parent的平衡因子一定需要调整

我们先按照二叉搜索树的规则将节点插入到AVL树中,并判断插入的节点在父节点的左边还是右边

按照平衡因子的规则,如果新节点插入到了父节点的左侧,那么父节点的平衡因子-1

在这里插入图片描述

如果新节点插入到了父节点的右侧,那么父节点的平衡因子+1

在这里插入图片描述

以上,便是新增节点的父节点平衡因子可能的变化情况。

但是! 插入一个节点不但会影响父节点,还可能会影响到祖先节点。

我们观察上面的四种可能,其中左边的两种情况下,插入节点后以父节点为根的子树高度发生了变化;在右边的两种情况下,插入节点后以父节点为根的子树高度没有发生变化。

观察过后可以发现,当父节点的平衡因子从0变为1/-1后,子树高度发生变化;当父节点的平衡因子从1/-1变为0后,子树高度不发生变化

如果以父节点为根的子树高度没有发生变化,那么就不会影响到祖先节点的平衡因子;如果高度变了就会继续向上影响到祖先节点的平衡因子

因此,我们可以通过判断节点的插入位置来计算父节点的平衡因子,进而判断子树高度是否发生变化,再进一步计算对祖先节点平衡因子的影响,来判断AVL树是否失衡。

至此,我们已经可以开始写插入新节点和更新平衡因子的代码了:

<code>template<class K, class V>

class AVLTree

{

typedef AVLTreeNode<K, V> Node;

public:

bool insert(const pair<const K, V>& kv)

{

if (_root == nullptr) //检测为空树的情况

{

_root = new Node(kv);

return true;

}

Node* parent = nullptr;

Node* cur = _root;

while (cur) //搜索新节点的插入位置

{

parent = cur;

if (kv.first > cur->_kv.first)

cur = cur->_right;

else if (kv.first < cur->_kv.first)

cur = cur->_left;

else

return false;

}

cur = new Node(kv);

//将父节点与新节点链接

//比较新节点和父节点的key判断插入到左边还是右边

if (kv.first > parent->_kv.first)

{

parent->_right = cur;

cur->_parent = parent;

}

else

{

parent->_left = cur;

cur->_parent = parent;

}

while (cur != _root)

{

//插入节点后除了对父节点造成影响还可能对祖宗节点造成影响

//因此随着循环进行,这里的cur不一定为新节点,可以理解为高度发生变化的子树的根节点

//更新父节点的平衡因子

if (cur == parent->_left)

parent->_bf--;

else

parent->_bf++;

//更新后检测父节点的平衡因子

if (parent->_bf == 0) //平衡因子为0说明没有打破性质,跳出循环

break;

else if (parent->_bf == 1 || parent->_bf == -1) //更新后平衡因子为1或-1说明高度发生变化,改变cur和parent的指向后继续向上更新

{

cur = parent;

parent = parent->_parent;

}

else if (parent->_bf == 2 || parent->_bf == -2) //更新后平衡因子为2或-2.说明已经失衡,需要调整

{

//不同情况的调整方法...

if (parent->_bf == 2)

{

if (cur->_bf == 1)

{

//...

}

else if (cur->_bf == -1)

{

//...

}

}

else

{

if (cur->_bf == 1)

{

//...

}

else if (cur->_bf == -1)

{

//...

}

}

break;

}

else //平衡因子出现意外情况,报错

{

assert(false);

}

}

return true;

}

private:

Node* _root = nullptr;

};

通过上述分析我们可以知道:插入后,parent的平衡因子可能有三种情况:0,正负1, 正负2

如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功如果parent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此时以parent为根的树的高度增加,需要继续向上更新如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理接下来我们就重点讲解AVL树的旋转操作

四: 🔥 AVL树的平衡调整(附动图)

如果在一颗原本平衡的AVL树中插入一个新节点,可能会造成失衡,此时需要调整树的结构使之重新平衡,这种调整方法称为旋转。

根据树的原本结构和节点插入位置的不同分为四种情况和四种旋转方式:

(1)新节点插入较高左子树的左侧:右单旋

在这里插入图片描述

问题来了:如何判断插入的新节点的方位呢?

很简单,以上面的情况为例,插入新节点后60的平衡因子变成-2,说明左子树更高,而30的平衡因子变成-1,说明新节点插入到了30的左子树。后面左单旋以及双旋中都同理,我们使用平衡因子就可以判断新节点插入的位置

右单旋代码如下:

<code>void RotateRight(Node *parent) //parent为平衡因子发生失衡的节点

{

Node *subL = parent->_left; //subL为parent的左子节点

Node *subLR = subL->_right; //subLR为subL的右子节点

//parent,subL和subLR三个节点是旋转中唯三需要进行操作的三个节点

// 将parent与subLR节点进行链接

parent->_left = subLR;

if (subLR) //subLR可能为空

subLR->_parent = parent;

Node *parentParent = parent->_parent; //记录parent的父节点

if (parent != _root)

{

subL->_parent = parentParent; //将subL与parent的父节点链接

if (parent == parentParent->_left)

parentParent->_left = subL;

else

parentParent->_right = subL;

}

else //如果parent为根,旋转后subL成为新的根

{

_root = subL;

subL->_parent = nullptr;

}

//将subL与parent链接

subL->_right = parent;

parent->_parent = subL;

parent->_bf = subL->_bf = 0; //更新平衡因子

}

通过右单旋我们可以看到,原本的_parent、_left以及_right的平衡因子都变成了0

(2)新节点插入较高右子树的右侧:左单旋

在这里插入图片描述

因为左单旋的原理和右单旋是类似的,只要理解了右单旋,加上动图的配合,左单旋和后面的双旋都是很好理解的

左单旋代码如下:

<code>void RotateLeft(Node *parent)

{

Node *subR = parent->_right;

Node *subRL = subR->_left;

parent->_right = subRL;

if (subRL)

subRL->_parent = parent;

Node *parentParent = parent->_parent;

if (parent != _root)

{

subR->_parent = parentParent;

if (parent == parentParent->_left)

parentParent->_left = subR;

else

parentParent->_right = subR;

}

else

{

_root = subR;

subR->_parent = nullptr;

}

subR->_left = parent;

parent->_parent = subR;

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

}

同样:原本的_parent、_left以及_right的平衡因子都变成了0

(3)新节点插入较高左子树的右侧:先左单旋再右单旋(左右双旋)

这种情况又可以分为两种情况:

在这里插入图片描述

不过这两种情况都属于在较高左子树的右侧插入,处理方式都是相同的,唯一的区别在于最后旋转完成后,更新平衡因子时的值不同。

还存在一种情况就是30本身就是新插入的节点没有bc子树,这点会在代码里体现比较容易理解这里就不赘述了

接下来我们以上面的那个情况为例展示左右双旋的过程:

在这里插入图片描述

而下面的情况和上面的情况唯一的区别在于,最后更新的平衡因子不同

在这里插入图片描述

如何去决定每个节点更新后的平衡因子呢?可以看到这两种情况中,如果在b下面插入新节点,那么旋转过后30和60的平衡因子更新成0,90的平衡因子更新成1;如果在c下面插入新节点,则是60和90的平衡因子更新成0,30的平衡因子更新成-1

关键一点:而新节点究竟插入到了b下面还是在c下面,我们可以通过插入节点后60的平衡因子来判断

在这里插入图片描述

左右双旋代码如下:

<code>void RotateLR(Node *parent)

{

Node *subL = parent->_left;

Node *subLR = subL->_right;

int bf = subLR->_bf; //记录插入节点后subLR的平衡因子

RotateLeft(subL); //先左单旋

RotateRight(parent); //再右单旋

//更新平衡因子

//通过前面记录的平衡因子判断更新的情况

if (bf == 0)

{

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

}

else if (bf == 1)

{

subL->_bf = -1;

parent->_bf = subLR->_bf = 0;

}

else if (bf == -1)

{

parent->_bf = 1;

subL->_bf = subLR->_bf = 0;

}

else

{

assert(false);

}

}

(4)新节点插入较高右子树的左侧:先右单旋再左单旋(右左双旋)

这种情况和左右双旋的情况原理一样,我们直接上动图和代码

右左双旋的代码如下:

在这里插入图片描述

<code>void RotateRL(Node *parent)

{

Node *subR = parent->_right;

Node *subRL = subR->_left;

int bf = subRL->_bf;

RotateRight(subR);

RotateLeft(parent);

if (bf == 0)

{

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

}

else if (bf == 1)

{

parent->_bf = -1;

subR->_bf = subRL->_bf = 0;

}

else if (bf == -1)

{

subR->_bf = 1;

parent->_bf = subRL->_bf = 0;

}

else

{

assert(false);

}

}

现在四个旋转的函数都实现了,完整的插入函数代码如下:

bool Insert(const pair<K, V>& kv)

{

if (_root == nullptr) {

_root = new Node(kv);

return true;

}

Node* cur = _root, *parent = nullptr;

while (cur)

{

if (cur->_kv.first == kv.first) return false;

else if (cur->_kv.first < kv.first) {

parent = cur;

cur = cur->_right;

}

else {

parent = cur;

cur = cur->_left;

}

}

cur = new Node(kv);

if (kv.first > parent->_kv.first) parent->_right = cur;

else parent->_left = cur;

cur->_parent = 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 = 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) // 先右旋再左旋 (右边高,孩子左边高, 父子异号)

{

RotateRL(parent);

}

else

{

RotateLR(parent); // 先左旋再右旋 (左边高,孩子右边高, 父子异号)

}

break;

}

else {

assert(false);

}

}

return true;

}

总结:

假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑

parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR

当subR的平衡因子为1时,执行左单旋当subR的平衡因子为-1时,执行右左双旋

parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL

当subL的平衡因子为-1是,执行右单旋当subL的平衡因子为1时,执行左右双旋

五:🔥 AVL树的验证

5.1 验证有序

最重要的插入节点部分完成了,不过在验证是否符合AVL树性质前,我们首先需要验证其是否是一棵二叉搜索树

在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树

中序遍历代码如下:

void InOrder()

{

_InOrder(_root);

cout << endl;

}

void _InOrder(Node *root)

{

if (root == nullptr)

return;

_InOrder(root->_left);

cout << root->_kv.first << " "; // key/value模型,我们只打印key即可

_InOrder(root->_right);

}

5.2 验证平衡

要验证是否符合AVL树性质,只需要检测它的所有节点的子树高度差不超过1即可

需要注意的是,这里不可以直接通过判断平衡因子的绝对值是否大于1来验证平衡,因为平衡因子是不客观的,可以被修改

因此,我们通过递归来得到每棵子树的高度并进行判断即可

bool IsBalance()

{

return _IsBalance(_root);

}

bool _IsBalance(Node *root)

{

if (root == nullptr)

return true;

int leftHeigit = _Height(root->_left);

int rightHeight = _Height(root->_right);

if (rightHeight - leftHeigit != root->_bf)

{

cout << root->_kv.first << "平衡因子异常" << endl;

return false;

}

return abs(rightHeight - leftHeigit) <= 1

&& _IsBalance(root->_left)

&& _IsBalance(root->_right);

}

int Height()

{

return _Height(_root);

}

int _Height(Node *root)

{

if (root == nullptr)

return 0;

int higher = max(_Height(root->_left), _Height(root->_right));

return higher + 1;

}

六: 🔥 AVL树的删除

AVL树的删除并不是本文的重点,因为其原理我们在前面已经学习过了

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置

七: 🔥AVL树的性能和完整代码

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

l

o

g

2

(

N

)

log_2 (N)

log2​(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

相对于AVL树的严格平衡,红黑树则追求一种相对平衡,因此会略胜一筹,后面的文章中会对红黑树进行讲解。

AVL树的完整代码如下:

template<class K,class V>

struct AVLTreeNode

{

AVLTreeNode<K, V>* _left;

AVLTreeNode<K, V>* _right;

AVLTreeNode<K, V>* _parent;

pair<K, V> _kv;

int _bf; //平衡因子

AVLTreeNode(const pair<const K, V>& kv)

:_left(nullptr)

,_right(nullptr)

,_parent(nullptr)

,_kv(kv)

,_bf(0)

{ }

};

template<class K, class V>

class AVLTree

{

typedef AVLTreeNode<K, V> Node;

public:

bool insert(const pair<const K, V>& kv)

{

if (_root == nullptr)

{

_root = new Node(kv);

return true;

}

Node* parent = nullptr;

Node* cur = _root;

while (cur)

{

parent = cur;

if (kv.first > cur->_kv.first)

cur = cur->_right;

else if (kv.first < cur->_kv.first)

cur = cur->_left;

else

return false;

}

cur = new Node(kv);

if (kv.first > parent->_kv.first)

{

parent->_right = cur;

cur->_parent = parent;

}

else

{

parent->_left = cur;

cur->_parent = parent;

}

while (cur != _root)

{

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 || parent->_bf == -2)//平衡异常

{

if (parent->_bf == 2)

{

if (cur->_bf == 1)

{

RotateLeft(parent);

}

else if (cur->_bf == -1)

{

RotateRL(parent);

}

}

else

{

if (cur->_bf == 1)

{

RotateLR(parent);

}

else if (cur->_bf == -1)

{

RotateRight(parent);

}

}

break;

}

else

{

assert(false);

}

}

return true;

}

void RotateLeft(Node* parent) //新节点插入较高右子树的右侧:左单旋

{

Node* subR = parent->_right;

Node* subRL = subR->_left;

parent->_right = subRL;

if(subRL)

subRL->_parent = parent;

Node* parentParent = parent->_parent;

if (parent != _root)

{

subR->_parent = parentParent;

if (parent == parentParent->_left)

parentParent->_left = subR;

else

parentParent->_right = subR;

}

else

{

_root = subR;

subR->_parent = nullptr;

}

subR->_left = parent;

parent->_parent = subR;

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

}

void RotateRight(Node* parent) //新节点插入较高左子树的左侧:右单旋

{

Node* subL = parent->_left;

Node* subLR = subL->_right;

parent->_left = subLR;

if (subLR)

subLR->_parent = parent;

Node* parentParent = parent->_parent;

if (parent != _root)

{

subL->_parent = parentParent;

if (parent == parentParent->_left)

parentParent->_left = subL;

else

parentParent->_right = subL;

}

else

{

_root = subL;

subL->_parent = nullptr;

}

subL->_right = parent;

parent->_parent = subL;

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

}

void RotateRL(Node* parent)

{

Node* subR = parent->_right;

Node* subRL = subR->_left;

int bf = subRL->_bf;

RotateRight(subR);

RotateLeft(parent);

if (bf == 0)

{

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

}

else if (bf == 1)

{

parent->_bf = -1;

subR->_bf = subRL->_bf = 0;

}

else if (bf == -1)

{

subR->_bf = 1;

parent->_bf = subRL->_bf = 0;

}

else

{

assert(false);

}

}

void RotateLR(Node* parent)

{

Node* subL = parent->_left;

Node* subLR = subL->_right;

int bf = subLR->_bf;

RotateLeft(subL);

RotateRight(parent);

if (bf == 0)

{

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

}

else if (bf == 1)

{

subL->_bf = -1;

parent->_bf = subLR->_bf = 0;

}

else if (bf == -1)

{

parent->_bf = 1;

subL->_bf = subLR->_bf = 0;

}

else

{

assert(false);

}

}

void InOrder()

{

_InOrder(_root);

cout << endl;

}

bool IsBalance()

{

return _IsBalance(_root);

}

int Height()

{

return _Height(_root);

}

size_t Size()

{

return _Size(_root);

}

Node* Find(const K& key)

{

Node* cur = _root;

while (cur)

{

if (key > cur->_kv.first)

cur = cur->_right;

else if (key < cur->_kv.first)

cur = cur->_left;

else

return cur;

}

return nullptr;

}

private:

void _InOrder(Node* root)

{

if (root == nullptr)

return;

_InOrder(root->_left);

cout << root->_kv.first << " ";

_InOrder(root->_right);

}

bool _IsBalance(Node* root)

{

if (root == nullptr)

return true;

int leftHeigit = _Height(root->_left);

int rightHeight = _Height(root->_right);

if (rightHeight - leftHeigit != root->_bf)

{

cout << root->_kv.first << "平衡因子异常" << endl;

return false;

}

return abs(rightHeight - leftHeigit) <= 1

&& _IsBalance(root->_left)

&& _IsBalance(root->_right);

}

int _Height(Node* root)

{

if (root == nullptr)

return 0;

int higher = max(_Height(root->_left), _Height(root->_right));

return higher + 1;

}

size_t _Size(Node* root)

{

if (root == nullptr)

return 0;

return _Size(root->_left) + _Size(root->_right) + 1;

}

private:

Node* _root = nullptr;

};

以上就是AVL搜索树的图解与完整实现过程,欢迎在评论区留言,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。