數據結構與算法(十三)平衡二叉樹之AVL樹

本文主要包括如下內容:node

  1. 平衡二叉樹的概念
  2. AVL樹
  3. 插入操做保持AVL樹的平衡
  4. 刪除操做保持AVL樹的平衡

平衡二叉樹的概念

爲何須要平衡二叉樹?git

經過前面的 二分搜索樹(Binary Search Tree)BinarySearchTree的時間複雜度分析 的介紹咱們知道,二分搜索樹的性能跟樹的高度(h)有關係 :github

h 爲二分搜索樹的高度,那麼高度 h 和二分搜索樹節點數 n 的關係是什麼呢?bash

分析下滿的二叉樹的狀況就知道了節點數量和二叉樹高度的的關係了:性能

層數 該層的節點數
0層 1
1層 2
2層 4
3層 8
4層 16
h-1層 2^(h-1)

那麼一個h層的滿二叉樹總共有多少節點呢?就是每層的元素個數相加便可:ui

n = 2^0+2^1+2^3+2^4+...+2^(h-1) = 2^h - 1spa

用對數表示就是:h = log(n+1).net

用大O表示法就是: O(h) = O(log n)code

上面是基於 滿二叉樹 的狀況,因此二分搜索樹最好狀況的時間複雜度爲 O(log n)cdn

可是根據二分搜索樹的性質知道,在最壞的狀況二分搜索樹會退化成鏈表,那麼二分搜索樹的在最壞的狀況的時間複雜度爲 O(n).

二分搜索樹的最好狀況的 O(log n) 和 最壞狀況的 O(n) 是個什麼概念呢?下面用一個表格對比下:

對比下 nlog(n) 之間的差距

n log(n) 差距
16 4 4 倍
1024 10 100倍
100w 20 5萬倍

隨着數量不斷的加大,它們之間性能的差距不斷的兩極分化。

這個時候就須要一個可以平衡的二分搜索樹,就算在最壞的狀況也能保證二分搜索樹的性能保持在 O(log n)

那麼什麼是平衡二叉樹,平衡二叉樹 也稱 平衡二分搜索樹(Balanced Binary Tree)是一種結構平衡的二分搜索樹

平衡二叉樹由二分搜索樹發展而來,在二分搜索樹的基礎上平衡二叉樹須要知足兩個條件:

  1. 它的左右兩個子樹的高度差的絕對值不超過1
  2. 左右兩個子樹都是一棵平衡二叉樹

常見的平衡二叉搜索樹有:

  1. AVL樹
  2. 紅黑樹
  3. Treap

下面咱們介紹下出現最先的平衡二叉樹 AVL樹

AVL樹

AVL樹 是由 G. M. Adelson- V elsky 和 E. M. Landis於1962年提出。AVL樹是最先的平衡二叉樹。

AVL樹維護自身的平衡涉及到兩個概念:

  1. 節點的高度
  2. 節點的平衡因子

節點的高度就是從根節點到該節點的邊的總和

節點的 平衡因子 是左子樹的高度減去它的右子樹的高度

帶有平衡因子一、0或 -1的節點被認爲是平衡的,由於它的左右子樹高度差不超過 1

以下面一顆 AVL樹:

這裏寫圖片描述

上圖的AVL樹中,節點最大的平衡因子是1,因此它是一顆平衡二叉樹。

一顆平衡二叉樹的平衡性被打破確定是在插入或者刪除的時候,下面就來看如何在插入和刪除的時候保持AVL樹的平衡性。

插入操做保持AVL樹的平衡

插入的元素在不平衡節點左側的左側,簡稱 LL

以下面一顆AVL樹,在插入節點 5 後 節點 15 的平衡因子變成了 2,樹的平衡性被打破:

這裏寫圖片描述

這種狀況咱們稱之爲 插入的元素在不平衡節點左側的左側 簡稱 LL

遇到該狀況須要對不平衡的節點進行右旋轉:

這裏寫圖片描述

通用狀況以下:

這裏寫圖片描述

右旋轉代碼:

private Node<K, V> rotateRight(Node<K, V> node) {
    Node<K, V> nodeLeft = node.left;
    Node<K, V> lRight = nodeLeft.right;
    //右旋轉
    nodeLeft.right = node;
    node.left = lRight;

    //維護節點高度
    node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
    nodeLeft.height = 1 + Math.max(getHeight(nodeLeft.left), getHeight(nodeLeft.right));

    return nodeLeft;
}

複製代碼

插入的元素在不平衡節點右側的右側,簡稱 RR

這種狀況也就是上一個狀況的鏡像。它須要對不平衡的節點向左旋轉:

這裏寫圖片描述

左旋轉代碼:

private Node<K, V> rotateLeft(Node<K, V> node) {
    Node<K, V> nodeRight = node.right;
    Node<K, V> rLeft = nodeRight.left;
    //左旋轉
    nodeRight.left = node;
    node.right = rLeft;

    //維護節點高度
    node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
    nodeRight.height = 1 + Math.max(getHeight(nodeRight.left), getHeight(nodeRight.right));

    return nodeRight;
}

複製代碼

插入的元素在不平衡節點的左側的右側,簡稱 LR

插入的元素在不平衡節點的左側的右側,以下圖所示:

這裏寫圖片描述

這個時候就不能單純的對節點 12 右旋轉,11和12都比10要大。這種狀況須要兩次旋轉:

這裏寫圖片描述

插入的元素在不平衡節點的右側的左側,簡稱 RL

插入的元素在不平衡節點的右側的左側,以下圖所示:

這裏寫圖片描述

插入操做維護AVL平衡性的相關代碼:

public void add(K key, V value) {
    root = _add(root, key, value);
}
    
private Node<K, V> _add(Node<K, V> node, K key, V value) {
    if (node == null) {
        size++;
        return new Node<>(key, value);
    }

    if (key.compareTo(node.key) < 0)
        node.left = _add(node.left, key, value);
    else if (key.compareTo(node.key) > 0)
        node.right = _add(node.right, key, value);
    else //若是已經存在,修改對應value的值
        node.value = value;

    //維護node的高度
    //左右子樹最高的高度+1
    node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));

    //獲取節點的平衡因子
    int balanceFactor = getBalanceFactor(node);

    // 右旋轉
    // 左子樹比右子樹要高超過了1,說明當前節點的平衡被打破
    // 且新添加的節點是在左子樹的左子樹的左側

    //LL
    if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
        return rotateRight(node);

    //RR
    if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
        return rotateLeft(node);

    //LR
    if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
        node.left = rotateLeft(node.left);//轉化LL形式
        return rotateRight(node);
    }

    //RL
    if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
        node.right = rotateRight(node.right);//轉化成RR
        return rotateLeft(node);
    }

    return node;
}

複製代碼

刪除操做保持AVL樹的平衡

刪除操做和插入操做須要保持平衡的狀況基本是同樣的,代碼以下所示:

private Node<K, V> remove(Node<K, V> node, K key) {
    if (node == null) {
        return null;
    }

    Node<K, V> retNode = null;

    //若是要刪除的節點小於當前節點,繼續查詢其左子樹
    if (key.compareTo(node.key) < 0) {
        node.left = remove(node.left, key);
        retNode = node;
    }
    //若是要刪除的節點大於當前節點,繼續查詢其右子樹
    else if (key.compareTo(node.key) > 0) {
        node.right = remove(node.right, key);
        retNode = node;
    }

    //要刪除的節點就是當前的節點
    else {
        //若是要刪除節點的左子樹爲空
        if (node.left == null) {
            Node<K, V> rightNode = node.right;
            node.right = null;
            size--;
            retNode = rightNode;
        }

        //若是要刪除節點的右子樹爲空
        else if (node.right == null) {
            Node<K, V> leftNode = node.left;
            node.left = null;
            size--;
            retNode = leftNode;
        }
        //=======若是要刪除的節點左右子樹都不爲空
        else {
            //找到要刪除節點的後繼,也就是右子樹的最小值
            Node<K, V> successor = getMin(node.right);
            successor.right = remove(node.right, successor.key);
            successor.left = node.left;
            node.left = node.right = null;

            retNode = successor;
        }
    }

    //若是刪除的節點是葉子節點
    if (retNode == null) {
        return null;
    }


    //獲得retNode以後,維護平衡性

    //維護node的高度
    //左右子樹最高的高度+1
    retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));

    //獲取節點的平衡因子
    int balanceFactor = getBalanceFactor(retNode);

    // 右旋轉
    // 左子樹比右子樹要高超過了1,說明當前節點的平衡被打破
    // 且新添加的節點是在左子樹的左子樹的左側

    //LL
    if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
        return rotateRight(retNode);

    //RR
    if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
        return rotateLeft(retNode);

    //LR
    if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
        retNode.left = rotateLeft(retNode.left);//轉化LL形式
        return rotateRight(retNode);
    }

    //RL
    if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
        retNode.right = rotateRight(retNode.right);//轉化成RR
        return rotateLeft(retNode);
    }

    return retNode;
}


複製代碼

本文相關源代碼github

下面是個人公衆號,乾貨文章不錯過,有須要的能夠關注下,很是感謝:

個人公衆號
相關文章
相關標籤/搜索