一文读懂 AVL 树


背景


AVL 树是一棵平衡的二叉查找树,于 1962 年,G. M. Adelson-Velsky 和 E. M. Landis 在他们的论文《An algorithm for the organization of information》中发表。


所谓的平衡之意,就是树中任意一个结点下左右两个子树的高度差不超过 1。(本文对于树的高度约定为:空结点高度是 0,叶子结点高度是 1。)



那 AVL 树和普通的二叉查找树有何区别呢?如图,如果我们插入的是一组有序上升或下降的数据,则一棵普通的二叉查找树必然会退化成一个单链表,其查找效率就降为 O(n)。而 AVL 树因其平衡的限制,可以始终保持 O(logn) 的时间复杂度。


具体实现与代码分析


在我们进行完插入或删除操作后,很可能会导致某个结点失去平衡,那么我们就需要把失衡结点旋转一下,使其重新恢复平衡。


经过分析,不管是插入还是删除,它们都会有四种失衡的情况:左左失衡,右右失衡,左右失衡,右左失衡。因此每次遇到失衡时,我们只需判断一下是哪个失衡,再对其进行相对应的恢复平衡操作即可。


好,下面以插入操作为例,来看下这四种失衡的庐山真面目。(以下统一约定:红色结点为新插入结点,y 结点为失衡结点)


(1)左左失衡



所谓的左左,即 "失衡结点" 的左子树比右子树高 2,左孩子下的左子树比右子树高 1。


我们只需对 "以 y 为根的子树" 进行 "左左旋转 (ll_rotate)" 即可。一次旋转后,恢复平衡。


Node * AVL::ll_rotate(Node * y)

{

    Node * x = y->left;

    y->left = x->right;

    x->right = y;

    y->height = max(get_height(y->left), get_height(y->right)) + 1;

    x->height = max(get_height(x->left), get_height(x->right)) + 1;

    return x;

}


(2)右右失衡



所谓的右右,即 "失衡结点" 的右子树比左子树高 2,右孩子下的右子树比左子树高 1。


我们只需对 "以 y 为根的子树" 进行 "右右旋转 (rr_rotate)" 即可。一次旋转后,恢复平衡。


Node * AVL::rr_rotate(Node * y)

{

    Node * x = y->right;

    y->right = x->left;

    x->left = y;

    y->height = max(get_height(y->left), get_height(y->right)) + 1;

    x->height = max(get_height(x->left), get_height(x->right)) + 1;

    return x;

}


(3)左右失衡



所谓的左右,即 "失衡结点" 的左子树比右子树高 2,左孩子下的右子树比左子树高 1。


观察发现,若先对 "以 x 为根的子树" 进行 "右右旋转 (rr_rotate)",此时 "以 y 为根的子树" 恰好符合 "左左失衡",所以再进行一次 "左左旋转 (ll_rotate)"。两次旋转后,恢复平衡。


Node * AVL::lr_rotate(Node * y)

{

    Node * x = y->left;

    y->left = rr_rotate(x);

    return ll_rotate(y);

}


(4)右左失衡



所谓的右左,即 "失衡结点" 的右子树比左子树高 2,右孩子下的左子树比右子树高 1。


观察发现,若先对 "以 x 为根的子树" 进行 "左左旋转 (ll_rotate)",此时 "以 y 为根的子树" 恰好符合 "右右失衡",所以再进行一次 "右右旋转 (rr_rotate)"。两次旋转后,恢复平衡。


Node * AVL::rl_rotate(Node * y)

{

    Node * x = y->right;

    y->right = ll_rotate(x);

    return rr_rotate(y);

}


插入操作


插入成功后,在递归回溯时依次对经过的结点判断是否失衡,若失衡就需要对其进行对应的旋转操作使其恢复平衡,在这期间,原先作为一棵子树的根结点就会因为旋转被替换,因此设置insert_real( )返回的是新根结点,这样就可以实时更新根结点。


插入操作实现代码如下:


int AVL::get_height(Node * node)

{

    if (node == nullptr)

        return 0;

    return node->height;

}


int AVL::get_balance(Node * node)

{

    if (node == nullptr)

        return 0;

    return get_height(node->left) - get_height(node->right);

}


Node * AVL::insert_real(int key, Node * node)

{

    if (node == nullptr)

        return new Node(key);


    if (key < node->key)

        node->left = insert_real(key, node->left);

    else if (key > node->key)

        node->right = insert_real(key, node->right);

    else

        return node;


    node->height = max(get_height(node->left), get_height(node->right)) + 1;


    int balance = get_balance(node);


    // 左左失衡

    if (balance > 1 && get_balance(node->left) > 0)

        return ll_rotate(node);


    // 右右失衡

    if (balance < -1 && get_balance(node->right) < 0)

        return rr_rotate(node);


    // 左右失衡

    if (balance > 1 && get_balance(node->left) < 0)

        return lr_rotate(node);


    // 右左失衡

    if (balance < -1 && get_balance(node->right) > 0)

        return rl_rotate(node);


    return node;

}


void AVL::insert(int key)

{

    header->left = insert_real(key, header->left);

}


查找操作


Node * AVL::find_real(int key, Node * node)

{

    if (node == nullptr)

        return nullptr;


    if (key < node->key)

        return find_real(key, node->left);

    else if (key > node->key)

        return find_real(key, node->right);

    else

        return node;

}


Node * AVL::find(int key)

{

    return find_real(key, header->left);

}


删除操作


删除操作的四种失衡情况和插入操作一样,读者可以参考前文。下面是删除操作的实现代码:


Node * AVL::erase_real(int key, Node * node)

{

    if (node == nullptr)

        return node;


    if (key < node->key)

        node->left = erase_real(key, node->left);

    else if (key > node->key)

        node->right = erase_real(key, node->right);

    else

    {

        if (node->left && node->right)

        {

            // 找到后继结点

            Node * x = node->right;

            while (x->left)

                x = x->left;


            // 后继直接复制

            node->key = x->key;


            // 转化为删除后继

            node->right = erase_real(x->key, node->right);

        }

        else

        {

            Node * t = node;

            node = node->left ? node->left : node->right;

            delete t;

            if (node == nullptr)

                return nullptr;

        }

    }


    node->height = max(get_height(node->left), get_height(node->right)) + 1;


    int balance = get_balance(node);


    // 左左失衡

    if (balance > 1 && get_balance(node->left) >= 0) // 需要加等号

        return ll_rotate(node);


    // 右右失衡

    if (balance < -1 && get_balance(node->right) <= 0) // 需要加等号

        return rr_rotate(node);


    // 左右失衡

    if (balance > 1 && get_balance(node->left) < 0)

        return lr_rotate(node);


    // 右左失衡

    if (balance < -1 && get_balance(node->right) > 0)

        return rl_rotate(node);


    return node;

}


void AVL::erase(int key)

{

    header->left = erase_real(key, header->left);

}


完整代码


/**

 *

 * author : 刘毅(Limer)

 * date   : 2017-08-17

 * mode   : C++

 */


#include <iostream>

#include <algorithm>


using namespace std;


struct Node

{

    int key;

    int height;

    Node * left;

    Node * right;

    Node(int key = 0)

    {

        this->key = key;

        this->height = 1;

        this->left = this->right = nullptr;

    }

};


class AVL

{

private:

    Node * header;

private:

    Node * ll_rotate(Node * y);

    Node * rr_rotate(Node * y);

    Node * lr_rotate(Node * y);

    Node * rl_rotate(Node * y);

    void destroy(Node * node);

    int get_height(Node * node);

    int get_balance(Node * node);

    Node * insert_real(int key, Node * node);

    Node * find_real(int key, Node * node);

    Node * erase_real(int key, Node * node);

    void in_order(Node * node);

public:

    AVL();

    ~AVL();

    void insert(int key);

    Node * find(int key);

    void erase(int key);

    void print();

};


Node * AVL::ll_rotate(Node * y)

{

    Node * x = y->left;

    y->left = x->right;

    x->right = y;


    y->height = max(get_height(y->left), get_height(y->right)) + 1;

    x->height = max(get_height(x->left), get_height(x->right)) + 1;


    return x;

}


Node * AVL::rr_rotate(Node * y)

{

    Node * x = y->right;

    y->right = x->left;

    x->left = y;


    y->height = max(get_height(y->left), get_height(y->right)) + 1;

    x->height = max(get_height(x->left), get_height(x->right)) + 1;


    return x;

}


Node * AVL::lr_rotate(Node * y)

{

    Node * x = y->left;

    y->left = rr_rotate(x);

    return ll_rotate(y);

}


Node * AVL::rl_rotate(Node * y)

{

    Node * x = y->right;

    y->right = ll_rotate(x);

    return rr_rotate(y);

}


void AVL::destroy(Node * node)

{

    if (node == nullptr)

        return;

    destroy(node->left);

    destroy(node->right);

    delete node;

}


int AVL::get_height(Node * node)

{

    if (node == nullptr)

        return 0;

    return node->height;

}


int AVL::get_balance(Node * node)

{

    if (node == nullptr)

        return 0;

    return get_height(node->left) - get_height(node->right);

}


Node * AVL::insert_real(int key, Node * node)

{

    if (node == nullptr)

        return new Node(key);


    if (key < node->key)

        node->left = insert_real(key, node->left);

    else if (key > node->key)

        node->right = insert_real(key, node->right);

    else

        return node;


    node->height = max(get_height(node->left), get_height(node->right)) + 1;


    int balance = get_balance(node);


    // 左左失衡

    if (balance > 1 && get_balance(node->left) > 0)

        return ll_rotate(node);


    // 右右失衡

    if (balance < -1 && get_balance(node->right) < 0)

        return rr_rotate(node);


    // 左右失衡

    if (balance > 1 && get_balance(node->left) < 0)

        return lr_rotate(node);


    // 右左失衡

    if (balance < -1 && get_balance(node->right) > 0)

        return rl_rotate(node);


    return node;

}


Node * AVL::find_real(int key, Node * node)

{

    if (node == nullptr)

        return nullptr;


    if (key < node->key)

        return find_real(key, node->left);

    else if (key > node->key)

        return find_real(key, node->right);

    else

        return node;

}


Node * AVL::erase_real(int key, Node * node)

{

    if (node == nullptr)

        return node;


    if (key < node->key)

        node->left = erase_real(key, node->left);

    else if (key > node->key)

        node->right = erase_real(key, node->right);

    else

    {

        if (node->left && node->right)

        {

            // 找到后继结点

            Node * x = node->right;

            while (x->left)

                x = x->left;


            // 后继直接复制

            node->key = x->key;


            // 转化为删除后继

            node->right = erase_real(x->key, node->right);

        }

        else

        {

            Node * t = node;

            node = node->left ? node->left : node->right;

            delete t;

            if (node == nullptr)

                return nullptr;

        }

    }


    node->height = max(get_height(node->left), get_height(node->right)) + 1;


    int balance = get_balance(node);


    // 左左失衡

    if (balance > 1 && get_balance(node->left) >= 0) // 需要加等号

        return ll_rotate(node);


    // 右右失衡

    if (balance < -1 && get_balance(node->right) <= 0) // 需要加等号

        return rr_rotate(node);


    // 左右失衡

    if (balance > 1 && get_balance(node->left) < 0)

        return lr_rotate(node);


    // 右左失衡

    if (balance < -1 && get_balance(node->right) > 0)

        return rl_rotate(node);


    return node;

}


void AVL::in_order(Node * node)

{

    if (node == nullptr)

        return;


    in_order(node->left);

    cout << node->key << " ";

    in_order(node->right);

}


AVL::AVL()

{

    header = new Node(0);

}


AVL::~AVL()

{

    destroy(header->left);

    delete header;

    header = nullptr;

}


void AVL::insert(int key)

{

    header->left = insert_real(key, header->left);

}


Node * AVL::find(int key)

{

    return find_real(key, header->left);

}


void AVL::erase(int key)

{

    header->left = erase_real(key, header->left);

}


void AVL::print()

{

    in_order(header->left);

    cout << endl;

}


int main()

{

    AVL avl;


    // test "insert"

    avl.insert(7);

    avl.insert(2);

    avl.insert(1); avl.insert(1);

    avl.insert(5);

    avl.insert(3);

    avl.insert(6);

    avl.insert(4);

    avl.insert(9);

    avl.insert(8);

    avl.insert(11); avl.insert(11);

    avl.insert(10);

    avl.insert(12);

    avl.print(); // 1 2 3 4 5 6 7 8 9 10 11 12


    // test "find"

    Node * p = nullptr;

    cout << ((p = avl.find(2)) ? p->key : -1) << endl;   //  2

    cout << ((p = avl.find(100)) ? p->key : -1) << endl; // -1


    // test "erase"

    avl.erase(1);

    avl.print(); // 2 3 4 5 6 7 8 9 10 11 12

    avl.erase(9);

    avl.print(); // 2 3 4 5 6 7 8 10 11 12

    avl.erase(11);

    avl.print(); // 2 3 4 5 6 7 8 10 12


    return 0;

}


起初构造的 AVL 树为下图:



总结


和二叉查找树相比,AVL 树的特点是时间复杂度更稳定,但缺点也是很明显的。


插入操作中,至多需要一次恢复平衡操作,递归回溯的量级为 O(logn)。有一点需要我们注意,在对第一个失衡结点进行恢复平衡后,递归回溯就应该立即停止(因为失衡结点的父亲及其祖先们肯定都是处于平衡状态的)。


但让 "递归的回溯" 中途停止,不好实现,所以我上面的编码程序都不可避免的会继续回溯,直到整棵树的根结点,而这些回溯都是没有必要的。(谢谢 LLL 的提醒,若在结点中增设父亲结点,就可以解决递归回溯的问题)


删除操作中,若存在失衡,则至少需要一次恢复平衡操作,递归回溯的量级亦为 O(logn)。与插入操作不同,当对第一个失衡结点恢复平衡后,它的父亲或者是它的祖先们也可能是非平衡的(见下图,删除 1),所以删除操作的回溯很有必要。



没有参照物对比的探讨是没有意义的,所以此文就止于此吧,有兴趣的朋友可以看下我后面《红黑树》及《AVL 树与红黑树的对比》的文章。


参考文献


维基百科. AVL 树.


GeeksforGeeks. AVL Tree | Set 1 (Insertion).


GeeksforGeeks. AVL Tree | Set 2 (Deletion).


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304557.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

欧几里得范数_从范数到正则化

范数是一个在数学领域中常用的工具&#xff0c;同时也是学习机器学习原理中经常碰到的概念。本文将从范数的定义出发&#xff0c;逐步带你理解其在机器学习中的应用。首先需要明确的是&#xff0c;范数是一个函数&#xff0c;在机器学习中我们通常用它来衡量向量的大小。 范数定…

IdentityServer4之持久化很顺手的事

前言原计划打算在春节期间多分享几篇技术文章的&#xff0c;但到最后一篇也没出&#xff0c;偷懒了吗&#xff1f;算是吧&#xff0c;过程是这样的&#xff1a;每次拿出电脑&#xff0c;在孩姥姥家的院子总有阳光沐浴&#xff0c;看不清屏幕&#xff0c;回屋又有点冷(在强行找理…

手写体识别代码_Python识别图片中的文字

一、前言不知道大家有没有遇到过这样的问题&#xff0c;就是在某个软件或者某个网页里面有一篇文章&#xff0c;你非常喜欢&#xff0c;但是不能复制。或者像百度文档一样&#xff0c;只能复制一部分&#xff0c;这个时候我们就会选择截图保存。但是当我们想用到里面的文字时&a…

递推与储存,是动态规划的关键

小智最近由于项目需要&#xff0c;经常要接触到一些规划类的问题。那今天就给大家讲一讲旅行商问题及其解法吧。旅行商问题&#xff0c;即TSP问题&#xff08;Travelling Salesman Problem&#xff09;。问题是&#xff0c;有一个旅行商人要拜访n个城市&#xff0c;每个城市只能…

dotnet core TargetFramework 解析顺序探索

dotnet core TargetFramework 解析顺序测试Intro现在 dotnet 的 TargetFramework 越来越多&#xff0c;抛开 .NET Framework 不谈&#xff0c;如果一个类库支持多个 TargetFramework 应用实际运行的时候会使用哪个版本的 API 呢&#xff0c;之前一直都是想当然的自以为是了&…

大数据时代,掌握数据分析需要做到这几点

这些年来&#xff0c;随着进入大数据时代&#xff0c;各行各业均有一个词频频被提到&#xff0c;那就是数据分析。那么数据分析究竟是什么呢&#xff1f;数据分析就是指用适当的统计分析方法对收集来的大量数据进行处理分析&#xff0c;提取有用信息并形成结论&#xff0c;从而…

93.7%的程序员!竟然都不知道Redis为什么默认16个数据库?

背景在实际项目中redis常被应用于做缓存&#xff0c;分布式锁/消息队列等。但是在搭建配置好redis服务器后很多朋友应该会发现和有这样的疑问&#xff0c;为什么redis默认建立了16个数据库&#xff0c;16个数据库的由来redis是一个字典结构的存储服务器&#xff0c;一个redis实…

“一边熬夜一边求不要猝死”,90后养生朋克指南,条条扎心!

随着一批又一批的90后步入中年秃头、失眠、衰老...健康的压力如影如随是时候开始养生朋克了当代青年&#xff1a;养生朋克指南养生朋克一边作死一边自救的养生方式比如一边熬夜一边涂贵价护肤品用最贵的眼霜 熬最长的夜心理活动经常是&#xff1a;一边熬夜一边祈祷自己不要猝死…

万级 K8S 集群背后,etcd 如何保持稳定性?

这几年&#xff0c;随着 Kubernetes 成为容器编排领域霸主&#xff0c;etcd 越来越火&#xff0c;GitHub star 已超过 34.2K。这与它的应用场景广泛密不可分&#xff0c;从服务发现到分布式锁&#xff0c;从配置存储到分布式协调&#xff0c;可以说&#xff0c;etcd 已成为云原…

qdialog 只有点击才能获得焦点_4 个突破点,让你的 Banner 点击率提升10倍

双 11 刚过&#xff0c;双 12又来了每年这个时候作为一名设计师&#xff0c;真的很难...老板压着做电商 banner还有很多人指指戳戳让你改稿好不容易按照别人的想法过稿结果banner 的点击率还不高其实&#xff0c;只需要稍微调整视觉重点你的电商banner 就会变得变得更吸引人&am…

穿背心的老院士,86岁,重病,还在敲代码,单手!

昨天有幸看到了一个视频&#xff0c;视频中&#xff0c;一位老先生穿着朴素的白背心&#xff0c;伏在桌上&#xff0c;对着电脑&#xff0c;一手按着写满密密麻麻数学公式的本子&#xff0c;另一只手仅用单指吃力又缓慢地按着键盘。老先生全神贯注地研究他是 “背心院士” 高伯…

百度广告点击软件_结束了,百度 “毒瘤” 广告!

“ 通过本文&#xff0c;你可以获取一款上网必备的插件&#xff0c;让你摆脱各种"毒瘤"广告。”大家天天逛网页&#xff0c;最恼人的也就是广告了吧。尤其是百度搜索时。01—毒瘤广告有多愁&#xff1f;百度广告有多可怕&#xff0c;小哥哥给你演示一下。百度搜“智齿…

java跳转画面后画面白了_如何跳转指定页面后再次跳转到另一页面或原来的页面...

c#图解教程第5版数万读者认可的新99.33元(需用券)去购买 >这里可以采用redirect以get形式向指定页面发出get请求&#xff0c;指定页面完成后会再次跳转到redirect后边指定的页面(注意&#xff1a;这里的redirect只是一个自定义参数&#xff0c;并不是自带的&#xff0c;名称…

【基础数学知识】带你理解泰勒展开式本质

推荐阅读时间&#xff1a;5min~8min主要内容&#xff1a;更好的理解&#xff0c;并且记忆泰勒展开式我们学习泰勒展开&#xff0c;本质上就是为了在某个点附近&#xff0c;用多项式函数取近似其他函数。可能有些童鞋就要问了&#xff0c;既然有一个函数了&#xff0c;为什么还需…

C#使用NPOI进行word的读写

目录一、简介1、操作Word的类库&#xff1a;二、简单使用1、XWPFDocument类的实例化2、设置页面的大小3、段落处理4、表格处理5、页眉页脚处理三、综合示例四、参考一、简介1、操作Word的类库&#xff1a;二、简单使用1、XWPFDocument类的实例化该类的实例对应一个word文档XWPF…

weblogic修改控制台ip_「Weblogic学习」Weblogic知识要点之JNDI/JTA编程开发

给它一个名字&#xff0c;它就能提供你要的服务&#xff0c;JNDI就是这么地任性。想任性的霸占数据&#xff1f;JTA为你把风。这期&#xff0c;谈谈JNDI和JTA的简单介绍&#xff0c;一起看看吧……JNDI编程JNDI(Java Naming and Directory Interface)是SUN公司提供的一种标准的…

java不适合开发cv吗_JavaCV开发环境的配置

一.安装opencv2.4.4、测试运行OpenCV-2.4.4.exe&#xff0c;就是解压&#xff0c;把它解压到一个你喜欢的地方 ——1.1 路径最好不要太深&#xff0c;点啊点的最讨厌了2. 添加 ***\OpenCV2.4.4\build\x64\vc11\bin; 到环境变量的PATH后面 ——2.1 ***是你放OpenCV2.4.4的地方…

NA-NP-IE系列实验5:配置文件的备份和IOS 的备份

实验5:配置文件的备份和IOS 的备份<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />1. 实验目的通过本实验&#xff0c;读者可以掌握如下技能&#xff1a;&#xff08;1&#xff09; 熟悉TFTP 服务器的使用&#xff08;2&#xf…

浅析 Sunday 算法

背景Sunday 算法是 Daniel M.Sunday 于 1990 年提出的字符串模式匹配。其效率在匹配随机的字符串时比其他匹配算法还要更快。Sunday 算法的实现可比 KMP&#xff0c;BM 的实现容易太多。算法过程假定主串为 "HERE IS A SIMPLE EXAMPLE"&#xff0c;模式串为 "EX…

.NET 6 Preview 1发布!

喜欢就关注我们吧&#xff01;.NET 6 首个预览版已发布&#xff0c;官方表示 .NET 6 的核心目标是完成从 .NET 5 开始的 .NET 统一计划的最后部分。此版本还将对 .NET 整体进行重大改进&#xff0c;包括针对云、桌面和移动应用程序的改进。.NET 6 计划于2021年11月正式发布&…