數據結構—平衡二叉樹

  二叉排序樹集中了數組的查找優點以及鏈表的插入、刪除優點,所以在數據結構中佔有必定的地位。但在必定的狀況下二叉排序樹又有可能變爲鏈表,例如插入從1~100的數,這時進行數據查找的效率就要下降。node

爲了解決二叉排序樹這種左右子樹深度不均勻的狀況引入了一種平衡二叉樹(AVLTree):任何一個節點的左右子樹深度差不超過1.經過這個限定,阻止了二叉樹的左右子樹深度差較大的狀況,維持了二叉樹的穩定。數組

  如何讓二叉樹的左右子樹深度差不超過1呢?這就須要對節點進行旋轉,也就是當某個節點的左右子樹深度超過1時須要對這個節點進行旋轉(旋轉以後依舊是左子樹小於節點小於右子樹),從新調整樹的結構。數據結構

例如:這兩棵二叉樹雖然結構不一樣,可是都是二叉排序樹,所謂的旋轉就是把左邊的深度爲3的樹旋轉爲右邊深度爲2的二叉樹。dom

      

在平衡二叉樹進行插入操做時遇到的不平衡狀況有多種,可是這麼多種狀況均可以分解爲一下四中基礎情景:把它叫作:左左、左右、右右、右左。測試

在解釋這四種情景以前須要先明白一個定義:最小不平衡節點—插入一個節點以後,距離這個插入節點最近的不平衡節點就是最小不平衡節點(如上圖左樹的10節點)。全部的旋轉都是在最小不平衡節點的基礎上進行的。this

繼續解釋四種情景命名意義:左左:節點插入在最小不平衡節點的左子樹的左子樹上。     左右:節點插入在最小不平衡節點的左子樹的右子樹上面spa

                右:節點插入在最小不平衡樹的右子樹的右子樹上面。   右左:節點插入在最小不平衡樹的右子樹的左子樹上面。.net

下面就具體分析這四種狀況:3d

  左左:右旋code

左左簡單不用詳解。

左右:先左旋再右旋

這裏有人又有疑問了,上面的左左(圖2)看明白了,可這裏左右情景爲何要旋轉兩次呢?爲何先左旋,再右旋呢?

先別急,看看這種狀況:(圖4)

 

毫無疑問這也是 左右 情景(左左情景有不少種,圖3演示的是最基礎的情景,全部 的左左情景的旋轉狀況和圖3都是同樣的),那麼該怎麼旋轉呢?

 

直接右旋不對吧?由於6節點的右子樹(以根節點10爲中心,靠近內部的子樹)6-8通過旋轉以後要充當10節點的左子樹,這樣會致使依舊不平衡。因此在這種左右情景下須要進行兩次旋轉,先把6的右子樹下降高度,而後在進行右旋。即:

把圖7 情景和圖3的情景同樣,這就是爲何 左右情景 須要先左旋再右旋的緣由。

在這裏能夠記做:最小不平衡節點的左節點的內部(以根節點作對稱軸,偏向對稱軸的爲內部。也就是以7爲節點的子樹)的子樹高度高於外部子樹的高度時須要進行兩次旋轉。

右右:左旋

 

右右情景直接左旋便可。不在詳解

右左:先右旋,再左旋

 

 

爲何這樣旋轉明白了吧?如同左右情景,考慮到圖10的 右左情景

   

這種情景旋轉如圖11

旋轉的四種情景就這些了。須要說明的是,下面這兩對情景旋轉是同樣的。

圖12都是右左情景,具體看代碼的旋轉方法就明白了在第一次右旋的時候進行的操做。private Node<T> rotateSingleRight(Node<T> node);

圖13都是左右情景,第一次左旋見:private Node<T> rotateSingleLeft(Node<T> node);

旋轉情景弄明白以後就是怎麼代碼實現了,在實現代碼以前須要考慮如何進行樹高判斷。這裏就根據定義來,|左子樹樹高-右子樹樹高|<2。若是大於等於2則該節點就不在 平衡,須要進行旋轉操做。所以在程序中節點中須要定義一個height屬性來存儲該節點的樹高。

因爲平衡二叉樹的性質,二叉樹的高度不會很高,程序使用遞歸進行數據插入查找不會形成棧溢出異常,因此程序採用遞歸操做進行插入查找。

平衡的斷定策略是在進行遞歸回溯的時候依照回溯路徑更新節點的樹高,而後根據|左子樹樹高-右子樹樹高|<2來斷定該節點是否失衡,進一步對是夠旋轉進行斷定。

程序中的平衡斷定策略比較漂亮,當時就是一直卡在這裏沒法繼續進行,而後參考了 AVL樹-自平衡二叉查找樹(Java實現) 以後採用這種方法才得以解決。

平衡二叉樹的刪除操做。

對於平衡二叉樹的刪除操做,只要明白一點就能夠了:

    若是該節點沒有左右子樹(該節點爲葉子節點)或者只有其中一個子樹則能夠直接進行刪除

       不然須要繼續進行斷定該節點:若是該節點的外部(內外:以根節點作對稱軸,靠近對稱軸的子樹爲內部子樹)子樹樹高低於內部子樹樹高,則找到該節點內部子樹的最值(最值:若是內部子樹是該節點的右子樹則數值爲右子樹的最小值;若是內部節點是該節點的左子樹則數值爲該節點左子樹的最大值)進行數值交換,交換以後刪除該節點便可。

刪除以後進行回溯的時候要更新節點的樹高,而後判斷節點是否平衡,不平衡進行旋轉。這時對旋轉次數的斷定就不一樣於插入時的斷定。

如圖14 刪除11節點

這種情景需不須要進行兩次旋轉?該如何斷定?

毫無疑問確定是要進行一次右旋的,可是在右旋以前是否是要進行一次左旋呢?

  這就要根據最小不平衡節點的左節點6進行斷定,若是6的左節點樹高低於6的右節點樹高則須要進行一次左旋,最後進行一次右旋結束。

   若是6的左子樹樹高高於6的右子樹樹高則不需進行左旋能夠直接對10節點進行右旋結束操做。

 如圖15,這種狀況確定須要進行左旋,至於在左旋以前要不要對13節點進行右旋,相信知道該如何判斷了。

  根據13節點的左右子樹高度來判斷,左子樹(內部)高於右子樹(外部)高度則須要進行左旋,圖15這種情景是不須要的。

知道了各類旋轉的斷定標準,程序中就沒有其餘什麼難點了,下面看一下代碼:

package com.zpj.datastructure.avlTree;

/**
 * @author PerKins Zhu
 * @date:2016年8月30日 下午8:01:03
 * @version :1.1
 * 
 */
// 存儲數據類型必須實現Comparable接口,實現比較方法
public class AVLTree<T extends Comparable<T>> {
    private Node<T> root;

    // 定義節點存儲數據
    private static class Node<T> {
        Node<T> left;// 左孩子
        Node<T> right;// 右孩子
        T data; // 存儲數據
        int height; // 樹高

        public Node(Node<T> left, Node<T> right, T data) {
            this.left = left;
            this.right = right;
            this.data = data;
            this.height = 0;
        }
    }

    // 對外公開的方法進行插入
    public Node<T> insert(T data) {
        return root = insert(data, root);
    }

    // 私有方法進行遞歸插入,返回插入節點
    private Node<T> insert(T data, Node<T> node) {
        // 遞歸終止條件
        if (node == null)
            return new Node<T>(null, null, data);
        // 比較插入數據和待插入節點的大小
        int compareResult = data.compareTo(node.data);
        if (compareResult > 0) {// 插入node的右子樹
            node.right = insert(data, node.right);
            // 回調時判斷是否平衡
            if (getHeight(node.right) - getHeight(node.left) == 2) {// 不平衡進行旋轉
                // 判斷是須要進行兩次旋轉仍是須要進行一次旋轉
                int compareResult02 = data.compareTo(node.right.data);
                if (compareResult02 > 0)// 進行一次左旋(右右)
                    node = rotateSingleLeft(node);
                else
                    // 進行兩次旋轉,先右旋,再左旋
                    node = rotateDoubleLeft(node);
            }
        } else if (compareResult < 0) {// 插入node的左子樹
            node.left = insert(data, node.left);
            // 回調時進行判斷是否平衡
            if (getHeight(node.left) - getHeight(node.right) == 2) {// 進行旋轉
                // 判斷是須要進行兩次旋轉仍是須要進行一次旋轉
                int intcompareResult02 = data.compareTo(node.left.data);
                if (intcompareResult02 < 0)// 進行一次左旋(左左)
                    node = rotateSingleRight(node);
                else
                    // 進行兩次旋轉,先左旋,再右旋
                    node = rotateDoubleRight(node);
            }
        }
        // 從新計算該節點的樹高
        node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
        return node;
    }

    // 右右狀況--進行左旋
    private Node<T> rotateSingleLeft(Node<T> node) {
        Node<T> rightNode = node.right;
        node.right = rightNode.left;
        rightNode.left = node;
        // 旋轉結束計算樹高
        node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
        rightNode.height = Math.max(node.height, getHeight(rightNode.right)) + 1;
        return rightNode;
    }

    // 左左狀況--進行右旋
    private Node<T> rotateSingleRight(Node<T> node) {
        Node<T> leftNode = node.left;
        node.left = leftNode.right;
        leftNode.right = node;
        // 旋轉結束計算樹高
        node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
        leftNode.height = Math.max(getHeight(leftNode.left), node.height) + 1;
        return leftNode;
    }

    // 右左狀況--先右旋再左旋
    private Node<T> rotateDoubleLeft(Node<T> node) {
        // 先進行右旋
        node.right = rotateSingleRight(node.right);
        // 再加上左旋
        node = rotateSingleLeft(node);
        return node;
    }

    // 左右--先左旋再右旋
    private Node<T> rotateDoubleRight(Node<T> node) {
        // 先進行左旋
        node.left = rotateSingleLeft(node.left);
        // 在進行右旋
        node = rotateSingleRight(node);
        return node;
    }

    // 計算樹高
    private int getHeight(Node<T> node) {
        return node == null ? -1 : node.height;
    }

    // public 方法供外部進行刪除調用
    public Node<T> remove(T data) {
        return root = remove(data, root);
    }

    // 遞歸進行刪除,返回比較節點
    private Node<T> remove(T data, Node<T> node) {
        if (node == null) {// 不存在此節店,返回null.不須要調整樹高
            return null;
        }
        int compareResult = data.compareTo(node.data);
        if (compareResult == 0) {// 存在此節點進入
            /**
             * 找到節點以後進行節點刪除操做 判斷node是否有子樹,若是沒有子樹或者只有一個子樹則直接進行刪除
             *     若是有兩個子樹,則須要判斷node的平衡係數balance
             *         若是balance爲0或者1則把node和node的左子樹的最大值進行交換 不然把node和右子樹的最小值進行交換
             *         交換數據以後刪除該節點 刪除以後判斷delete節點的父節點是否平衡,若是不平衡進行節點旋轉
             * 旋轉以後返回delete節點的父節點進行回溯
             * */
            if (node.left != null && node.right != null) { // 此節點存在左右子樹
                // 判斷node節點的balance,而後進行數據交換刪除節點
                int balance = getHeight(node.left) - getHeight(node.right);
                Node<T> temp = node;// 保存須要進行刪除的node節點
                if (balance == -1) {
                    // 與右子樹的最小值進行交換
                    exChangeRightData(node, node.right);
                } else {
                    // 與左子樹的最大值進行交換
                    exChangeLeftData(node, node.left);
                }
                // 此時已經交換完成而且把節點刪除完成,則須要從新計算該節點的樹高
                temp.height = Math.max(getHeight(temp.left), getHeight(temp.right)) + 1;
                // 注意此處,返回的是temp,也就是保存的須要刪除的節點,而不是替換的節點
                return temp;
            } else {
                // 把node的子節點返回調用處等於刪除了node節點
                // 此處隱含了一個node.left ==null && node.right == null 的條件,這時返回null
                return node.left != null ? node.left : node.right;
            }
        } else if (compareResult > 0) {// 沒找到須要刪除的節點繼續遞歸進行尋找
            node.right = remove(data, node.right);
            // 刪除以後進行樹高更新
            node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
            // 若是不平衡則進行右旋調整。
            if (getHeight(node.left) - getHeight(node.right) == 2) {// 進行旋轉
                Node<T> leftSon = node.left;
                // 判斷是否須要進行兩次右旋仍是一次右旋
                // 判斷條件就是比較leftSon節點的左右子節點樹高
                if (leftSon.left.height > leftSon.right.height) {
                    // 右旋一次
                    node = rotateSingleRight(node);
                } else {
                    // 兩次旋轉,先左旋,後右旋
                    node = rotateDoubleRight(node);
                }
            }
            return node;
        } else if (compareResult < 0) {// 沒找到須要刪除的節點繼續遞歸進行尋找
            node.left = remove(data, node.left);
            // 刪除以後進行樹高更新
            node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
            // 若是不平衡進行左旋操做
            if (getHeight(node.left) - getHeight(node.right) == 2) {// 進行旋轉
                Node<T> rightSon = node.right;
                // 判斷是否須要進行兩次右旋仍是一次右旋
                // 判斷條件就是比較rightSon節點的左右子節點樹高
                if (rightSon.right.height > rightSon.left.height) {
                    node = rotateSingleLeft(node);
                } else {
                    // 先右旋再左旋
                    node = rotateDoubleLeft(node);
                }
            }
            return node;
        }
        return null;
    }

    // 遞歸尋找right節點的最大值
    private Node<T> exChangeLeftData(Node<T> node, Node<T> right) {
        if (right.right != null) {
            right.right = exChangeLeftData(node, right.right);
        } else {
            // 數據進行替換
            node.data = right.data;
            // 此處已經把替換節點刪除
            return right.left;
        }
        right.height = Math.max(getHeight(right.left), getHeight(right.right)) + 1;
        // 回溯判斷left是否平衡,若是不平衡則進行左旋操做。
        int isbanlance = getHeight(right.left) - getHeight(right.right);
        if (isbanlance == 2) {// 進行旋轉
            Node<T> leftSon = node.left;
            // 判斷是否須要進行兩次右旋仍是一次右旋
            // 判斷條件就是比較leftSon節點的左右子節點樹高
            if (leftSon.left.height > leftSon.right.height) {
                // 右旋一次
                return node = rotateSingleRight(node);
            } else {
                // 兩次旋轉,先左旋,後右旋
                return node = rotateDoubleRight(node);
            }
        }
        return right;
    }

    // 遞歸尋找left節點的最小值
    private Node<T> exChangeRightData(Node<T> node, Node<T> left) {
        if (left.left != null) {
            left.left = exChangeRightData(node, left.left);
        } else {
            node.data = left.data;
            // 此處已經把替換節點刪除
            return left.right;
        }
        left.height = Math.max(getHeight(left.left), getHeight(left.right)) + 1;
        // 回溯判斷left是否平衡,若是不平衡則進行左旋操做。
        int isbanlance = getHeight(left.left) - getHeight(left.right);
        if (isbanlance == -2) {// 進行旋轉
            Node<T> rightSon = node.right;
            // 判斷是否須要進行兩次右旋仍是一次右旋
            // 判斷條件就是比較rightSon節點的左右子節點樹高
            if (rightSon.right.height > rightSon.left.height) {
                return node = rotateSingleLeft(node);
            } else {
                // 先右旋再左旋
                return node = rotateDoubleLeft(node);
            }
        }
        return left;
    }

    // ************************中序輸出  輸出結果有小到大*************************************
    public void inorderTraverse() {
        inorderTraverseData(root);
    }

    // 遞歸中序遍歷
    private void inorderTraverseData(Node<T> node) {
        if (node.left != null) {
            inorderTraverseData(node.left);
        }
        System.out.print(node.data + "、");
        if (node.right != null) {
            inorderTraverseData(node.right);
        }
    }

}

 

這段測試程序能夠進行測試:

package com.zpj.datastructure.avlTree;

import org.junit.Test;

/**
 * @author PerKins Zhu
 * @date:2016年8月30日 下午8:42:15
 * @version :1.1
 * 
 */
public class AVLTreeTest {

    @Test
    public void test01() {
        AVLTree tree = new AVLTree();
        int array[] = { 28, 35, 5, 35, 26, 30, 1, 21, 18, 35, 7, 30, 25, 1, 7, };
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + ",");
            tree.insert(array[i]);
        }
        System.out.println();
        tree.inorderTraverse();
        tree.remove(12);
        System.out.println();
        tree.inorderTraverse();
    }

    @Test
    public void test02() {
        AVLTree tree = new AVLTree();
        int temp = 0;
        for (int i = 0; i < 15; i++) {
            int num = (int) (Math.random() * 40);
            System.out.print(num + ",");
            tree.insert(num);
            temp = num;
        }
        System.out.println();
        tree.inorderTraverse();
        tree.remove(temp);// 刪除插入的最後一個數據
        System.out.println();
        tree.inorderTraverse();
    }

}

 

在測試過程當中對幾種特殊狀況都進行了測試,到目前爲止沒發現有問題,若是有朋友在測試的時候發現問題,歡迎指出討論。 

------------------------------------------------------    

相關文章
相關標籤/搜索