Java底層實現 AVL 平衡二叉樹

AVL 平衡二叉樹


1、爲什麼要有AVL平衡二叉樹

  我們之前寫過文章叫做BST二分搜索樹,這種數據結構有一個缺點就是會退化爲鏈表形式,到這導致了我們的樹結構發揮不出來它應有的優勢。

退化結構

  從上圖可以發現如果按照順序進行添加操作,那麼二分搜索樹就會退化爲鏈表形式,樹結構也就失去了它的意義。

  AVL(Adelson-Velsky-Landis Tree)是以創造者的名字命名的。這種樹結構就是一種平衡二叉樹。它是最早的認爲可以自平衡的一種樹結構。

2、什麼是AVL平衡二叉樹

  我們之前寫過的滿二叉樹、完全二叉樹、線段樹、最大堆等等都是一種平衡樹的例子(葉子節點的高度差不會大於 1 )。其實上面的平衡二叉樹都是比較理想的例子。但是在AVL樹中維護的平衡二叉樹有所不同。

對於任意一個節點,左子樹和右子樹的高度差不能超過 1

上面的規則相對來說更加寬鬆一些。比如下面這張圖:

AVL結構

  這個結構並不滿足我們之前的平衡二叉樹的規則,根節點的左右子樹高度差不大於 1 。卻滿足我們上面的規則(對於任意節點)。

  對於時間複雜度方面,平衡二叉樹的高度和節點數量之間也是O(log (N) )。

3、AVL樹的基本實現

3.1、實現的方法

  首先,我們需要標註每個節點的高度信息平衡因子,平衡因子的計算就是左子樹的高度減去右子樹的高度的絕對值。這樣我們就可以依靠平衡因子來維護的AVL樹結構。

高度信息和平衡因子

3.2、構造函數

  我們首先需要構造一個節點信息作爲內部類,主要包含我們要存儲的內容,左右子樹的指針,還有高度信息。對於平衡因子我們可以使用左右子樹的差來進行計算。
節點信息:

private class Node{  //內部類
    public K key;
    public V value;
    public Node left, right;
    public int height;

    public Node(K key, V value){
        this.key = key;
        this.value = value;
        left = null;
        right = null;
        height = 1; //新節點高度爲 1
    }

構造函數:

private Node root;
private int size;

public AVLTree(){
    root = null;
    size = 0;
}

3.3、基本成員函數

  首先我們需要獲取樹結構的大小信息getSize()方法和判斷是否爲空isEmpty()方法。

程序實現:

public int getSize() {
    return size;
}

public boolean isEmpty() {
    return size == 0;
}

  因爲我們需要高度信息來判斷樹結構,所以引入getHeight()方法。

程序實現:

private int getHeight(Node node) {
    if (node == null)
        return 0;
    return node.height;
}

  爲了方便我們後續的操作,我們引入getNode()方法,通過索引key值來獲得節點。

程序實現:

private Node getNode(K key) {
    return getNode(root, key);
}

private Node getNode(Node node, K key) {
    if (node == null)
        return null;
    if (node.key.compareTo(key) < 0)
        return getNode(node.left, key);
    else if (node.key.compareTo(key) > 0)
        return getNode(node.right, key);
    else
        return node;
}

  在添加操作的時候,我們需要根據高度信息來獲得平衡因子,進而判斷樹結構是否滿足平衡樹的性質。

大於0:偏左,小於0:偏右

程序實現:

private int getBalanceFactor(Node node){
    if (node == null)
        return 0;
    return getHeight(node.left) - getHeight(node.right);
}

4、左旋轉和右旋轉

  我們知道原來平衡的樹變成不平衡會是在添加元素的時候,所以在添加元素的時候我們需要維護平衡性。下面就分四種情況分別討論:

4.1、LL 右旋轉

  有一種添加元素後的情況是這樣的。

RR

  可以認爲添加的元素在節點的左邊(L)的右邊(L)。
  在添加元素2,後會導致樹結構的平衡性破壞,對於這種情況的判斷就是當前節點的平衡因子大於1(向左偏斜),並且左節點的平衡因子大於0(向左偏斜),這樣就保證了這種情況的出現,向左偏斜。

if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)

  那麼我們就需要對上面的結構進行維護。進行右旋轉操作。

RR旋轉

  爲了不失一般性,我們引入更復雜的情況,如下圖:

RR旋轉

  按照平衡二叉樹的排序規則,對元素進行右旋轉(類似於 y 繞 x 右旋轉),相當於降低樹的高度。旋轉後仍然滿足二叉樹的排序規則。我們可以發現相對位置發生改變的就是節點 y 和 x 的右子樹 T3 。最後更新高度信息,高度發生變化的只有節點 x , y 。
程序實現:

private Node rightRotate(Node y) {
    Node x = y.left; //獲得旋轉中心
    Node T3 = x.right;

    x.right = y;  //進行旋轉操作
    y.left = T3;

    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;  //重新更新高度信息
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

    return x;
}

4.2、RR 左旋轉

  我們有了上面的右旋轉的概念,那麼左旋轉就變得簡單多了,左旋轉的發生的前提就是節點的平衡因子小於-1(偏右),右子樹的平衡因子小於0(偏右),也就是類似於下面的結構。
  可以認爲添加的元素在節點的右邊(R)的右邊(R)。

if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)

LL

下面就是對結構進行左旋轉操作,維護平衡性。

LL旋轉

  以x爲中心,進行左旋轉操作(類似於 y 繞 x 左旋轉),需要轉移的分別是 x 的左子樹 T2 和節點 y 。
程序實現:

private Node leftRotate(Node y) {

    Node x = y.right;
    Node T2 = x.left;

    x.left = y; //需要轉移的元素
    y.right = T2;

    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1; // 更新高度信息
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

    return x;
}

4.3、LR 左右旋轉

  這種情況就是添加元素在節點的左邊的右邊,左右旋轉指的是先進行左旋轉,後進行右旋轉。類似於下面這種情況。

LR

具體細節的旋轉如下:
  先進行左旋轉操作,結構就會變成我們之前LL的形式,然後對其進行右旋轉操作即可。

LR左右旋轉

4.4、RL 右左旋轉

  這種情況就是添加元素在節點的右邊的左邊,右左旋轉指的是先進行右旋轉,後進行左旋轉。類似於下面這種情況。

RL

具體細節的旋轉如下:
  先進行左旋轉操作,結構就會變成我們之前LL的形式,然後對其進行右旋轉操作即可。

RL右左旋轉

4.5、四種情況總結

  上面的四種情況完全包含了添加元素所需要的情況。

// LL
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
    return rightRotate(node);
// RR
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
    return leftRotate(node);
// LR
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0){
    node.left = leftRotate(node.left);
    return rightRotate(node);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0){
    node.right = rightRotate(node.right);
    return leftRotate(node);
}

5、增刪改查操作的實現

5.1、添加操縱

步驟:

  • 遞歸到底的時候添加元素,否則就更新元素
  • 更新每個節點的高度信息
  • 對結構進行平衡處理
private Node add(Node node, K key, V value) {
    /*
    * BST 的源代碼片段 ····
    */
    node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
    int balanceFactor = getBalanceFactor(node);
    // LL
    if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
        return rightRotate(node);
    // RR
    if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
        return leftRotate(node);
    // LR
    if (balanceFactor > 1 && getBalanceFactor(node.left) < 0){
        node.left = leftRotate(node.left);
        return rightRotate(node);
    }
    // RL
    if (balanceFactor < -1 && getBalanceFactor(node.right) > 0){
        node.right = rightRotate(node.right);
        return leftRotate(node);
    }
    return node;
}

5.2、刪除操作

  這裏的刪除操作,在原本BST二分搜索樹的基礎上進行改變,增加刪除元素後對節點進行平衡後的處理,同添加操作基本相同,這裏只對增加的程序片段進行展示。


private Node remove(Node node, K key) {
    /*
    * BST 的源代碼片段 ····
    */
    if (retNode == null)
        return null;
    retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right)); //更新高度值
    int balanceFactor = getBalanceFactor(retNode);
    // LL
    if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
        return rightRotate(retNode);
    // RR
    if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
        return leftRotate(retNode);
    // LR
    if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0){
        retNode.left = leftRotate(retNode.left);
        return rightRotate(retNode);
    }
    // RL
    if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0){
        retNode.right = rightRotate(retNode.right);
        return leftRotate(retNode);
    }
    return retNode;
}

5.3、查詢操作

  查詢操作主要包含查看元素是否在樹結構中,另一個就是通過key值查找對應的 value 值。兩者均是藉助getNode方法進行實現。
程序實現:

public boolean contains(K key) {
    return getNode(key) != null;
}

public V get(K key) {
    Node node = getNode(key);
    return node == null ? null : node.value;
}

5.4、更改操作

  更改操作也是藉助getNode方法進行更改。

public void set(K key, V newValue) {
    Node node = getNode(key);
    if (node != null)
        node.value = newValue;
    else
        throw new IllegalArgumentException(key + "doesn't exist");
}

最後

更多精彩內容,大家可以轉到我的主頁:曲怪曲怪的主頁

或者關注我的微信公衆號:TeaUrn

源碼地址:可在公衆號內回覆 數據結構與算法源碼 即可獲得。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章