數據結構 - 從二叉搜索樹說到AVL樹(二)之AVL樹的操做與詳解(Java)

  寫在前:我在儘量的寫一篇能比較清晰且完整的講完整個AVL樹操做的文章,全部文字以及例圖都是我一筆一劃寫出來的。因爲AVL樹的操做包含了查找,刪除,插入操做,除了一些規律以外,一些處理細節,好比旋轉操做,失衡時候的調整步驟等,除了死記硬背沒有別的辦法,因此我建議讀者能夠拿起筆,集中精神,跟着思路一口氣看完,由於一些操做麻煩,蜻蜓點水的閱讀勢必會多花沒必要要的精力,不如一次性掌握起來,這也是我學習過程的一些體會,先結合例圖理解過程,能夠不看代碼,省得增長理解負擔。講完我會把完整代碼分享出來,同時也但願該篇文章能給予你一些幫助,多謝支持。html

  本篇將要講的是平衡二叉樹,簡稱BBT(Balanced Binary Tree),其中的一種 -- AVL樹。 AVL樹得名於它的發明者 G.M. Adelson-Velsky 和 E.M. Landis,他們在 1962 年的論文 "An algorithm for the organization of information" 中發表了它。node

  爲何要有平衡二叉樹呢, 一樣都是二叉搜索樹,使用如下三棵樹搜索元素 1 有什麼區別數據結構

  

  能夠發現第三棵樹的搜索效率是最高的,任何一個節點的左子樹和右子樹都差很少高(或者同樣高),爲了表示這個屬性,AVL樹的作法是給每個節點增長一個屬性,叫「平衡因子」(balance factor),它的值等於當前節點的左子樹高度減去右子樹高度,當一個節點的平衡因子爲-1或者1時,咱們說這個節點是傾斜的,若是平衡因子爲0,則這個節點是平衡的,傾斜和平衡都是AVL樹的正常狀態,但若是平衡因子小於-1或者大於1,則表明失去了平衡,此時要經過旋轉來調整樹的平衡。ide

  旋轉分爲兩種 —— 右旋和左旋:學習

  右旋:測試

  

  作法是以A節點爲軸,節點A的左子樹指向其左孩子B的右子樹2,而後節點B的左子樹指向節點A,而後本來節點A的父節點R對應的子樹指向節點B,其餘節點不做變化,這邊便完成了左旋操做。this

  相應的代碼以下:以A點爲軸進行右旋spa

    private void rotateRight(TreeNode pivot) {
        TreeNode leftChild = pivot.getLeft();
        TreeNode grandChildRight = leftChild.getRight();
        TreeNode parent = pivot.getParent();
        if (null == parent) {
            this.root = leftChild;
        } else if (pivot == parent.getLeft()) {
            parent.setLeft(leftChild);
        } else {
            parent.setRight(leftChild);
        }
        leftChild.setParent(parent);

        pivot.setLeft(grandChildRight);
        if (null != grandChildRight) {
            grandChildRight.setParent(pivot);
        }

        leftChild.setRight(pivot);
        pivot.setParent(leftChild);
    }
View Code

 

 

  左旋:code

  

  左旋的操做跟右旋同樣,可是結構是相反的,以節點A爲軸,節點A的右子樹指向其有孩子B的左子樹2,而後節點B的左子樹指向節點A,再使原節點A的父節點對應的子樹指向節點B,其餘節點不作改變。orm

  相應的代碼以下:以A點爲軸進行左旋

    private void rotateLeft(TreeNode pivot) {
        TreeNode rightChild = pivot.getRight();
        TreeNode grandChildLeft = rightChild.getLeft();
        TreeNode parent = pivot.getParent();
        if (null == parent) {
            // pivot node is root
            this.root = rightChild;
        } else if(pivot == parent.getLeft()) {
            parent.setLeft(rightChild);
        } else {
            parent.setRight(rightChild);
        }
        rightChild.setParent(parent);

        pivot.setRight(grandChildLeft);
        if (null != grandChildLeft) {
            grandChildLeft.setParent(pivot);
        }

        rightChild.setLeft(pivot);
        pivot.setParent(rightChild);
    }
View Code

 

  剛開始可能難吃透旋轉的含義,能夠拿筆紙而後本身寫例子多畫幾遍就清晰了,這樣旋轉的目的是既不破壞一棵二叉搜索樹的性質,又能使軸節點的平衡因子對應的下降或者升高到正常狀態。

  

  AVL樹的操做:

  AVL樹節點的數據結構,比普通的二叉搜索樹多了一個平衡因子屬性,如下爲樹節點的數據結構

public class TreeNode {
    private int elem;
    private TreeNode left, right;
    private TreeNode parent;
    private int balanceFactor;
    public TreeNode(int elem) {
        this.elem = elem;
        this.balanceFactor = 0;
    }
}

 

  1、查找

  AVL樹做爲一棵二叉搜索樹,其查找操做沒有任何區別,可參考 數據結構 - 從二叉搜索樹說到AVL樹(一)之二叉搜索樹的操做與詳解(Java)

  2、插入

  根據普通二叉搜索樹的插入步驟插入一個新的節點以後,對整棵樹受到影響的節點更新其平衡因子,更新的規則在討論完失衡狀況後進行講解,失衡狀況主要分爲如下四種,以及調節方式以下,調節以後須要更新某些節點的平衡因子,也是至關重要的部分:

  *第一個L(R)表示當前節點的左(右)子樹失去平衡,即軸節點的平衡因子爲2(-2),第二個L(R)表示軸節點的左(右)子樹是向左(右)傾斜的,即左(右)子樹的平衡因子爲1(-1)。

  1. LL 型

  以失去平衡的節點爲parent,parent左節點爲left,處理方式是以parent爲軸作右旋操做,旋轉操做沒有技巧,多寫幾遍直到看到一個樹就能在腦海想到旋轉以後的樣子。

  平衡因子調整:

  旋轉以後作平衡因子調整,LL型的調整規則是把parent和left節點的平衡因子設置爲0,其餘節點保持不變。

   

 

  代碼以下:

    private void rotateLLFix(TreeNode parent) {
        TreeNode left = parent.getLeft();
        rotateRight(parent);
        // update the balance factor
        parent.setBalanceFactor(0);
        left.setBalanceFactor(0);
    }
View Code

 

  2. LR 型

  LR型的旋轉操做麻煩一些,以軸爲parent,parent的左節點爲left,left的右節點爲grandchild,如下圖第一行三個圖爲例,若是直接對着三個圖作右轉操做,會發現這麼作並不能使這課子樹回覆平衡。必須先把LR型轉化爲LL型,作法是先以left爲軸作左轉操做,獲得下圖第二行的結果展現,接着再以parent爲軸作右轉操做,這樣才使這棵子樹回覆到平衡狀態。

  平衡因子調整:

    LR型的平衡因子調整根據原grandchild的平衡因子分爲三種狀況:

    ① 若是grandchild原平衡因子爲+1,則parent的平衡因子設置爲-1,left的平衡因子設置爲0

    ② 若是grandchild原平衡因子爲0, 則parent和left的平衡因子設置爲0

    ③ 若是grandchild原平衡因子爲-1,則parent的平衡因子設置爲0, left的平衡因子設置爲+1

    以上三種狀況的grandchild平衡因子皆設置爲0, 其餘節點沒有變化

  代碼以下:

    private void rotateLRFix(TreeNode parent) {
        TreeNode left = parent.getLeft();
        TreeNode grandchild = parent.getRight();
        rotateLeft(left);
        rotateRight(parent);
        // update the balance factor
        if (0 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(0);
            left.setBalanceFactor(0);
        } else if (-1 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(0);
            left.setBalanceFactor(-1);
        } else {
            left.setBalanceFactor(0);
            parent.setBalanceFactor(-1);
        }
        grandchild.setBalanceFactor(0);
    }
View Code

 

  3. RR 型

  RR型的調整規則與LL型的調整規則鏡面對稱的, parent設置不變,把parent的右節點設置爲right,而後以parent爲軸作左旋操做

  平衡因子調整:

  RR型的調整規則是把parent和right節點的平衡因子設置爲0,其餘節點保持不變。

  

  代碼以下:

    private void rotateRRFix(TreeNode parent) {
        TreeNode right = parent.getRight();
        rotateLeft(parent);
        parent.setBalanceFactor(0);
        right.setBalanceFactor(0);
    }
View Code

 

  4. RL 型

   RL型和LR型一樣道理也是鏡面對稱的,把parent的右節點設置爲right,right節點的左節點設置爲grandchild,處理方式是先以right節點爲軸作右旋操做,使之轉化爲RR型,而後再以parent位軸作左旋操做,便可恢復平衡。

  平衡因子調整:

  一樣根據grandchild的平衡因子分爲三種狀況

    ① 若是grandchild原平衡因子爲+1,則parent的平衡因子設置爲0,right的平衡因子設置爲-1

    ② 若是grandchild原平衡因子爲0, 則parent和right的平衡因子設置爲0

    ③ 若是grandchild原平衡因子爲-1,則parent的平衡因子設置爲+1, right的平衡因子設置爲0

    以上三種狀況的grandchild平衡因子皆設置爲0, 其餘節點沒有變化

  

  代碼以下:

    private void rotateRLFix(TreeNode parent) {
        TreeNode right = parent.getRight();
        TreeNode grandchild = right.getLeft();
        rotateRight(right);
        rotateLeft(parent);
        if (0 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(0);
            right.setBalanceFactor(0);
        } else if (-1 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(1);
            right.setBalanceFactor(0);
        } else {
            parent.setBalanceFactor(0);
            right.setBalanceFactor(-1);
        }
        grandchild.setBalanceFactor(0);
    }
View Code

 

  討論完上面幾種調節失衡狀況的細節,接下來討論怎麼在插入節點時候找到這個須要調節的節點。

  1. 插入一個新節點到某個節點(父節點)的左子樹,此時父節點的平衡因子在原來的基礎上加上1,然後分爲兩種狀況:

    ① 父節點此時的平衡因子爲0。則說明原本父節點的平衡因子爲-1,父節點如下的子樹高度並無發生變化,縱觀整棵樹,插入這個新節點除了影響了父節點的平衡因子以外,對其餘節點均沒有影響,此時,只須要更新父節點的平衡因子以後,插入操做結束

    ② 父節點此時的平衡因子爲1,則說明以父節點如下的這課子樹高度增長了1,影響到了從這個新節點開始向上到根節點路徑的全部節點平衡因子,須要從節點開始向上調整全部節點的平衡因子知道根節點,若是路徑上有節點的平衡因子調整後爲2或者-2,則根據具體狀況對節點平衡調整,使該節點的平衡因子回覆到正常狀態,插入操做結束。這樣調整以後爲何對其餘都不會有影響呢,由於原本這種狀況下新的節點會使這個子樹高度增長1,因此經過旋轉調整讓這顆子樹的高度又減小了1,因此對其餘節點來講,這個子樹高度沒有變化,因此便沒有影響了。在這個特色上須要和下文即將說到的刪除節點後調整平衡狀況做出對比。

  2. 插入一個新節點到某個節點(父節點)的右子樹,此時父節點的平衡因子在原來的基礎上減去1,然後分爲兩種狀況:

    ① 父節點此時的平衡因子爲0。通過上面插入左子樹的狀況討論,此處再也不贅述。

    ② 父節點此時的平衡因子爲-1,同理上溯到根節點,上溯路徑若是有節點的平衡因子爲-2或者2,則對該節點進行平衡調整,一樣再也不繼續影響該節點以上路徑節點,插入操做結束。

 

  詳細代碼以下:

    private boolean insertNode(TreeNode parent, TreeNode node) {
        if (parent.getElem() == node.getElem()) {
            return false;
        } else if (parent.getElem() > node.getElem()) {
            if (null == parent.getLeft()) {
                parent.setLeft(node);
                node.setParent(parent);
                insertFixUp(node);
                return true;
            } else {
                return insertNode(parent.getLeft(), node);
            }
        } else {
            if (null == parent.getRight()) {
                parent.setRight(node);
                node.setParent(parent);
                insertFixUp(node);
                return true;
            } else {
                return insertNode(parent.getRight(), node);
            }
        }
    }
View Code

  其中的 insertFixUp(node); 指從node開始上溯到根節點調整該路徑的節點平衡因子,並作必要的旋轉操做,代碼以下:

    private void insertFixUp(TreeNode node) {
        TreeNode parent = node.getParent();
        while (null != parent) { // track to root when parent is not null
            if (node == parent.getLeft()) {
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            } else {
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            }
            if (0 == parent.getBalanceFactor()) {
                break;
            }
            if (-2 == parent.getBalanceFactor() || 2 == parent.getBalanceFactor()) {
                if (2 == parent.getBalanceFactor()) {
                    TreeNode left = parent.getLeft();
                    if (-1 == left.getBalanceFactor()) {
                        rotateLRFix(parent);
                    } else {
                        rotateLLFix(parent);
                    }
                } else {
                    TreeNode right = parent.getRight();
                    if (1 == right.getBalanceFactor()) {
                        rotateRLFix(parent);
                    } else {
                        rotateRRFix(parent);
                    }
                }
                break;
            }
            node = parent;
            parent = node.getParent();
        }
    }
View Code

 

  3、刪除

  最後一個操做,操做方式也跟普通的BST同樣,可參考: 數據結構 - 從二叉搜索樹說到AVL樹(一)之二叉搜索樹的操做與詳解(Java)(刪除的節點只有左子樹,刪除的節點只有右子樹,刪除的節點是葉子,若是刪除的節點同時擁有左右子樹也能夠轉化爲以上三種狀況)。

  不一樣的是把該節點刪除以後的平衡調整操做。

  討論刪除節點後的狀況:

  1. 刪除的節點是其父節點的左子樹,用刪除節點的左子樹或者右子樹代替被刪除的節點,若刪除的節點是葉子節點,則直接刪除,刪除後父節點的平衡因子在原來的基礎上減去1,然後能夠分爲如下兩種:

    ① 刪除以後父節點平衡因子爲-1,則說明本來父節點的左右子樹是高度一致的,刪除掉這個節點沒有影響了從父節點下來的這棵子樹的總體高度,影響的只是父節點的平衡因子,調整父節點的平衡因子,刪除操做結束

    ②刪除以後父節點平衡因子爲0,說明父節點的平衡因子從1變成0,從父節點下來的這棵子樹高度變低了,則須要從父節點開始上溯直到根節點,調整路徑上全部節點的平衡因子,若是通過的節點調整以後平衡因子爲-2或者2,則作對應的平衡調整操做,使其平衡因子回覆到正常。而後,此處就是前面所說的和插入操做旋轉調整以後不一樣的地方,由於插入一個新節點而致使須要旋轉調整實際上是由於某棵子樹的高度由於插入新節點而增長了,此時旋轉能夠把高度又下降到原來的狀態,因此調整後對父節點以上的全部節點都沒有影響了,由於高度已經恢復了。但刪除操做裏面,當調整平衡以後,其實本來失去平衡的節點如下子樹的高度已經比原來的高度減小了1(由於刪除的節點必然不是這顆子樹最大深度?),因此失去平衡的節點調整以後也影響了父節點以上的節點平衡因子,因此必須一直上溯直到根節點爲止。

  2. 刪除的節點是其父節點的右子樹,用刪除節點的左子樹或者右子樹代替被刪除的節點,若刪除的節點是葉子節點,則直接刪除,刪除以後父節點的平衡因子在原來的基礎上加上1,一樣可兩種狀況,與上面的狀況是鏡像對稱的,此處再也不贅述。

    下面用圖例來講明這個過程:

    

    若是要刪除值爲1的節點,刪除以後調整平衡到有圖,可注意到由值爲5的節點的左子樹其實高度比本來小了1,因此此時須要繼續向上調整平衡因子並作必要的旋轉

    

    往上找發現值爲5的節點也失去平衡,旋轉調整後,發現5是根節點,刪除操做結束。若值爲5的節點不是根節點,能夠發現該子樹的高度比原來也少了1,一樣須要繼續上溯。

  

  刪除的代碼以及刪除節點以後的調節代碼以下:

    public boolean delete(int elem) {
        if (null == this.root) {
            return false;
        } else {
            TreeNode node = this.root;
            // find out the node need to be deleted
            while (null != node) {
                if (node.getElem() == elem) {
                    deleteNode(node);
                    return true;
                } else if (node.getElem() > elem) {
                    node = node.getLeft();
                } else {
                    node = node.getRight();
                }
            }
            return false;
        }
    }

    private void deleteNode(TreeNode node) {
        TreeNode parent = node.getParent();
        if (null == node.getLeft() && null == node.getRight()) {
            if (null == parent) {
                this.root = null;
            } else if (node == parent.getLeft()) {
                parent.setLeft(null);
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            } else {
                parent.setRight(null);
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            }
            deleteFixUp(parent);
        } else if (null == node.getLeft()) {
            TreeNode right = node.getRight();
            if (null == parent) {
                this.root = right;
            } else if (node == parent.getLeft()) {
                parent.setLeft(right);
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            } else {
                parent.setRight(right);
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            }
            if (null != right) {
                right.setParent(parent);
            }
            deleteFixUp(parent);
        } else if (null == node.getRight()) {
            TreeNode left = node.getLeft();
            if (null == parent) {
                this.root = left;
            } else if (node == parent.getLeft()) {
                parent.setLeft(left);
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            } else {
                parent.setRight(left);
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            }
            if (null != left) {
                left.setParent(parent);
            }
            deleteFixUp(parent);
        } else {
            TreeNode pre = node.getLeft();
            while (null != pre.getRight()) {
                pre = pre.getRight();
            }
            TreeUtils.swapTreeElem(pre, node);
            deleteNode(pre);
        }
    }

    /**
     * fix up tree from node after delete node
     * @param node
     */
    private void deleteFixUp(TreeNode node) {
        if (null == node || -1 == node.getBalanceFactor() || 1 == node.getBalanceFactor()) {
            return;
        } else {
            TreeNode parent = node.getParent();
            boolean isLeft = null != parent && parent.getLeft() == node ? true : false;
            if (-2 == node.getBalanceFactor()) {
                TreeNode right = node.getRight();
                if (-1 == right.getBalanceFactor()) {
                    rotateRRFix(node);
                } else {
                    rotateRLFix(node);
                }
            } else  if (2 == node.getBalanceFactor()) {
                TreeNode left = node.getLeft();
                if (1 == left.getBalanceFactor()) {
                    rotateLLFix(node);
                } else {
                    rotateLRFix(node);
                }
            }
            if (null != parent) {
                if (isLeft) {
                    parent.setBalanceFactor(parent.getBalanceFactor() - 1);
                } else {
                    parent.setBalanceFactor(parent.getBalanceFactor() + 1);
                }
                // up tracking until root
                deleteFixUp(parent);
            }
        }
    }
View Code

   

  最後咱們就來測試一下這個刪除操做吧。

  構建上圖刪除節點前的樹以下圖:

  

  這個圖要順時針旋轉90°來看,中括號表示該節點的平衡因子。

  刪除值爲1的節點,刪除成功

--------------------------------------------------------------
1. insert
2. delete
3. search
4. print
5. exit
->2 1
->delete success

  刪除後的樹結構爲:

  

  和咱們意料之中同樣,問題不大。

  整個AVL樹除了旋轉操做須要死記硬背以外,其餘的操做只要懂得這麼操做的原理,代碼實現起來都是相對比較簡單。

  至此,全部AVL樹的操做均所有完成,若是不妥之處歡迎你們提出斧正。

 

 

 

 

 

 

 

 

   尊重知識產權,引用轉載請標明出處並通知原做者

相關文章
相關標籤/搜索