看動畫學演算法之:平衡二叉搜尋樹AVL Tree

flydean發表於2021-10-15

簡介

平衡二叉搜尋樹是一種特殊的二叉搜尋樹。為什麼會有平衡二叉搜尋樹呢?

考慮一下二叉搜尋樹的特殊情況,如果一個二叉搜尋樹所有的節點都是右節點,那麼這個二叉搜尋樹將會退化成為連結串列。從而導致搜尋的時間複雜度變為O(n),其中n是二叉搜尋樹的節點個數。

而平衡二叉搜尋樹正是為了解決這個問題而產生的,它通過限制樹的高度,從而將時間複雜度降低為O(logn)。

AVL的特性

在討論AVL的特性之前,我們先介紹一個概念叫做平衡因子,平衡因子表示的是左子樹和右子樹的高度差。

如果平衡因子=0,表示這是一個完全平衡二叉樹。

如果平衡因子=1,那麼這棵樹就是平衡二叉樹AVL。

也就是是說AVL的平衡因子不能夠大於1。

先看一個AVL的例子:

總結一下,AVL首先是一個二叉搜尋樹,然後又是一個二叉平衡樹。

AVL的構建

有了AVL的特性之後,我們看下AVL是怎麼構建的。

public class AVLTree {

    //根節點
    Node root;

    class Node {
        int data; //節點的資料
        int height; //節點的高度
        Node left;
        Node right;

        public Node(int data) {
            this.data = data;
            left = right = null;
        }
    }

同樣的,AVL也是由各個節點構成的,每個節點擁有data,left和right幾個屬性。

因為是二叉平衡樹,節點是否平衡還跟節點的高度有關,所以我們還需要定義一個height作為節點的高度。

在來兩個輔助的方法,一個是獲取給定的節點高度:

//獲取給定節點的高度
    int height(Node node) {
        if (node == null)
            return 0;
        return node.height;
    }

和獲取平衡因子:

//獲取平衡因子
    int getBalance(Node node) {
        if (node == null)
            return 0;
        return height(node.left) - height(node.right);
    }

AVL的搜尋

AVL的搜尋和二叉搜尋樹的搜尋方式是一致的。

先看一個直觀的例子,怎麼在AVL中搜尋到7這個節點:

搜尋的基本步驟是:

  1. 從根節點15出發,比較根節點和搜尋值的大小
  2. 如果搜尋值小於節點值,那麼遞迴搜尋左側樹
  3. 如果搜尋值大於節點值,那麼遞迴搜尋右側樹
  4. 如果節點匹配,則直接返回即可。

相應的java程式碼如下:

//搜尋方法,預設從根節點搜尋
    public Node search(int data){
        return search(root,data);
    }

    //遞迴搜尋節點
    private Node search(Node node, int data)
    {
        // 如果節點匹配,則返回節點
        if (node==null || node.data==data)
            return node;

        // 節點資料大於要搜尋的資料,則繼續搜尋左邊節點
        if (node.data > data)
            return search(node.left, data);

        // 如果節點資料小於要搜素的資料,則繼續搜尋右邊節點
        return search(node.right, data);
    }

AVL的插入

AVL的插入和BST的插入是一樣的,不過插入之後有可能會導致樹不再平衡,所以我們需要做一個再平衡的步驟。

看一個直觀的動畫:

插入的邏輯是這樣的:

  1. 從根節點出發,比較節點資料和要插入的資料
  2. 如果要插入的資料小於節點資料,則遞迴左子樹插入
  3. 如果要插入的資料大於節點資料,則遞迴右子樹插入
  4. 如果根節點為空,則插入當前資料作為根節點

插入資料之後,我們需要做再平衡。

再平衡的邏輯是這樣的:

  1. 從插入的節點向上找出第一個未平衡的節點,這個節點我們記為z
  2. 對z為根節點的子樹進行旋轉,得到一個平衡樹。

根據以z為根節點的樹的不同,我們有四種旋轉方式:

  • left-left:

如果是left left的樹,那麼進行一次右旋就夠了。

右旋的步驟是怎麼樣的呢?

  1. 找到z節點的左節點y
  2. 將y作為旋轉後的根節點
  3. z作為y的右節點
  4. y的右節點作為z的左節點
  5. 更新z的高度

相應的程式碼如下:

Node rightRotate(Node node) {
        Node x = node.left;
        Node y = x.right;

        // 右旋
        x.right = node;
        node.left = y;

        // 更新node和x的高度
        node.height = max(height(node.left), height(node.right)) + 1;
        x.height = max(height(x.left), height(x.right)) + 1;

        // 返回新的x節點
        return x;
    }
  • right-right:

如果是right-right形式的樹,需要經過一次左旋:

左旋的步驟正好和右旋的步驟相反:

  1. 找到z節點的右節點y
  2. 將y作為旋轉後的根節點
  3. z作為y的左節點
  4. y的左節點作為z的右節點
  5. 更新z的高度

相應的程式碼如下:

//左旋
    Node leftRotate(Node node) {
        Node x = node.right;
        Node y = x.left;

        //左旋操作
        x.left = node;
        node.right = y;

        // 更新node和x的高度
        node.height = max(height(node.left), height(node.right)) + 1;
        x.height = max(height(x.left), height(x.right)) + 1;

        // 返回新的x節點
        return x;
    }
  • left-right:

如果是left right的情況,需要先進行一次左旋將樹轉變成left left格式,然後再進行一次右旋,得到最終結果。

  • right-left:

如果是right left格式,需要先進行一次右旋,轉換成為right right格式,然後再進行一次左旋即可。

現在問題來了,怎麼判斷一個樹到底是哪種格式呢?我們可以通過獲取平衡因子和新插入的資料比較來判斷:

  1. 如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較新插入的data和node.left.data的大小

    如果data < node.left.data,表示是left left的情況,只需要一次右旋即可

    如果data > node.left.data,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋

  2. 如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較新插入的data和node.right.data的大小
    如果data > node.right.data,表示是Right Right的情況,只需要一次左旋即可

    如果data < node.left.data,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋

插入節點的最終程式碼如下:

//插入新節點,從root開始
    public void insert(int data){
        root=insert(root, data);
    }

    //遍歷插入新節點
    Node insert(Node node, int data) {

        //先按照普通的BST方法插入節點
        if (node == null)
            return (new Node(data));

        if (data < node.data)
            node.left = insert(node.left, data);
        else if (data > node.data)
            node.right = insert(node.right, data);
        else
            return node;

        //更新節點的高度
        node.height = max(height(node.left), height(node.right)) + 1;

        //判斷節點是否平衡
        int balance = getBalance(node);

        //節點不平衡有四種情況
        //1.如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較新插入的data和node.left.data的大小
        //如果data < node.left.data,表示是left left的情況,只需要一次右旋即可
        //如果data > node.left.data,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋
        //2.如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較新插入的data和node.right.data的大小
        //如果data > node.right.data,表示是Right Right的情況,只需要一次左旋即可
        //如果data < node.left.data,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋

        //left left
        if (balance > 1 && data < node.left.data)
            return rightRotate(node);

        // Right Right
        if (balance < -1 && data > node.right.data)
            return leftRotate(node);

        // Left Right
        if (balance > 1 && data > node.left.data) {
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }

        // Right Left
        if (balance < -1 && data < node.right.data) {
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }

        //返回插入後的節點
        return node;
    }

AVL的刪除

AVL的刪除和插入類似。

首先按照普通的BST刪除,然後也需要做再平衡。

看一個直觀的動畫:

刪除之後,節點再平衡也有4種情況:

  1. 如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較左節點的平衡因子

    如果左節點的平衡因子>=0,表示是left left的情況,只需要一次右旋即可

    如果左節點的平衡因<0,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋

  2. 如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較右節點的平衡因子

    如果右節點的平衡因子<=0,表示是Right Right的情況,只需要一次左旋即可

    如果右節點的平衡因子>0,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋

相應的程式碼如下:

Node delete(Node node, int data)
    {
        //Step 1. 普通BST節點刪除
        // 如果節點為空,直接返回
        if (node == null)
            return node;

        // 如果值小於當前節點,那麼繼續左節點刪除
        if (data < node.data)
            node.left = delete(node.left, data);

        //如果值大於當前節點,那麼繼續右節點刪除
        else if (data > node.data)
            node.right = delete(node.right, data);

       //如果值相同,那麼就是要刪除的節點
        else
        {
            // 如果是單邊節點的情況
            if ((node.left == null) || (node.right == null))
            {
                Node temp = null;
                if (temp == node.left)
                    temp = node.right;
                else
                    temp = node.left;

                //沒有子節點的情況
                if (temp == null)
                {
                    node = null;
                }
                else // 單邊節點的情況
                    node = temp;
            }
            else
            {  //非單邊節點的情況
                //拿到右側節點的最小值
                Node temp = minValueNode(node.right);
                //將最小值作為當前的節點值
                node.data = temp.data;
                // 將該值從右側節點刪除
                node.right = delete(node.right, temp.data);
            }
        }

        // 如果節點為空,直接返回
        if (node == null)
            return node;

        // step 2: 更新當前節點的高度
        node.height = max(height(node.left), height(node.right)) + 1;

        // step 3: 獲取當前節點的平衡因子
        int balance = getBalance(node);

        // 如果節點不再平衡,那麼有4種情況
        //1.如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較左節點的平衡因子
        //如果左節點的平衡因子>=0,表示是left left的情況,只需要一次右旋即可
        //如果左節點的平衡因<0,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋
        //2.如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較右節點的平衡因子
        //如果右節點的平衡因子<=0,表示是Right Right的情況,只需要一次左旋即可
        //如果右節點的平衡因子>0,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋
        // Left Left Case
        if (balance > 1 && getBalance(node.left) >= 0)
            return rightRotate(node);

        // Left Right Case
        if (balance > 1 && getBalance(node.left) < 0)
        {
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }

        // Right Right Case
        if (balance < -1 && getBalance(node.right) <= 0)
            return leftRotate(node);

        // Right Left Case
        if (balance < -1 && getBalance(node.right) > 0)
        {
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }
        return node;
    }

本文的程式碼地址:

learn-algorithm

本文收錄於 http://www.flydean.com/11-algorithm-avl-tree/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章