二叉搜索树虽然可以提高查找数据的效率,但是若插入二叉搜索树的数据有序或者接近有序,那么二叉搜索树将会退化为单支结构,因此查找数据的效率降低。这种特殊场景下二叉搜索树不能高效率查询数据。
因此,1962年两位俄罗斯的科学家 G. M. Adelson-Velsky 和 E. M. Landis 在论文中发表了一种解决上诉问题的数据结构 - AVL树:其解决方法是当向二叉搜索树中插入新增节点后,若能够保证树中任意节点的左右子树的高度差绝对值不超过1,即可降低树的高度,减少平均搜索长度。
AVL树可以是一棵空树或者是具有以下性质的二叉搜索树:
AVL树若有N个节点,它的高度可以保持在O(logN) , 搜索的时间复杂度是O(logN) 。
这里我们实现为KV结构,为便于后序的插入和调整节点,我们将AVL树中节点定义为三叉链结构。
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // 平衡因子 = 右子树高度 - 左子树高度
pair<K, V> _kv; // 存储KV的键值对
AVLTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0) // 新增的节点平衡因子为0
{}
};
AVL树在二叉搜索树的基础上引入了平衡因子,因此AVL树是在二叉搜索树的基础上调整变化而来的,因此AVL树的插入主要分为以下两个步骤:
一个节点的平衡因子是否需要更新,取决于插入新节点之后它的左右子树的高度是否发生了变化,即该新插入节点的祖先节点的平衡因子可能需要更新。
新增节点之后的平衡因子更新规则如下(parent 为新增节点的父节点):
此时 parent 的平衡因子有三种情况:0、+1、-1、+2、-2
每更新一次平衡因子,则进行以下判断,决定是否需要继续更新:
在更新过程中平衡因子违反了规则,出现了平衡因子为 -2 或 2 的节点,这时我们需要对违反规则的节点进行旋转处理,AVL树的旋转分为以下4种情况:
新增节点为 cur , 其父节点为 parent , cur 节点插入后其平衡因子为0,我们首先更新其父节点的平衡因子,更新完成之后,我们执行以下代码逻辑继续向上更新:
cur = parent;
parent = parent->_parent;
AVL树的节点插入:
pair<Node*, bool> insert(const pair<K, V>& kv)
{
// 空树,插入的第一个节点为根节点
if (_root == nullptr)
{
_root = new Node(kv);
return make_pair(_root, true);
}
// 按照二叉搜索树的规则,找到新插入节点的对应位置
Node* cur = _root;
Node* parent = _root;
while (cur)
{
if (cur->_kv.first > kv.first) // 需要插入的值比当前节点小,到当前节点左子树查找
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first) // 需要插入的值比当前节点大,到当前节点右子树查找
{
parent = cur;
cur = cur->_right;
}
else // 需要插入的值等于当前节点的值,表示树中已有相同的值,则插入失败
{
return make_pair(cur, false);
}
}
// 走到这里,就已经找到了新节点插入的对应位置
cur = new Node(kv);
Node* newnode = cur;
// 判断当前节点插入parent的左边还是右边
if (parent->_kv.first < kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
// 插入完成,更新平衡因子
while (cur != _root) // 最坏情况下可能更新到根节点
{
if (parent->_left == cur) //插入在parent左边,平衡因子-1
parent->_bf--;
else //插入在parent右边,平衡因子+1
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)
{
RotateR(parent); //右旋
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent); //左右双旋
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent); //左旋
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent); //右左双旋
}
break;
}
else
{
// 插入节点之前AVL树就已经不平衡了
assert(false);
}
}
return make_pair(newnode, true);
}
以下的旋转情况我们使用具象图来表示,用长方形块代表各种复杂子树结构,长方形块里面的子树已经平衡,因此我们不需要在对其进行处理。
右单旋的旋转过程:
旋转过程中的注意事项:
右单旋代码:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
// 将subLR链接到parent的左边,这里注意subLR可能为空的情况,需要判断一下
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
// 将parent这棵子树链接到subL的右指针
subL->_right = parent;
parent->_parent = subL;
// 若parent为根,则更新新的根节点
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else //若parent为一棵子树,则链接与parentParent的关系
{
if (parentParent->_right == parent)
parentParent->_right = subL;
else
parentParent->_left = subL;
subL->_parent = parentParent;
}
// 更新旋转完成之后的平衡因子
subL->_bf = parent->_bf = 0;
}
旋转过程中的注意事项:
左单旋代码:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
// 让parent的右指针指向subRL,判断一下subRL是否为空
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
// subR的左指针链接parent
subR->_left = parent;
parent->_parent = subR;
// parent为根的情况,更新根节点,让根节点指向空
if (_root == parent)
{
_root = subR;
_root->_parent = nullptr;
}
else //若parent为一棵子树,则链接与parentParent的关系
{
if (parentParent->_right == parent)
parentParent->_right = subR;
else
parentParent->_left = subR;
subR->_parent = parentParent;
}
// 更新旋转完成之后的平衡因子
parent->_bf = subR->_bf = 0;
}
在 c 子树上新增节点:
当 h = 0 时,57 为新插入节点:
左右双旋旋转步骤:
左右双旋后,平衡因子的更新分为三种情况,参考上图:
左右双旋代码:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf; // 保持旋转前subLR的平衡因子
RotateL(subL);
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); // 旋转前平衡因子已经有问题了
}
在 b 子树上新增节点:
当 h = 0 时,57 为新插入节点:
右左双旋旋转步骤:
右左双旋后,平衡因子的更新分为三种情况,参考上图:
右左双旋代码:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf; // 保持旋转前subRL的平衡因子
RotateR(subR);
RotateL(parent);
// 更新三种情况的平衡因子
if (bf == -1)
{
subRL->_bf = 0;
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 1)
{
subRL->_bf =0;
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == 0)
{
subRL->_bf = 0;
subR->_bf = 0;
parent->_bf = 0;
}
else
assert(false);
}
AVL树删除指定节点,我们需要在树中查找到该节点,查找节点的方法等同于二叉搜索树的查找,即删除的节点可以分为三种情况:
删除的具体方法看这里:二叉搜索树
当我们找到要删除的节点之后,我们需要对删除节点之后树的平衡因子进行调节,平衡因子更新的规则如下:
更新之后 parent 的平衡因子可以分为以下三种情况:
删除节点之后的旋转分为以下几种情况:
注意:5号情况和6号情况更新完成之后树的高度没有发生变化,因此旋转完成之后不需要继续向上更新平衡因子。平衡因子的更新如下图所示:
注意:写代码的时候对照着图去写,这样就不太会出错。
AVL树删除代码:
bool erase(const K& key)
{
// 空树,直接返回false
if (_root == nullptr)
return false;
Node* parent = _root;
Node* cur = _root;
Node* deleteParent = nullptr; // 记录要删除节点的父节点
Node* deleteCur = nullptr; // 记录要删除的节点
while (cur)
{
if (cur->_kv.first < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
parent = cur;
cur = cur->_left;
}
else // 找到了要删除的节点
{
if (cur->_right == nullptr) // 待删除节点的右子树为空
{
if (cur == _root) // 待删除节点为根的情况
{
// 让待删除节点的左子树作为新的根
_root = cur->_left;
if (_root)
_root->_parent = nullptr;
// 删除节点并返回
delete cur;
return true;
}
else
{
// 记录将要删除的节点和其父节点
deleteParent = parent;
deleteCur = cur;
}
}
else if (cur->_left == nullptr) // 待删除节点的左子树为空
{
if (cur == _root) // 待删除节点为根的情况
{
// 让待删除节点的右子树作为新的根
_root = cur->_right;
if (_root)
_root->_parent = nullptr;
delete cur;
return true;
}
else
{
// 记录将要删除的节点和其父节点
deleteParent = parent;
deleteCur = cur;
}
}
else // 待删除节点左右子树均不为空
{
// 找到待删除节点右子树中的最左节点进行替换删除
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
// 将找到的右子树中最小值中的kv里的数据赋值给待删除节点
cur->_kv.first = minRight->_kv.first;
cur->_kv.second = minRight->_kv.second;
// 记录待删除的节点及其父节点
deleteParent = minParent;
deleteCur = minRight;
}
// 更新节点祖先的平衡因子
break;
}
}
// 没有找到待删除节点,返回false
if (cur == nullptr)
return false;
// 更新cur和parent
cur = deleteCur;
parent = deleteParent;
// 最坏的情况下可能一直更新到更节点
while (cur != _root)
{
// 待删除的节点为parent的左节点,parent平衡因子+1
if (cur == parent->_left)
parent->_bf++;
// 待删除的节点为parent的右节点,parent平衡因子-1
else if (cur == parent->_right)
parent->_bf--;
//parent平衡因子为0,继续沿着祖先节点向上更新
if (parent->_bf == 0)
{
cur = parent;
parent = parent->_parent;
}
// 删除后保存平衡,停止需要更新
else if (parent->_bf == 1 || parent->_bf == -1)
{
break;
}
//以下为图中的6种情况,对照图去完成旋转和平衡因子的更新
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 情况3
if (parent->_bf == 2 && parent->_right->_bf == 1)
{
Node* tmp = parent->_right; //记录旋转完成之后的根节点,方便后续继续往上更新
RotateL(parent);
parent = tmp; // 更新parent
}
// 情况4
else if (parent->_bf == 2 && parent->_right->_bf == -1)
{
Node* tmp = parent->_right->_left;
RotateRL(parent);
parent = tmp;
}
// 情况6
else if (parent->_bf == 2 && parent->_right->_bf == 0)
{
Node* tmp = parent->_right;
RotateL(parent);
parent = tmp;
//更新旋转后树节点的平衡因子
parent->_bf = -1;
parent->_left->_bf = 1;
// 旋转之后仍热保持平衡,停止更新
break;
}
// 情况1
else if (parent->_bf == -2 && parent->_left->_bf == -1)
{
Node* tmp = parent->_left;
RotateR(parent);
parent = tmp;
}
// 情况2
else if (parent->_bf == -2 && parent->_left->_bf == 1)
{
Node* tmp = parent->_left->_right;
RotateLR(parent);
parent = tmp;
}
//情况5
else if (parent->_bf == -2 && parent->_left->_bf == 0)
{
Node* tmp = parent->_left;
RotateR(parent);
parent = tmp;
parent->_bf = 1;
parent->_right->_bf = -1;
break;
}
else
{
// 说明删除节点旋转前平衡因子已经出现错误
assert(false);
}
// 继续沿着父节点往上更新
cur = parent;
parent = parent->_parent;
}
}
// 待删除节点的左子树为空
if (deleteCur->_left == nullptr)
{
if (deleteCur == deleteParent->_left)
{
deleteParent->_left = deleteCur->_right;
if (deleteCur->_right)
deleteCur->_right->_parent = deleteParent;
}
else
{
deleteParent->_right = deleteCur->_right;
if (deleteCur->_right )
deleteCur->_right->_parent = deleteParent;
}
}
// 待删除节点的右子树为空
else
{
if (deleteCur == deleteParent->_left)
{
deleteParent->_left = deleteCur->_left;
if (deleteCur->_left)
{
deleteCur->_left->_parent = deleteParent;
}
}
else
{
deleteParent->_right = deleteCur->_left;
if (deleteCur->_left )
{
deleteCur->_left->_parent = deleteParent;
}
}
}
// 删除指定节点
delete deleteCur;
return true;
}
AVL数的查找按照二叉搜索树的规则进行查找,找到了返回节点指针,找不到返回空指针。
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;
}
析构函数中调用一个子函数递归分别删除AVL树的左子树和右子树,最后删除根节点并置为空。
void _Destroy(Node* root)
{
if (root == nullptr)
return;
_Destroy(root->_left); // 递归删除左子树
_Destroy(root->_right);// 递归删除右子树
delete root; // 删除根节点
}
~AVLTree()
{
_Destroy(_root);
_root = nullptr;
}
V& operator[](const K& key)
{
pair<Node*, bool> ret = insert(make_pair(key, V()));
return ret.first->_kv.second;
}
AVL树是在二叉搜索树的基础上加入了平衡因子的限制,因此要验证AVL树是否实现正确,可以分两步:
验证其是否为二叉搜索树:
使用中序遍历得到的是一个有序的序列,则说明其满足二叉搜索树的性质。
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " -> " << root->_kv.second << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
验证其为平衡树:
// 求树的高度
int _Height(Node* root)
{
if (root == nullptr)
return 0; // 空树,高度为0
int leftHeight = _Height(root->_left); // 递归求左子树的高度
int rightHeight = _Height(root->_right); // 递归求右子树的高度
// 树的高度为左子树或者右子树中较高的那一棵树的高度加 1
return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}
// 验证平衡因子是否符合规则
bool _isBalance(Node* root)
{
// 空树符合要求
if (root == nullptr)
return true;
// 算出这棵树中左子树和右子树的高度
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
// 检测一下平衡因子是否正确,判断左子树与右子树的高度差是否满足要求
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常 :" << root->_kv.first << endl;
return false;
}
// 递归检测以每一个节点为根节点的子树
return abs(rightHeight - leftHeight) < 2 && _isBalance(root->_left) && _isBalance(root->_right);
}
bool IsAVLTree()
{
return _IsBalance(_root);
}
AVL树是一棵绝对平衡的二叉搜索树,其中任意节点的左子树和右子树的高度差绝对值都不超过1,这样就保证了高效率的查找数据。但是若要对AVL树中做一些结构修改的操作性能比较低。例如:插入时为了保持树的平衡需要旋转很多次,删除节点时,有可能要让其一直旋转持续到根节点的位置。因此若需要一种查询高效且有序的数据结构,且数据的个数为静态的(不会改变),可以考虑使用AVL树。需要经常用到修改删除时就不太合适。