赞
踩
目录
我们在使用map/multimap/set/multiset这些容器时,有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
它具有以下性质:
AVL树的创建,进行插入与删除节点,同时进行平衡判断。(我在分析时,会将代码拆开来,而且只会写核心的部分,最终代码我会附上链接(C++实现链接),需要的直接去拿。)
- 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){}
- };
我们在对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;
- }
插入节点,如果此时的树满足平衡,那么就不用进行调节,但是如果此时树不再保持平衡,就要对树进行旋转调整,分为四种情况。(我在分析时,会将代码拆开来,而且只会写核心的部分,最终代码我会附上链接(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节点的右子树通过左单旋进行调整,使其满足左单旋的情况,再进行左单旋。此时二叉树就会满足平衡。
在插入之前,平衡因子是满足情况的,当插入了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;
- }
- }
- }
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- //求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树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。 因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树, 但一个结构经常修改,就不太适合。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。