[路飞]前端算法——数据结构篇(三、树): 强迫症的杰作AVL树

520 阅读3分钟

计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

前言

前端算法系列是我对算法学习的一个记录, 主要从常见算法数据结构算法思维常用技巧几个方面剖析学习算法知识, 通过LeetCode平台实现刻意练习, 通过掘金和B站的输出来实践费曼学习法, 我会在后续不断更新优质内容并同步更新到掘金、B站和Github, 以记录学习算法的完整过程, 欢迎大家多多交流点赞收藏, 让我们共同进步, daydayup👊

目录地址:目录篇

相关代码地址: Github

相关视频地址: 哔哩哔哩-百日算法系列

(一)、AVL树-基础

上一篇: [路飞]前端算法——数据结构篇(三、树): 二叉搜索树

一、缘起

在上一节中我们学习了 二叉搜索树, 知道了它的主要性质是:

  • 左子树永远小于根节点
  • 右子树永远大于根节点

那么我们来看一下假如使用下列的数据初始化二叉搜索树是什么样子的?

// [1, 2, 3, 4, 5]

我们知道二叉树搜索树有两个重要的作用, 其中一个就是它能将我们的搜索的时间复杂度降低为 O(logn), 但是, 当我们的初识对象是一个有序的数组时, 二叉搜索树退化为链表的状态, 同时也就失去了其 O(logn) 的时间复杂度

为了防止二叉查找树退化成链表的状态、AVL树诞生了

AVL树的本身还是一个二叉搜索树,只不过它在每一次执行插入操作的时候都会进行一次调整, 使得它的左右子树高度相差不超过1, 也就是说AVL树就是一个带着平衡功能的二叉搜索树

二、性质

1、左子树 < 根节点

2、右子树 > 根节点

3、|左子树高度 - 右子树高度| <= 1 ❗️❗️❗️

三、优点

除了二叉树拥有的优点外, AVL树最大的优点就是对树高做了限制、使其不会退化链表

四、调整

1、AVL树-左旋

image.png

2、AVL树-右旋

image.png

3、AVL树-失衡类型

红色三角表示高度超出限制的子树

image.png

(二)、AVL树-实现

// 左旋
function rotateLeft(root) {
    let temp = root.right
    root.right = temp.left
    temp.left = root
    updateHeight(root)
    updateHeight(temp)
    return temp
}

// 右旋
function rotateRight(root) {
    let temp = root.left
    root.left = temp.right
    temp.right = root
    updateHeight(root)
    updateHeight(temp)
    return temp
}

function maintain(root) {
    // 不需要调整
    if (Math.abs(root.left.h - root.right.h) <= 1) return root
    if (root.left.h > root.right) {
        // 左子树更高, 失衡条件是L
        if (root.left.right.h > root.left.left.h) {
            // LR型失衡, 先左旋, 再右旋
            root.left = rotateLeft(root.right)
        }
        // LL型失衡, 右旋
        root = rotateRight(root)
    } else {
        // 右子树更高, 失衡条件是R
        if (root.right.left.h > root.right.right.h) {
            // RL型失衡, 先右旋, 再左旋
            root.right = rotateRight(root.right)
        }
        // RR型失衡, 左旋
        root = rotateLeft(root)
    }
    return root
}

// 然后需要在原二叉树的代码中修改插入和删除时返回的root, 具体操作如下

这是我们上一章中二叉搜索树的代码

// 构造函数
function Node(val, left, right) {
    this.val = val || 0
    this.left = left || null
    this.right = right || null
    this.height = 0 // 树高
}

// 创建
Node.prototype.getNewNode = function (val) {
    let p = new Node(val)
    p.height = 1
    return p
}

// 插入
Node.prototype.insert = function (root, target) {
    if (root === null) return this.getNewNode(target)
    if (root.val === target) return root
    if (root.val > target) {
        root.left = this.insert(root.left, target)
    } else {
        root.right = this.insert(root.right, target)
    }
    updateHeight(root)
    // return root
    // 这里进行了修改
    return maintain(root)
}

// 删除
Node.prototype.earse = function (root, target) {
    if (root === null) return root
    if (root.val > target) {
        root.left = this.earse(root.left, target)
    } else if (root.val < target) {
        root.right = this.earse(root.right, target)
    } else {
        if (!root.left && !root.right) { // 出度为0
            return null
        } else if (!root.left || !root.right) { // 出度为1
            return root.left || root.right
        } else { // 出度为2
            // 获取前驱、替换、删除前驱
            let pre = this.getPre(root.left)
            root.val = pre.val
            // fix: 在左子树中删除前驱节点
            root.left = this.earse(root.left, root.val)
        }
    }
    // return root
    // 这里进行了修改
    return maintain(root)
}

// 清理
Node.prototype.clear = function (root) {
    if (root === null) return
    this.clear(root.left)
    this.clear(root.right)
    return
}

// 获取前驱
Node.prototype.getPre = function (root) {
    let temp = root
    while(temp.right) {
        temp = temp.right
    }
    return temp
}

// 更新树高
function updateHeight(root) {
    root.height = Math.max(root.left.height, root.right.height) + 1
}