【愚公系列】2023年12月 数据结构(九)-AVL树

举报
愚公搬代码 发表于 2023/12/30 23:59:14 2023/12/30
【摘要】 数据结构是计算机科学中的一个重要概念,它描述了数据之间的组织方式和关系,以及对这些数据的访问和操作。常见的数据结构有:数组、链表、栈、队列、哈希表、树、堆和图。

🏆 作者简介,愚公搬代码
🏆《头衔》:华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,阿里云专家博主,腾讯云优秀博主,掘金优秀博主,51CTO博客专家等。
🏆《近期荣誉》:2022年CSDN博客之星TOP2,2022年华为云十佳博主等。
🏆《博客内容》:.NET、Java、Python、Go、Node、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、大数据、人工智能、U3D游戏、小程序等相关领域知识。
🏆🎉欢迎 👍点赞✍评论⭐收藏

🚀前言

数据结构是计算机科学中的一个重要概念,它描述了数据之间的组织方式和关系,以及对这些数据的访问和操作。常见的数据结构有:数组、链表、栈、队列、哈希表、树、堆和图。

  1. 数组(Array):是一种线性数据结构,它将一组具有相同类型的数据元素存储在一起,并为每个元素分配一个唯一的索引。数组的特点是具有随机访问的能力。

  2. 链表(Linked List):也是一种线性数据结构,它由一系列的节点组成,每个节点包含数据和指向下一个节点的引用。链表的特点是可以动态地插入或删除节点,但访问某个节点时需要从头开始遍历。

  3. 栈(Stack):是一种后进先出(LIFO)的数据结构,它只能在栈顶进行插入和删除操作。栈通常用于实现递归算法、表达式求值和内存管理等场景。

  4. 队列(Queue):是一种先进先出(FIFO)的数据结构,它可以在队尾插入元素,在队头删除元素。队列通常用于数据的缓存、消息队列和网络通信等场景。

  5. 哈希表(Hash Table):也称为散列表,它是一种根据关键字直接访问数据的数据结构。哈希表通常由数组和散列函数组成,可以在常数时间内进行插入、删除和查找操作。

  6. 树(Tree):是一种非线性数据结构,它由一系列的节点组成,每个节点可以有若干个子节点。树的特点是可以动态地插入或删除节点,常见的树结构包括二叉树、平衡树和搜索树等。

  7. 堆(Heap):是一种特殊的树结构,它通常用于实现优先队列和堆排序等算法。堆分为最大堆和最小堆,最大堆的每个节点的值都大于等于其子节点的值,最小堆则相反。

  8. 图(Graph):是一种由节点和边组成的非线性数据结构,它可以用来表示各种实体之间的关系,如社交网络、路线图和电路图等。图的遍历和最短路径算法是常见的图算法。

🚀一、AVL树

🔎1.基本思想

AVL树的基本思想是通过对树的平衡因子进行调整,使得树保持平衡。平衡因子指的是左右子树的高度差,AVL树要求任何一个节点的左右子树高度差不能超过1。如果一个节点的平衡因子超过1,就需要通过旋转操作来调整树的结构,使之重新达到平衡。

AVL树的插入和删除操作都会引起树的不平衡,需要通过旋转操作来重新平衡。当节点插入到AVL树中时,需要从插入节点的父节点开始,一直到根节点,检查每个节点的平衡因子是否超过1,如果有,则需要旋转该节点,直到根节点。删除操作同理。由于AVL树保持平衡,因此任何操作的时间复杂度都是O(log n)。

在这里插入图片描述

🔎2.AVL树常见术语

AVL树的节点高度是该节点到其最远叶子节点的路径长度,即从该节点往下到最底层节点的路径长度。对于空节点,其高度为-1。

节点的平衡因子是其左子树的高度减去右子树的高度,因此平衡因子只可能是-1、0或1。如果一个节点的平衡因子的绝对值超过1,则该节点需要进行旋转操作,以保持AVL树的平衡性。

🔎3.AVL树旋转

🦋3.1 右旋

AVL树右旋的具体步骤如下:

  1. 以需要右旋的节点为基准,设该节点为x,x的左子节点为y。

  2. 将y的右子节点B(如果有的话)作为x的左子节点,将y作为x的父节点。

  3. 将x的父节点p(如果存在)指向y,将y的父节点指向p。

  4. 如果p存在,根据x是p的左子节点还是右子节点,将p的左/右子节点指向y。

  5. 最后,更新x和y的高度。

右旋的示意图如下:

           p                              p
           |                              |
           x                              y
          / \      right rotate         / \
         y   C     ------------>       A   x
        / \                                  \
       A   B                                  B

其中,p为需要右旋的节点的父节点,x为需要右旋的节点,y为x的左子节点。

在这里插入图片描述

🦋3.2 左旋

AVL树左旋是一种平衡操作,其步骤如下:

  1. 确定需要进行左旋的节点为节点A。
  2. 将A节点的右子节点B提升为根节点,并将A节点的父节点P与B节点连接起来。
  3. 将B节点的左子节点C连接到A节点的右子节点上。
  4. 如果C节点不为空,则将C节点的父节点改为A节点。
  5. 计算A节点和B节点的高度差,更新它们的高度属性。
  6. 返回修改后的AVL树。

在这里插入图片描述

🦋3.3 先左旋后右旋

AVL树左旋后右旋的实现步骤如下:

  1. 找到需要右旋的结点,记为节点A。

  2. 找到A的左子结点B,并把B的右子结点C的指针指向A的左子结点。

  3. 把A的左子结点指针指向B的右子结点。

  4. 把B的右子结点指针指向A。

  5. 计算A和B节点的平衡因子。

  6. 更新A和B节点的平衡因子。

  7. 返回B节点。

左旋后,根据平衡因子,我们可以得出旋转后各个节点的平衡因子以及新的根节点。然后,我们需要按照步骤进行右旋操作。右旋操作的实现步骤与左旋操作类似,只是方向相反。

在这里插入图片描述

🦋3.4 先右旋后左旋

先来了解一下什么是AVL树:AVL树是一种自平衡二叉搜索树,它的每个节点的左子树和右子树的高度差至多为1,这就保证了它的查找、插入和删除操作的时间复杂度都是O(log n)。

当AVL树的某个节点的左右子树高度差超过1时,就需要进行旋转操作来保持平衡,而先右旋后左旋就是一种旋转操作。

具体步骤如下:

  1. 找到不平衡的节点。

  2. 如果不平衡节点的左子树比右子树高,则进行右旋操作,否则进行左旋操作。

  3. 对于右旋操作,需要按照以下步骤进行:

    3.1 将不平衡节点的左子节点设为新的根节点。

    3.2 将新的根节点的右子树设为原不平衡节点的左子树。

    3.3 将原不平衡节点设为新的根节点的右子树。

  4. 对于左旋操作,需要按照以下步骤进行:

    4.1 将不平衡节点的右子节点设为新的根节点。

    4.2 将新的根节点的左子树设为原不平衡节点的右子树。

    4.3 将原不平衡节点设为新的根节点的左子树。

  5. 更新每个节点的高度,并根据新的高度确定是否需要继续进行旋转操作。

  6. 最终得到平衡的AVL树。

需要注意的是,先右旋后左旋并不一定是所有情况下都适用的,而是取决于不平衡节点的左右子树的高度差和节点的左右子节点的高度差。因此,在实际应用中,需要根据具体情况来选择合适的旋转策略。

在这里插入图片描述

🦋3.5 旋转的选择

AVL树的旋转分为左旋和右旋,旋转的目的是为了让AVL树保持平衡。

选择左旋还是右旋,需要根据树的实际情况来决定。如果某个节点的左子树比右子树高,则选择右旋;如果某个节点的右子树比左子树高,则选择左旋。

具体选择方法如下:

  1. 如果某个节点的左子树比右子树高2个以上,说明该节点的左子树过深,需要进行右旋来平衡。右旋的中心是该节点,旋转后将该节点的左子树右旋,使得该节点的左子树高度降低,右子树高度增加,达到平衡。

  2. 如果某个节点的右子树比左子树高2个以上,说明该节点的右子树过深,需要进行左旋来平衡。左旋的中心是该节点,旋转后将该节点的右子树左旋,使得该节点的右子树高度降低,左子树高度增加,达到平衡。

需要注意的是,某些情况可能需要进行双旋转,即先左旋再右旋或先右旋再左旋,才能达到平衡。

在这里插入图片描述

🔎4.AVL树常用操作

/**
 * File: avl_tree.cs
 * Created Time: 2022-12-23
 * Author: haptear (haptear@hotmail.com)
 */

namespace hello_algo.chapter_tree;

/* AVL 树 */
class AVLTree {
    public TreeNode? root; // 根节点

    /* 获取节点高度 */
    public int height(TreeNode? node) {
        // 空节点高度为 -1 ,叶节点高度为 0
        return node == null ? -1 : node.height;
    }

    /* 更新节点高度 */
    private void updateHeight(TreeNode node) {
        // 节点高度等于最高子树高度 + 1
        node.height = Math.Max(height(node.left), height(node.right)) + 1;
    }

    /* 获取平衡因子 */
    public int balanceFactor(TreeNode? node) {
        // 空节点平衡因子为 0
        if (node == null) return 0;
        // 节点平衡因子 = 左子树高度 - 右子树高度
        return height(node.left) - height(node.right);
    }

    /* 右旋操作 */
    TreeNode? rightRotate(TreeNode? node) {
        TreeNode? child = node.left;
        TreeNode? grandChild = child?.right;
        // 以 child 为原点,将 node 向右旋转
        child.right = node;
        node.left = grandChild;
        // 更新节点高度
        updateHeight(node);
        updateHeight(child);
        // 返回旋转后子树的根节点
        return child;
    }

    /* 左旋操作 */
    TreeNode? leftRotate(TreeNode? node) {
        TreeNode? child = node.right;
        TreeNode? grandChild = child?.left;
        // 以 child 为原点,将 node 向左旋转
        child.left = node;
        node.right = grandChild;
        // 更新节点高度
        updateHeight(node);
        updateHeight(child);
        // 返回旋转后子树的根节点
        return child;
    }

    /* 执行旋转操作,使该子树重新恢复平衡 */
    TreeNode? rotate(TreeNode? node) {
        // 获取节点 node 的平衡因子
        int balanceFactorInt = balanceFactor(node);
        // 左偏树
        if (balanceFactorInt > 1) {
            if (balanceFactor(node.left) >= 0) {
                // 右旋
                return rightRotate(node);
            } else {
                // 先左旋后右旋
                node.left = leftRotate(node?.left);
                return rightRotate(node);
            }
        }
        // 右偏树
        if (balanceFactorInt < -1) {
            if (balanceFactor(node.right) <= 0) {
                // 左旋
                return leftRotate(node);
            } else {
                // 先右旋后左旋
                node.right = rightRotate(node?.right);
                return leftRotate(node);
            }
        }
        // 平衡树,无须旋转,直接返回
        return node;
    }

    /* 插入节点 */
    public void insert(int val) {
        root = insertHelper(root, val);
    }

    /* 递归插入节点(辅助方法) */
    private TreeNode? insertHelper(TreeNode? node, int val) {
        if (node == null) return new TreeNode(val);
        /* 1. 查找插入位置,并插入节点 */
        if (val < node.val)
            node.left = insertHelper(node.left, val);
        else if (val > node.val)
            node.right = insertHelper(node.right, val);
        else
            return node;     // 重复节点不插入,直接返回
        updateHeight(node);  // 更新节点高度
        /* 2. 执行旋转操作,使该子树重新恢复平衡 */
        node = rotate(node);
        // 返回子树的根节点
        return node;
    }

    /* 删除节点 */
    public void remove(int val) {
        root = removeHelper(root, val);
    }

    /* 递归删除节点(辅助方法) */
    private TreeNode? removeHelper(TreeNode? node, int val) {
        if (node == null) return null;
        /* 1. 查找节点,并删除之 */
        if (val < node.val)
            node.left = removeHelper(node.left, val);
        else if (val > node.val)
            node.right = removeHelper(node.right, val);
        else {
            if (node.left == null || node.right == null) {
                TreeNode? child = node.left != null ? node.left : node.right;
                // 子节点数量 = 0 ,直接删除 node 并返回
                if (child == null)
                    return null;
                // 子节点数量 = 1 ,直接删除 node
                else
                    node = child;
            } else {
                // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
                TreeNode? temp = node.right;
                while (temp.left != null) {
                    temp = temp.left;
                }
                node.right = removeHelper(node.right, temp.val);
                node.val = temp.val;
            }
        }
        updateHeight(node);  // 更新节点高度
        /* 2. 执行旋转操作,使该子树重新恢复平衡 */
        node = rotate(node);
        // 返回子树的根节点
        return node;
    }

    /* 查找节点 */
    public TreeNode? search(int val) {
        TreeNode? cur = root;
        // 循环查找,越过叶节点后跳出
        while (cur != null) {
            // 目标节点在 cur 的右子树中
            if (cur.val < val)
                cur = cur.right;
            // 目标节点在 cur 的左子树中
            else if (cur.val > val)
                cur = cur.left;
            // 找到目标节点,跳出循环
            else
                break;
        }
        // 返回目标节点
        return cur;
    }
}

public class avl_tree {
    static void testInsert(AVLTree tree, int val) {
        tree.insert(val);
        Console.WriteLine("\n插入节点 " + val + " 后,AVL 树为");
        PrintUtil.PrintTree(tree.root);
    }

    static void testRemove(AVLTree tree, int val) {
        tree.remove(val);
        Console.WriteLine("\n删除节点 " + val + " 后,AVL 树为");
        PrintUtil.PrintTree(tree.root);
    }

    [Test]
    public void Test() {
        /* 初始化空 AVL 树 */
        AVLTree avlTree = new AVLTree();

        /* 插入节点 */
        // 请关注插入节点后,AVL 树是如何保持平衡的
        testInsert(avlTree, 1);
        testInsert(avlTree, 2);
        testInsert(avlTree, 3);
        testInsert(avlTree, 4);
        testInsert(avlTree, 5);
        testInsert(avlTree, 8);
        testInsert(avlTree, 7);
        testInsert(avlTree, 9);
        testInsert(avlTree, 10);
        testInsert(avlTree, 6);

        /* 插入重复节点 */
        testInsert(avlTree, 7);

        /* 删除节点 */
        // 请关注删除节点后,AVL 树是如何保持平衡的
        testRemove(avlTree, 8); // 删除度为 0 的节点
        testRemove(avlTree, 5); // 删除度为 1 的节点
        testRemove(avlTree, 4); // 删除度为 2 的节点

        /* 查询节点 */
        TreeNode? node = avlTree.search(7);
        Console.WriteLine("\n查找到的节点对象为 " + node + ",节点值 = " + node?.val);
    }
}

🔎5.优点和缺点

优点:

  1. AVL树保证了每个节点的左右子树高度差不超过1,因此查询效率较高,时间复杂度为O(logn)。

  2. AVL树是一种自平衡二叉搜索树,插入和删除操作后会自动进行树的平衡操作,因此插入和删除的效率也较高。

  3. AVL树具有高度平衡性,能够保证树的高度始终在log n范围内,因而具有稳定的时间复杂度。

缺点:

  1. 由于需要维护平衡,需要对每个节点进行旋转操作,导致插入和删除的开销较大。

  2. 实现复杂度较高,难以理解和维护,需要对插入、删除和平衡操作进行复杂的判断。

  3. AVL树的空间需求比普通二叉搜索树稍微大一些。在某些应用场景下(如内存受限的环境)可能会受到限制。

🔎6.应用场景

AVL树可以应用于需要高效的数据插入和查询的场景。例如,在数据库中,AVL树常常被用来存储索引数据,以便快速地查找和访问表中的数据;在编译器中,AVL树通常被用来实现符号表,以便快速地查找和访问变量和函数等标识符信息;在路由算法中,AVL树常常被用来维护路由表,以便快速地查找和访问路由信息。总之,需要高效地进行数据插入和查询的场景都可以考虑使用AVL树。


🚀感谢:给读者的一封信

亲爱的读者,

我在这篇文章中投入了大量的心血和时间,希望为您提供有价值的内容。这篇文章包含了深入的研究和个人经验,我相信这些信息对您非常有帮助。

如果您觉得这篇文章对您有所帮助,我诚恳地请求您考虑赞赏1元钱的支持。这个金额不会对您的财务状况造成负担,但它会对我继续创作高质量的内容产生积极的影响。

我之所以写这篇文章,是因为我热爱分享有用的知识和见解。您的支持将帮助我继续这个使命,也鼓励我花更多的时间和精力创作更多有价值的内容。

如果您愿意支持我的创作,请扫描下面二维码,您的支持将不胜感激。同时,如果您有任何反馈或建议,也欢迎与我分享。

在这里插入图片描述

再次感谢您的阅读和支持!

最诚挚的问候, “愚公搬代码”

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。