看動畫學算法之:平衡二叉搜索樹AVL Tree 簡介 AVL的特性 AVL的構建 AVL的搜索 AVL的插入 AVL的刪除

簡介

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

考慮一下二叉搜索樹的特殊情況,如果一個二叉搜索樹所有的節點都是右節點,那麼這個二叉搜索樹將會退化成爲鏈表。從而導致搜索的時間複雜度變爲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/

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

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

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