本篇博客主要讲解 AVLTree 的插入 修改 查找 判断
感谢观看,欢迎提出建议和问题
联系方式:blbagony@163.com
完整代码
AVLTree
在計算機科學中,AVL 樹是一種自平衡二叉搜索樹。這是第一個要發明的數據結構。在 AVL 樹中,任何節點的兩個子樹的高度最多不同一個; 如果在任何時候它們不同於一個,則重新平衡來恢復此屬性。查找,插入和刪除都在平均和最差情況下都採用O(log n)時間,其中 n 是操作之前樹中的節點數。插入和刪除可能需要通過一個或多個樹輪重新平衡樹。
AVL 樹以其兩位蘇聯發明家 Georgy Adelson-Velsky 和 Evgenii Landis 命名,他們在1962年的“信息組織算法”中發表。
AVL 樹通常與紅黑樹進行比較,因為它們都支持相同的操作集,並只需要O(log n)的时间复杂度用於基本操作。對於查找密集型應用程序,AVL 樹比紅黑樹更快,因為它們更加嚴格平衡。與紅黑樹相似,AVL 樹高度更加平衡。兩者都是在一般情況下,既不重量平衡也不 μ- 均衡任何 μ≤ 1 / 2 ; 也就是說,左右子树结点个数不同,但高度不会相差过大。
平衡係數
在二叉樹中,將節點 N 的平衡因子定義為高度差
BalanceFactor(N):= Height(RightSubtree( N )) - Height(LeftSubtree( N ))即右孩子的高度减去左孩子的高度。如果高度差的绝对值不大于一,則二叉樹被定義為 AVL 樹。
BalanceFactor( N )∈{-1,0,+ 1 }一個節點的 BalanceFactor( N )< 0 被稱為“左重”,一個节点 BalanceFactor( N )> 0 稱為“右重”,一個节点 BalanceFactor( N )= 0 有時簡稱為近似平衡“ 。
节点插入
將元素插入到AVL樹中時,最初按照与插入二叉搜索樹的過程相似。更明確地說:如果前面的搜索沒有成功,先判断树是否为空,若为空,给 root 插入新節點。或者,如果樹不是空的,則搜索例程返回一個節點和方向(左或右),当返回的節點不具有子節點。判断给当前结点左边还是右边插入,返回的位置即为將要插入的節點的位置。
在插入之後,有必要檢查每個節點的祖先與 AVL 樹的平衡因子的一致性:這被稱為“回溯”。這是通過考慮每個節點的平衡因子來實現的。
由於單插入,AVL子樹的高度不能增加一個以上,插入後的節點的臨時平衡因子將在[-2,+ 2] 的範圍內。需要檢查之前的每個節點,如果当前父节点的平衡因子保持在-1到+1的范围内,則只需要更新平衡因子,而不需要旋轉。然而,如果当前父节点平衡因子變得小于 -1 或大于 +1,則根據該節點的子樹是 AVL 不平衡的,並且需要各种旋轉。
bool insert(const k& key, const v& value)
{
if (!_root){
_root = new Node(key, value);
return true;
}
Node* cur = _root;
Node* parent = NULL;
while (cur){
parent = cur;
if (key > cur->_key)
cur = cur->_right;
else if (key < cur->_key)
cur = cur->_left;
else
return false;
}
cur = new Node(key, value);
cur->_parent = parent;
if (key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
/* 寻找合适的插入位置 */
while (cur){
parent = cur;
if (key > cur->_key)
cur = cur->_right;
else if (key < cur->_key)
cur = cur->_left;
else
return false;
}
cur = new Node(key, value);
cur->_parent = parent;
if (key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
/* 更新平衡因子 */
while (parent)
{
/* 注意只能用指针判断,如果用 KEY 值判断,当左右旋转或右左旋转时,会修改原来更新好的平衡因子 */
if (parent->_left == cur)
parent->_bf--;
else if (parent->_right == cur)
parent->_bf++;
if (parent->_bf == 0)
break;
else if (1 == abs(parent->_bf)){
cur = parent;
parent = parent->_parent;
}
else /* |平衡因子| = 2*/
{
if (2 == parent->_bf){
Node* subR = parent->_right;
if (1 == subR->_bf) /* 左旋 */
_left_left_rotation(parent);
else if (-1 == subR->_bf) /*先右旋再左旋*/
_right_left_rotation(parent);
}
else if (-2 == parent->_bf)
{
Node* subL = parent->_left;
if (-1 == subL->_bf)
_right_right_rotation(parent); /* 右旋 */
else if (1 == subL->_bf)
_left_right_rotation(parent); /*先左旋再右旋*/
}
/* 防御式编程,代码不会执行到这里 */
else
return false;
}
}
}
单旋
- 这里主要讲左旋,因为左单璇和有单选类似
- 当平衡因子变成 2 时并且右孩子的子的平衡因子是 1,将父节点 x 移至右孩子(subR) z 的左边,因为有孩子的左边可能不为空,移动之前需要先保存右孩子的左边(subRL)t23,将 t23 移动至 x 的右边,因为即使是空也应该将 t23 移至x 的左边。判断 x 是其父节点 (grandfather)的左/右同时,判断父节点是否为空,若为空直接将 subR 交给 root, 不为空,改变 grandfather 的左或右指向 z (subR),判断 subRL 是否为空。不为空,更新其父亲指针指向 x,最后更新平衡因子。
void _left_left_rotation(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* grandfather = parent->_parent;
parent->_parent = subR;
subR->_left = parent;
if (!grandfather){
_root = subR;
subR->_parent = NULL;
}
else {
if (parent->_key > grandfather->_key)
grandfather->_right = subR;
else
grandfather->_left = subR;
subR->_parent = grandfather;
}
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
parent->_bf = subR->_bf = 0;
}
双旋
- 这里主要将右左双选
当平衡因子变为 2,并且 subR 的平衡因子为 -1 时。在这种情况下,改变平衡因子需要考虑三种可能。
当 subRL 的平衡因子是 -1 时,表示 subRL 当前左树重,旋转时将 subRL 的左树交给 parent 的右边,此时 parent 的平衡因子为 0;再看 subR,此时 subR 的右树重,更新其平衡因子为 1;
当 subRL 的平衡因子是 1 时,表示 subRL 当前右树重,旋转时将 subRL 的右树交给 subR 的左边,此时 subR 的平衡因子为 0;再看 parent,此时 parent 的左树重,更新器平衡因子为 -1;
当 subRL 的平衡因子为 0 时,表示只需要将 subRL 提升至父节点,parent subR, subRL 平衡因子都为 0;
void _right_left_rotation(Node*& parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
_right_right_rotation(subR);
_left_left_rotation(parent);
if (1 == bf){
parent->_bf = -1;
subRL->_bf = 0;
}
else if (-1 == bf){
parent->_bf = 0;
subR->_bf = 1;
}
else
{
subR->_bf = parent->_bf = 0;
}
subRL->_bf = 0;
}
判断是否为平衡树
判断是否为平衡树,我们需要比较其左树与右树的高度差,若高度差在 ( - 2, + 2) 这个区间内,则我们可以说这棵树是一颗平衡树,否则不是。
要求高度用递归方法很简便,返回条件为当前节点是否为空,不为空继续向左右子树找最深的那颗,若为空返回 0;
int _getDeep(Node* node){
if (!node) return 0;
int left = _getDeep(node->_left);
int right = _getDeep(node->_right);
/* +1 因为自己也算一层 */
return left > right ? left + 1: right + 1;
}
- 所以我们发现判断 AVL 树的代码就很好写了,只需要判断两颗字数高度差的绝对值不大于 1 就返回真,else 返回假。
bool _isBalance(Node* node){
if (!node) return true;
int left = getDeep(node->_left);
int right = getDoeep(node->_right);
if ( abs(left - right) < 2 && _isBalance(node->_left) && _isBalance(node->_right))
return true;
return false;
}
这里我们很容易发现一个问题,那就是时间复杂度和空间复杂度。每一次判断都需要将左子树和右子树的高度求一遍,时间复杂度为 o(N²),这种方法无疑增加了许多重复工作,素以我们需要让时间复杂度降低到 o(N)。
o(N)的方法判断是否为 AVL 树
我们发现在计算高度时,计算机做了大量的重复工作。所以在求子节点高度时,我们可以让子节点把他的高度事先保存下来到其节点的信息中,但这样就需要修改节点的结构,这是一种侵入式编程,只是因为需要判断是否为 AVL 树而去修改子节点结构是不划算的,侵入式编程往往是不好的。这里我们可以在判断是否为平衡树时,先判断左右子树是否平衡,这时我们可以传入一个输出型参数,该参数保存子树高度。这样我们就不需要重复求子树高度,只需要遍历一遍就可以判断。将时间复杂度降为o(N)。
bool _isBalance(Node* node, int& deep){
if (!node){
deep = 0;
return true;
}
int left = 0;
if ( !_isBalance(node->_left, left) )
return false;
if ( !_isBalance(node->_right, right) )
return false;
deep = left > right ? left + 1 : right + 1;
if ( abs(left - right) < 2)
return true;
return false;
}
后面还需要进行删除