数据结构 之 【AVL树的简介与部分实现】(部分实现只涉及AVL树的插入问题,包括单旋((右单旋、左单旋))、双旋(左右单旋、右左单旋)等操作)
目录
1.AVL树的概念
2.AVL树节点定义
3.AVL树的插入
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子及旋转
(1)更新平衡因子
(2)右单旋
(2)左单旋
(2)左右单旋
(2)右左单旋
3.快速记忆方法
3.调试技巧
1.AVL树的概念
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
(1)左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
(2)它的左右子树都是AVL树
显然,AVL树也是递归定义的
下面以K模型实现插入,K模型、KV模型参考我的博客 二叉搜索树的应用
2.AVL树节点定义
//模板,以适应不同数据类型
template<class K>
struct AVLTreeNode//struct定义,将节点暴露方便后续使用
{//三叉链AVLTreeNode<K>* _left;AVLTreeNode<K>* _right;AVLTreeNode<K>* _parent;//键值与平衡因子K _key;int _bf;//new一个新节点时需要调用节点的构造函数AVLTreeNode(const K& key):_left(nullptr),_right(nullptr),_parent(nullptr),_key(key),_bf(0){ }//其余默认构造使用编译器自动生成的即可,这里暂且不用手写,也用不到
};
这个类负责树节点的定义及初始化:
(1)使用模板,以适应不同数据类型
(2)struct定义,将节点暴露方便后续使用
(3这里使用三叉链,即一个节点既保存其左右孩子节点指针又保存其父亲节点指针
3.AVL树的插入
AVL树是特殊的二叉搜索树,AVL树的插入就是在二叉搜索树插入的基础上通过更新平衡因子,进而按需旋转,最终达到左右子树高度差的绝对值不超过1的过程
那么AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
template<class K>
class AVLTree
{typedef AVLTreeNode<K> Node;
public://这里只讲AVL树的插入问题,重在了解它的性质特点bool Insert(const K& key){//先查找,再插入(需要考虑树为空的情况)if (!_root){_root = new Node(key);return true;}//树不为空Node* cur = _root;Node* parent = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else//相同值不能插入,去重效果{return false;}}//找到插入位置了,但是需要链接其父亲节点//父亲节点一定不为空cur = new Node(key);if (key > parent->_key){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//更新平衡因子//插入结束即是成功return true;
}
private://可以偷懒在这初始化Node* _root = nullptr;
};
前半截插入过程与二叉搜索树一致:
向左向右走,遇到空就开始插入,遇到相同值的就返回false(去重效果)三叉链!!注意链接父亲节点
2. 调整节点的平衡因子及旋转
平衡因子不是必须的,也可以直接通过高度函数来算,只不过比较麻烦
这里定义 平衡因子 = 右子树高度 - 左子树高度
当然也可以反过来
如上图:
(1)插入节点的平衡因子为 0 (其左右子树为空)
(1)插入节点在左,其父亲节点的平衡因子减减
插入节点在右,其父亲节点的平衡因子加加
(1)调整后,父亲节点的平衡因子为0,说明原来的平衡因子为1或-1,即
以父亲节点为根节点的这颗AVL子树符合定义且高度未发生变化不用向上调整平衡因子
(1)调整后,父亲节点的平衡因子为1或-1,说明原来的平衡因子为0,即
以父亲节点为根节点的这颗AVL子树虽符合定义但高度确实发生了变化,需要继续向上调整
(1)调整后,父亲节点的平衡因子为2或-2,
以父亲节点为根节点的这颗子树已不符合定义,需要原地进行旋转操作使其符合定义(平衡)
(1)更新平衡因子
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);}
}
(1)最后一个else语句是为了检测平衡因子是否更新错误,这样一来可以便利调试
(2)最坏情况就是更新到根节点的平衡因子后截止
---------------------------------------------------------旋转--------------------------------------------------------------
(2)右单旋
注意三个节点:parent、cur、curright
(1)当 parent->_bf == -2 && cur->_bf == -1 触发右单旋条件
(2)右单旋的实质是把cur的右给到parent的左,parent作为cur的右
可以想象成:parent绕着cur顺时针旋下来
(3)但parent、cur、curright三个节点的平衡因子、父亲节点、左右孩子节点仍要按需更新
(4)旋转之后,三个节点的平衡因子都为0,且该子树的高度与插入前一致
void RotateR(Node* parent)
{Node* cur = parent->_left;Node* curright = cur->_right;//只需更新curright的_parentif (curright)curright->_parent = parent;//更新parent的 _parent,_left,_bfNode* ppnode = parent->_parent;parent->_left = curright;parent->_parent = cur;parent->_bf = 0;//更新cur的 _parent,_right,_bfcur->_bf = 0;cur->_right = parent;if (!ppnode){_root = cur;cur->_parent = nullptr;}else{cur->_parent = ppnode;if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}}
}
(1)curright可能为空
(2)parent的父亲需要指向现在的子树的根节点cur:
提前保存parent的父亲节点(ppnode),
ppnode为空,说明这是一棵独立的树,此时更新树的根节点及cur的父亲指向
ppnode不为空,说明这是一棵局部的子树,此时更新cur的父亲指向同时让ppnode指向cur
右单旋的场景是数不清的,上图通过抽象得出结论:
当 parent->_bf == -2 && cur->_bf == -1 触发右单旋条件
(2)左单旋
注意三个节点:parent、cur、curleft
(1)当 parent->_bf == 2 && cur->_bf == 1 触发左单旋条件
(2)左单旋的实质是把cur的左给到parent的右,parent作为cur的左
可以想象成:parent绕着cur逆时针旋下来
(3)但parent、cur、curleft三个节点的平衡因子、父亲节点、左右孩子节点仍要按需更新
(4)旋转之后,三个节点的平衡因子都为0,且该子树的高度与插入前一致
void RotateL(Node* parent)
{Node* ppnode = parent->_parent;Node* cur = parent->_right;Node* curleft = cur->_left;//只需更新curleft的_parentif (curleft)curleft->_parent = parent;//更新parent的_right,_parentparent->_right = curleft;parent->_parent = cur;//更新cur 的 _left,_parentcur->_left = parent;if (!ppnode){//一棵独立的树_root = cur;cur->_parent = nullptr;}else{//一棵局部子树if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}//平衡因子parent->_bf = cur->_bf = 0;
}
(1)curleft 可能为空
(2)parent的父亲需要指向现在的子树的根节点cur:
提前保存parent的父亲节点(ppnode),
ppnode为空,说明这是一棵独立的树,此时更新树的根节点及cur的父亲指向
ppnode不为空,说明这是一棵局部的子树,此时更新cur的父亲指向同时让ppnode指向cur
右单旋的场景是数不清的,上图通过抽象得出结论:
当 parent->_bf == 2 && cur->_bf == 1 触发左单旋条件
(2)左右单旋
双旋就是旋转两次的意思
注意三个节点:parent、cur、curright
(1)当 parent->_bf == -2 && cur->_bf == 1 触发左右单旋条件
(2)左右单旋的实质是将curright的左给到cur的右,curright的右给到parent的左
cur成为curright的左,parent成为curright的右,curright成为根节点,即
先左单旋根节点为cur的AVL树,再右单旋根节点为parent的AVL树
(3)旋转之后,该AVL树的高度与插入前一致
插入节点的祖先的平衡因子在插入后已经向上更新,但单旋会将parent、cur、curright的平衡因子都变为0,事实上,curright子树的插入会影响parent、cur的平衡因子
此时双旋的重点变为更新节点的平衡因子
(1)curright为插入节点时,
根据左右单旋的实质,此时三个节点(parent、cur、curright)的平衡因子均为0
(2)插入节点为curright的左孩子节点时,
根据左右单旋的实质,curright、cur的平衡因子为0,parent的平衡因子为1
(3)插入节点为curright的右孩子节点时,
根据左右单旋的实质,curright、parent的平衡因子为0,cur的平衡因子为-1
提前保存curright的平衡因子即可区分上述三种情况
void RotateLR(Node* parent)
{Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(cur);RotateR(parent);if (bf == 0){curright->_bf = 0;parent->_bf = 0;cur->_bf = 0;}else if (bf == 1){curright->_bf = 0;parent->_bf = 0;cur->_bf = -1;}else if (bf == -1){curright->_bf = 0;parent->_bf = 1;cur->_bf = 0;}else{assert(false);}}
最后一个else语句用来判断平衡因子是否更新错误
左右单旋的场景是数不清的,上图通过抽象得出结论:
当 parent->_bf == -2 && cur->_bf == 1 触发左右单旋条件
且旋转之后,该AVL树的高度与插入前一致
(2)右左单旋
双旋就是旋转两次的意思
注意三个节点:parent、cur、curleft
(1)当 parent->_bf == 2 && cur->_bf == -1 触发右左单旋条件
(2)右左单旋的实质是将curleft的左给到parent的右,curleft的右给到cur的左
parent成为curleft的左,cur成为curleft的右,curleft成为根节点,即
先右单旋根节点为cur的AVL树,再左单旋根节点为parent的AVL树
(3)旋转之后,该AVL树的高度与插入前一致
插入节点的祖先的平衡因子在插入后已经向上更新,但单旋会将parent、cur、curleft的平衡因子都变为0,事实上,curleft子树的插入会影响parent、cur的平衡因子
此时双旋的重点变为更新节点的平衡因子
(1)curleft为插入节点时,
根据左右单旋的实质,此时三个节点(parent、cur、curleft)的平衡因子均为0
(2)插入节点为curleft的左孩子节点时,
根据左右单旋的实质,curleft、parent的平衡因子为0,cur的平衡因子为1
(3)插入节点为curleft的右孩子节点时,
根据左右单旋的实质,curleft、cur的平衡因子为0,parent的平衡因子为-1
提前保存curleft的平衡因子即可区分上述三种情况
void RotateRL(Node* parent)
{Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;RotateR(cur);RotateL(parent);//双旋的实质是将curleft的左给到parent的右,curleft的右给到cur的左// curleft成为子树的根节点//双旋重在更新平衡因子if (bf == 0){curleft->_bf = 0;parent->_bf = 0;cur->_bf = 0;}else if (bf == 1){curleft->_bf = 0;parent->_bf = -1;cur->_bf = 0;}else if (bf == -1){curleft->_bf = 0;parent->_bf = 0;cur->_bf = 1;}else{assert(false);}
}
右左单旋的场景是数不清的,上图通过抽象得出结论:
当 parent->_bf == 2 && cur->_bf == -1 触发左右单旋条件
且旋转之后,该AVL树的高度与插入前一致
3.快速记忆方法
parent与cur的不同链接及cur的左右插入正好对应四种旋转要求
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)
{RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{RotateRL(parent);
}
3.调试技巧
AVL的插入包括查找、链接、更新平衡因子、旋转等多个步骤,单步调试很困难
这里罗列几个调试技巧
(1)以调试运行
vs2022中,快捷键F5,以调试运行,可使程序在异常抛出时中断
(2)调用堆栈
vs2022中,快捷键 Alt + 7,打开调用堆栈窗口,可初步查出崩溃的节
(3)条件断点
当主观判断可能是在某一节点出错时,写一段无关紧要的代码使程序运行到插入该节点前
(4)借助辅助代码
bool IsBalance()
{return _IsBalance(_root);
}int TreeHeight(Node* root)
{if (!root)return 0;int leftH = TreeHeight(root->_left);int rightH = TreeHeight(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;
}bool _IsBalance(Node* root)
{if (!root)return true;//后序遍历,计算高度稍微简化了if (!_IsBalance(root->_left))return false;if (!_IsBalance(root->_right))return false;int leftH = TreeHeight(root->_left);int rightH = TreeHeight(root->_right);//先手,平衡因子出错就不玩了if (rightH - leftH != root->_bf){cout << root->_key << "的平衡因子更新错误(实际上->存储的):"<< rightH - leftH << "->" << root->_bf << endl;return false;}//AVL树的特性,高度差小于1if (abs(rightH - leftH) > 1){cout << root->_key << "不平衡" << endl;return false;}return true;
}
中序遍历只能验证该树是否是二叉搜索树,AVL树的验证重点在于左右子树的高度差
该辅助代码可以找到平衡因子更新出错的节点,进而配合前面的条件断点锁定出错的地方