AVL树的完整实现

注:本篇的介绍风格会偏笔记向,如果第一次接触AVL树实现,看完本文关于旋转实现的思想还未完全理解,可以看看下面两个我比较推荐的教程,也作为作者本文的参考博客。
1. 手把手教AVL树(图文解析向博客)
2. AVL树从入门到入土(B站视频 “看完还不懂可以选择弃疗” )

下面步入正题:

AVL树基本概念

  • 中文名称:平衡二叉树
  • 结构特点:树中的任意节点的左右子树的高度之差不超过1的二叉搜索树
  • 实现核心操作:查询、插入、删除、旋转
  • 时间复杂度:查询、插入、删除的时间复杂度均为O(logn)

AVL模块化实现

结构体声明

struct AVLNode{
    int value;
    int balance;
    AVLNode* left;
    AVLNode* right;
    AVLNode* parent;
};

struct AVLTree{
    AVLNode* root;
}tree;

  • 平衡因子BF(Balanced Factor):该结点的左子树与右子树的高度之差的值。在每个节点中都要附有这样一个标签。
  • 平衡的识别条件:根据AVL的定义,树中的任意节点的左右子树的高度之差不超过 1。所以,AVL树中的任意结点的平衡因子只可能是:
    ① -1(右子树高于左子树)
    ② 0 (左右子树高度相等)
    ③ 1 (左子树高于右子树)
    如果因为插入新的节点或删除某一个节点使某个其他节点的BF值变为-2或2,则视为当前状态为不平衡的状态,那么就要通过旋转操作来使其恢复平衡。
    那么接下来获取高度的方法和二叉搜索树是一样的。

二叉排序树基本操作

递归查询即将要插入/删除的节点位置

struct AVLNode* select(int value,AVLNode* root){
    if(root==0){
        return 0;
    }
    if(root->value==value){
        return root;
    }
    if(root->value>value){
        if(root->left)
            return select(value,root->left);
        else
            return root;
    }
    if(root->value<value){
        if(root->right)
            return select(value,root->right);
        else
            return root;
    }
}

select函数要点:

  • 这段代码select函数将插入和删除功能中要进行的递归查询操作封装在了一起
  • 考虑空树情况,返回值为0
  • 如果value参数值与查询节点的value值相等,返回该节点。这部操作是为删除操作服务的。
  • 如果value参数值比查询节点值大/小,递归得到该节点的右/左节点的select函数返回值,如果被查询子节点为NULL,则返回该节点,在insert函数中生成新的子节点。

插入新节点

void insert(int value,AVLNode* root){
    AVLNode* node=select(value,root); // 找到即将被插入的目标节点
    if(node==0){  // 如果树为空
        tree.root=(AVLNode*)malloc(sizeof(AVLNode));
        tree.root->value=value;
        tree.root->left=tree.root->right=0;
        tree.root->parent=0;
        tree.root->balance=0;
    }
    else if(node->value!=value){  
        if(node->value>value){
            node->left=(AVLNode*)malloc(sizeof(AVLNode));
            node->left->value=value;
            node->left->left=node->left->right=0;
            node->left->parent=node;
            node->left->balance=0;
            rebalance(node);
        }else if(node->value<value){
            node->right=(AVLNode*)malloc(sizeof(AVLNode));
            node->right->value=value;
            node->right->left=node->right->right=0;
            node->right->parent=node;
            node->right->balance=0;
            rebalance(node);
        }
    }
}

Insert函数要点:

  • 考虑空树情况
  • 通过递归比较树中的节点并插入(这里通过select函数找到被插入节点后再insert函数体内再次进行左右树判断之所以略显繁琐,是为了方便删除节点时复用select函数体内逻辑。)
  • [AVL树] 检测并恢复平衡:rebalance函数。

删除某一节点

void delete(int value,AVLNode* root){
    AVLNode* node=select(value,root);
    if(node->value==value){
        if(node->left&&node->right){
            delete_Twochild(node);
        }else{
            delete_NotTwochild(node);
        }
    }
}

delete函数要点:

  • 通过select函数递归查找被删除节点,节省效率。
  • 考虑被删除节点子树个数的情况:
    1. 有两个子树
    2. 有一个或没有子树
被删除节点有一个子树或没有子树的情况
void delete_NotTwochild(AVLNode* node){
    if(node->parent==0){
        if(node->left){
            tree.root=node->left;
            node->left->parent=0;
        }else{
            tree.root=node->right;
            node->right->parent=0;
        }
    }else{
        if(node->parent->left==node){
            if(node->left){
                node->parent->left=node->left;
                node->left->parent=node->parent;
            }else{
                node->parent->left=node->right;
                if(node->right)
                    node->right->parent=node->parent;
            }
        }else{
            if(node->left){
                node->parent->right=node->left;
                node->left->parent=node->parent;
            }else{
                node->parent->right=node->right;
                if(node->right)
                    node->right->parent=node->parent;
            }
        }
        rebalance(node->parent);
    }
}

delete_NotTwochild函数要点:

  • 如果删除节点子节点数量为0或1,执行该函数。
  • 要考虑到被删除节点是根节点的情况,因为要避免root->parent为空指针的情况。
  • 通过node->parent->left == node的方式判断该节点是父节点的左孩子还是右孩子。
  • 先判断出该节点是其父亲节点的左子树还是右子树,方便在删除操作后将目标删除节点的父亲节点的孩子指针指向目标删除节点的孩子节点。
  • 然后判断目标删除节点的孩子在左子树还是右子树还是不存在。
  • 不管哪种情况,将目标删除节点的父亲节点的孩子指针指向目标删除节点的孩子节点。
  • 下一步要将目标删除节点的孩子节点的父亲指针指向删除节点的父亲节点,考虑到目标删除节点没有孩子的情况会导致node->left->parent出现空指针情况。所以要在这部之前先判断删除节点是否有孩子。
  • [AVL树]完成二叉排序树的基本删除操作后,检查并恢复平衡,rebalance函数。
void delete_Twochild(AVLNode* node){
    AVLNode* after=getMin(node->right);
    node->value=after->value;
    delete_NotTwochild(after);
}

delete_Twochild函数要点:

  • 如果删除节点有两个子节点,执行该函数。
  • 这种情况只需要在删除节点的右子树中通过getMin函数寻找到value值最小的节点,将删除节点的值与其互换,再将找到的最小value值的节点通过delete_NotTwochild函数删除。
struct AVLNode* getMin(AVLNode* node){
    if(node->left)
        getMin(node->left);
    else
        return node;
} 

getMin函数要点:

  • 此函数目的在于寻找某一节点作为根节点的树中value值最小的节点。
  • 根据二叉排序树,递归查找最左侧节点即可。

至此,二叉排序树基本操作介绍完成。

AVL树旋转操作

获取高度

int height(AVLNode* a){
    if(a == 0){
        return 0;
    }
    int rightheight = height(a->right);
    int leftheight = height(a->left);
    return rightheight > leftheight ? (rightheight+1) : (leftheight+1);
}

height函数要点:

  • 考虑空树情况。
  • 分别递归查询左右子树高度,层层递归向上返回高度累加值。

检查并回复平衡

void rebalance(AVLNode* a){
    setBalance(a);
    if(a->balance== -2){
        if(a->left->balance <=0){
            a=turnRight(a);
        }else{
            a=turnLeftThenRight(a);
        }
    }else if(a->balance==2){
        if(a->right->balance>=0){
            a=turnLeft(a);
        }else{
            a=turnRightThenLeft(a);
        }
    }
    if(a->parent){
        rebalance(a->parent);
    }else{
        tree.root=a;
    }
}
  • 首先通过setBalance函数刷新被检查节点的balance值。
  • 开始检查节点balance值:分为四种不平衡情况,对应以下四种旋转方案。
  • 递归向父亲节点递归检查,直到查到根节点。

刷新节点balance值

void setBalance(AVLNode* a){
    if(a)
        a->balance=height(a->right)-height(a->left);
}

单次旋转(向左/右旋转)

Turn Left
struct AVLNode* turnLeft(AVLNode* a){
    AVLNode* b=a->right;
    if(a->parent!=0){
        if(a->parent->right==a){
            a->parent->right=b;
        }else{
            a->parent->left=b;
        }
    }
    b->parent=a->parent;
    a->parent=b;
    a->right=b->left;
    b->left=a;
    if(a->right!=0)
        a->right->parent=a;
    setBalance(a);
    setBalance(b);
    return b;
}

Turn Right
struct AVLNode* turnRight(AVLNode* a){
    AVLNode* b=a->left;
    if(a->parent!=0){
        if(a->parent->right==a){
            a->parent->right=b;
        }else{
            a->parent->left=b;
        }
    }
    b->parent=a->parent;
    a->left=b->right;
    if(a->left!=0)
        a->left->parent=a;
    a->parent=b;
    b->right=a;
    setBalance(a);
    setBalance(b);
    return b;
}
Turn Right 举例

单次旋转函数要点:

  • 单次旋转函数触发条件:
    Turn Right: node->balance = -2 && node->left->balance <= 0
    Turn Left:node->balance = 2 && node->right->balance >= 0
  • 具体处理步骤:(Turn Right 举例)
    ① 把a节点以及他的右子树移除,a的左子节点b代替a的位置
if(a->parent!=0){
        if(a->parent->right==a){
            a->parent->right=b;
        }else{
            a->parent->left=b;
        }
    }

②把b节点的右子树移动成为a节点的左子树

   a->left=b->right;
    if(a->left!=0)
        a->left->parent=a;

③让a节点成为b节点的右孩子

    a->parent=b;
    b->right=a;

④刷新a,b节点的balance值。
⑤return b节点,让rebalance函数重新从b节点向上检查新树的平衡性。

两次旋转

Double Rotation
struct AVLNode* turnLeftThenRight(AVLNode* a){
    a->left = turnLeft(a->left);
    return turnRight(a);
}
struct AVLNode* turnRightThenLeft(AVLNode* a){
    a->right = turnRight(a->right);
    return turnLeft(a);
}

两次旋转函数要点:

  • 两次旋转函数触发条件:
    Turn Right Then Left: node->balance = 2 && node->left->balance = -1
    Turn Left Then Right:node->balance = -2 && node->right->balance = 1
  • 核心实现思路:对不平衡节点的不平衡方向的子节点先做旋转操作,再对不平衡节点做第二次旋转。

至此,旋转操作介绍完毕。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。 BST 二叉查找树(Binary...
    kanehe阅读 1,370评论 0 8
  • 树 树 树是一种重要的数据结构,通过链表建立一棵树: 用链表的结构构建树,就是使树的每一条分支都建立一个链表来存储...
    JnJnLee阅读 221评论 0 0
  • 1. 树的概念 一个树由节点组成,这些节点包含根节点,父节点,子节点,兄弟节点;没有任何一个节点的树称为空树;如果...
    HChase阅读 6,107评论 0 34
  • 什么是AVL树? AVL树,又称为平衡二叉树,它是一种特殊的二叉查找树(Binary Search Tree, B...
    wqbu阅读 827评论 0 0
  • 初春的楚德湖,大片湖面虽然仍被冰面覆盖,渔民凿出的小洞里却已蒸腾出热汽。湖岸白桦林已经吐露新绿,林下的苜蓿也已...
    麦克雷宗师阅读 710评论 0 1