目录
我们在使用map/multimap/set/multiset这些容器时,有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
一.AVL树的概念与性质
1.1概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
1.2性质
它具有以下性质:
- 他的左右子树都是AVL树
- 左右子树高度差(又称平衡因子)的绝对值不超过1
- 如果一颗二叉搜索树是高度平衡的,他就是AVL树,如果他有n个节点,其高度可保持在O(log2n),搜索时间复杂度O(log2n)。
二.AVL树操作
AVL树的创建,进行插入与删除节点,同时进行平衡判断。(我在分析时,会将代码拆开来,而且只会写核心的部分,最终代码我会附上链接(C++实现链接),需要的直接去拿。)
2.1AVL树节点定义
struct AVLTreeNode{ AVLTreeNode* left; AVLTreeNode* right; AVLTreeNode* parent; T data; int bf;//VAL树中的平衡因子 AVLTreeNode(const T& x) :left(nullptr) , right(nullptr) , parent(nullptr) , data(x) , bf(0){} };
2.2AVL树插入
1.插入节点
我们在对AVL树进行插入的时候,其实就是二叉搜索时的插入。
bool insert(const T& data){ //先判断是否为空树 if(nullptr == root){ root = new Node(data); return true; } Node* cur = root; Node* parent = nullptr; while(cur){ parent = cur; if(data < cur->data){ cur =cur->left; }else if (data > cur->data){ cur = cur ->right; }else{ return false; } } //当找到要插入的位置时,进行插入 cur = new Node(data); //如果此时插入的节点是大于父节点的,将其插入父节点的左边 if(parent->data > data) parent->left = cur; else parent->right = cur; return true; }
2.保持树的平衡
插入节点,如果此时的树满足平衡,那么就不用进行调节,但是如果此时树不再保持平衡,就要对树进行旋转调整,分为四种情况。(我在分析时,会将代码拆开来,而且只会写核心的部分,最终代码我会附上链接(C++实现),需要的直接去拿。)
(1)右单旋
发生的情况:当一个平衡二叉树满足具有较高左子树时(如下图A),且要插入的节点插入到了较高左子树的左侧(此时注意是左子树,并非左孩子,因此当插入节点20为30的有孩子,其操作是相同的),此时进行右单旋。
我们根据上图进行分析,在进行旋转时,第一步先将parent->left指向curR节点,此时并要对curR的parent进行更新,但是必须注意的是此时curR是否为空,如果此时curR为空再对他进行操作可能代码会崩溃,如下代码是对curR进行更新。
parent->left = curR; //因为我们使用的是孩子双亲表示法,因此更新curR节点的parent //此时curR不为空时,才能对其进行更新 if(curR) curR->parent = parent;
第一步操作结束,下来让cur->right指向parent节点,此时我们并不知道parent节点是否为之前树的根节点,因此要先将其父节点进行保存,后面要进行判断,再对插入的节点旋转结束后,对其旋转因子再进行更新。下面是第二步代码:
cur->right = parent; Node* pparent = parent->parent; //现在我们先更新parent节点的父节点 parent->parent = cur; //再更新cur的父节点 cur->parent = pparent; //此时我们要注意,判断pparent是否为空,如果为空,那么root直接就是cur,如果不为空在进行判断他是父节点的左子树还是右子树 if(pparent == nullptr)//说明此时更新节点后,cur应该为根节点 root = cur; else if(pparent->left == parent)//说明之前是根节点的左子树 pparent->left = cur; else//说明之前是右子树 pparent->right = cur; parent->bf = 0; cur->bf = 0;
(2)左单旋
发生的情况:当一个平衡二叉树满足具有较高右子树时(如下图A),且要插入的节点插入到了较高右子树的右侧(此时注意是右子树,并非有孩子,因此,其操作是相同的),此时进行左单旋。
我们根据上图进行分析,当开始进行左单旋时,我们先将parent节点进行处理,如下代码:
parent->right = curL;
//因为我们使用的是孩子双亲表示法,因此更新curL节点的parent
//此时curL不为空时,才能对其进行更新
if(curL) curL->parent = parent;
当处理完第一步之后,开始对cur节点进行处理,此时我们依旧要注意parent节点在左单旋之前是否是由父节点的,如下代码:
//在这里先创建parent的父节点,待会进行判断
Node* pparent = parent->parent;
cur->left = parent;
//更改parent的父节点
parent->parent = cur;
cur->parent = pparent;
//此时上面的代码将parent与curL两个节点的父节点都处理完毕,
//此时我们就要考虑cur此时是否有父节点,且是父节点的那个子树
if(pparent == nullptr)//此时parent的之前的父节点为空,那么此时cur直接就是根节点
root = cur;
else if(pparent->left == parent)
pparent->left = cur;
else
ppanret->right = cur;
cur->bf = 0;
parent->bf = 0;
(3)左右双旋与右左双旋
左右双旋发生的情况:当原本的平衡二叉树是满足具有较高的左子树时,此时带插入的节点插入到了较高左子树的右侧(内侧),如下图,此时就要使用左右双旋。
我们在这里发现,他与要进行右单旋的情况有些像,但是在向AVL树中进行插入时,左子树的内侧,此时我们先试一试右单旋,观察他是否可以解决平衡问题,如下图。
我们观察到上面的图,发现虽然这种情况下与右单旋的情况有些相似,但是不能够达到平衡的情况,因此我们就要重新考虑如何旋转使之平衡。如下图所示:
我们观察上图,可以先利用左单旋将parent的左子树进行调整,将其转化成结构B,此时的二叉树的结构符合右单旋的情况,因此再进行右单旋,最终满足平衡的情况,因此此时需要先进行左单旋,再进行右单旋。我们再看图D到图E的平衡因子的变化。因为,我们在前两次的左单旋与右单旋之后都对他当前的parent与cur的平衡因子进行的更改,在最后在curR上插入新节点的位置会引起不同的平衡因子的更新。此时的代码就是复用上面的代码。因此在这里就不再重复。
右左双旋发生的情况:当原本的平衡二叉树是满足具有较高的右子树时,此时带插入的节点插入到了较右子树的左侧(外侧),此时就要使用右左双旋。他的思路与左右双旋的思路相同,但是右左双旋实现将parent节点的右子树通过左单旋进行调整,使其满足左单旋的情况,再进行左单旋。此时二叉树就会满足平衡。
3.更新parent平衡因子
在插入之前,平衡因子是满足情况的,当插入了cur节点之后,我们要从cur插入的位置向上更新parent的平衡因子,在更新时,我们只需要判断cur被插入到了parent的那棵子树,插入到左子树时,对parent->buf--;插入到右子树时,对parent->buf++。代码如下:
while(parent){//当parent不为空,那就相当于要遍历到二叉树的根节点即可 if(parent->right == cur)//说明此时插入到了右子树的位置 parent->bf++; else//说明此时插入到了左子树的位置 parent->bf--; if( parent->bf == 0)//这里就是以parent为根的二叉树高度没有改变,对上层不会用影响 break; else if(1 == parent->bf || -1 == parent->bf){ //以parent为根的二叉树的高度增建了,需要继续往上层更新 cur = parent; parent = parent->parent; }else{//此时已经违反了AVL树的特性 if(parent->bf == -2){ if(cur->bf == -1){ //此时进行右单旋 }else{ //此时进行左右双旋 } else{ if(cur->bf == -1){ //此时进行右左双旋 }else{ //此时进行左单旋 } break; } } }
4.AVL树验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
- 验证其为平衡树 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子) 节点的平衡因子是否计算正确
//求AVL树的深度(高度) int get_hight(node* root){ if (root == nullptr) return 0; int hightleft = get_hight(root->left); int hightright = get_hight(root->right); int hight = hightleft > hightright ? hightleft + 1 : hightright + 1; return hight; } //判断是否是AVLTree bool isAVLTree(node* root){ if (nullptr == root) return true; int hightleft = get_hight(root->left); int hightright = get_hight(root->right); int bf = hightright - hightleft; if (abs(root->bf) > 1 || bf != root->bf){//当平衡因子的绝对值大于等于2,或者在更新平衡因子时出错,都返回false cout << root->data << ":" << bf << "--" << root->bf << endl; return false; } return isAVLTree(root->left) && isAVLTree(root->right);//在进行检测他的左右子树 }
三.AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。 因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树, 但一个结构经常修改,就不太适合。