數據結構與算法之美筆記——平衡二叉查找樹

摘要:

平衡二叉查找樹(Balance Binary Search Tree)」用以解決二叉查找樹因不平衡情況而導致的執行效率下降問題,不過為了提高整體操作的效率,基本上使用非嚴格的平衡二叉查找樹,代表是「紅黑樹(Red-Black Tree)」。

平衡才是美

前面關于二叉查找樹的文章已經提到過,平衡情況下二叉查找樹的時間復雜度才是 O(\log{n}),但按照之前二叉查找樹的插入、刪除操作聽之任之,不加以干預,二叉查找樹的平衡性遲早會被破壞,于是引入了平衡二叉查找樹。

對平衡二叉查找樹的定義是樹中任意節點的左右子樹高度之差不大于 1,這樣可以確保某個子樹不會高度過大,影響二叉查找樹的平衡。節點可以通過存儲左右子樹高度差確定是否符合平衡的定義,當插入或者刪除操作時更新相應節點的高度,而查找操作并不受到影響。

不平衡到平衡

上一小節已經告訴我們如何確定一棵樹是否平衡,或者樹中的某個節點是否保持平衡,但如果出現不平衡的情況我們需要如何將其轉換為平衡狀態?一般「左旋」「右旋」是解決此問題的方法,其實左旋應該稱為以某個節點為中心向左旋轉,右旋與之相對應,應該被稱為以某個節點為中心向右旋轉,在左旋和右旋的操作中一般會有兩個關鍵的節點,一個為「root」節點,一個為「pivot」節點,此處的 root 并不是指樹的根節點,可以是樹中的任意節點,可以理解為以此節點為根節點,左旋和右旋的操作就是以 pivot 節點為中心節點進行左右旋轉。

幾種不平衡情況

雖然二叉查找樹有多種不平衡的具體情況,但最終被抽象總結為四種情況,接下來我們一起分析一下。

左-左(L-L)

此情況下 root 節點的左子樹高度大于右子樹,且高度差大于 1,而 pivot 為 root 的左子節點,而導致不平衡的是此節點的左子樹,此時為了保持平衡需要進行右旋操作,將 root 移至 pivot 右子節點,pivot 的右子節點移至 root 的左子節點。

為何這樣旋轉?因為將 pivot 右旋時 pivot 會頂替 root 節點位置,所以 root 節點需要重新放置,按照二叉查找樹的規則,pivot 是 root 的左子節點,root 節點值大于 pivot 值,root 節點應該放置在 pivot 的右子節點。如果 pivot 本身存在右子節點,那右子節點的位置將被 root 節點頂替,也需要重新放置。作為 pivot 的右子節點數值肯定大于 pivot 節點,而此節點依然屬于原 root 節點的左子樹,數值小于 root 節點,所以此節點應該放置于 root 節點的 左子節點處。

右-右(R-R)

右-右情況正好與左-左情況相反,當然也使用相反的旋轉——右旋,將 root 節點移至 pivot 節點的左子節點處,將 pivot 節點的原左子節點移至 root 節點的右子節點,當然原因與左-左情況的相似。除了這兩種情況外還有另外兩種情況,分別是「左-右(L-R)」「右-左(R-L)」,左-右需要先進行左旋再進行右旋,右-左要先進行右旋再進行左旋,具體情況可以根據下圖分析。

L-R-01.png

[圖片上傳失敗...(image-a0c404-1569162501263)]

AVL 樹

AVL 樹是平衡二叉查找樹的代表,通過左右子樹的高度差來判斷是否平衡,如果出現不平衡情況,需要根據具體非平衡形態進行相應的左右旋操作,但平衡二叉查找樹的節點需要存儲該節點的高度,所以每個節點的父節點可以通過左右子節點的高度屬性快速判斷左右子樹是否出現了不平衡。在平衡性的實現上我采用左子樹高度減右子樹高度的方式,相減結果等于 0 表示兩個子樹高度一致,當結果大于 1 時表示左子樹更高,當結果小于 -1 時表示右子樹更高,這樣的結果更加有利于判斷當前樹狀處于哪種不平衡形態下。

代碼實現

public class AvlTree {
    private Node root;

    public Node insertNode(Node node, int num) {
        if (node == null) {
            Node leaf = new Node(num);
            if (root == null) {
                root = leaf;
            }
            return leaf;
        }

        if (num < node.data) {
            node.left = insertNode(node.left, num);
        } else if (num > node.data) {
            node.right = insertNode(node.right, num);
        } else {
            return node;
        }

        int balance = getBalance(node);

        if (balance > 1 && num < node.left.data) {
            rotateRight(node);
        } else if (balance < -1 && num > node.right.data) {
            rotateLeft(node);
        } else if (balance > 1 && num > node.left.data) {
            rotateLeft(node.left);
            rotateRight(node);
        }

        node.height = updateNodeHeight(node);

        return node;
    }

    private int getNodeHeight(Node node) {
        return node == null ? 0 : node.height;
    }

    public List<Node> sortNode() {
        List<Node> nodes = new ArrayList<>();
        getSortNodes(nodes, root);
        return nodes;
    }

    private void getSortNodes(List<Node> sortNodes, Node node) {
        if (node == null) {
            return;
        }
        if (node.left == null && node.right == null) {
            sortNodes.add(node);
            return;
        }

        getSortNodes(sortNodes, node.left);
        sortNodes.add(node);
        getSortNodes(sortNodes, node.right);
    }

    public int getBalance(Node node) {
        return getNodeHeight(node.left) - getNodeHeight(node.right);
    }

    public Node getRoot() {
        return root;
    }

    public int getHeight() {
        return root.height;
    }

    public void rotateRight(Node rRoot) {
        Node pivot = rRoot.left;
        Node pivotR = pivot.right;

        pivot.right = rRoot;
        rRoot.left = pivotR;

        updateRotateNodeState(rRoot, pivot);
    }

    private void updateRotateNodeState(Node rRoot, Node pivot) {
        rRoot.height = updateNodeHeight(rRoot);
        pivot.height = updateNodeHeight(pivot);

        if (rRoot == root) {
            root = pivot;
        }
    }

    private int updateNodeHeight(Node rRoot) {
        return 1 + Math.max(getNodeHeight(rRoot.left), getNodeHeight(rRoot.right));
    }

    public void rotateLeft(Node rRoot) {
        Node pivot = rRoot.right;
        Node pivotL = pivot.left;

        pivot.left = rRoot;
        rRoot.right = pivotL;

        updateRotateNodeState(rRoot, pivot);
    }

    public class Node {
        public final int data;
        private Node left;
        private Node right;
        private int height = 1;

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

AVL 樹的實現代碼中只是實現了插入節點的操作,刪除節點與之類似,按照二叉樹刪除節點的的規則將節點刪除后判斷節點是否處于平衡狀態,如果未處于平衡狀態就根據非平衡形態進行相應的旋轉操作。

非嚴格平衡二叉樹

雖然 AVL 樹利用節點的旋轉保持了整個樹的平衡,但是每次插入節點或者刪除節點都需要進行相關節點的旋轉操作,必然會使操作效率下降,為了在二叉樹的平衡性與執行效率之間找到一個平衡點,就提出了非嚴格定義的平衡二叉樹,這樣的二叉樹不需要遵守任意節點的左右子樹高度差不大于 1 的規定,而只要保持高度與 \log_2{n} 不會相差過大即可。

說理論過于抽象,我們舉個實際的例子,比如非嚴格平衡二叉查找樹的代表——「紅黑樹(Red-Black Tree/R-B Tree)」,紅黑樹也是二叉樹,不同的是它的節點會是紅色或者黑色,一棵紅黑樹需要滿足以下幾個條件

  • 根節點必須為黑色節點
  • 葉子節點都是黑色的空節點(NIL)
  • 兩個相鄰節點不能同時為紅色節點,也就是說紅色節點都是被黑色節點隔開的
  • 從任意節點出發,到達其可達的葉子節點路徑上的黑色節點數量相同

第 1,3,4 點都比較容易理解,第 2 條保證葉子節點都為黑色空節點是為了簡化紅黑樹的實現,現在的分析我們可以暫時不關心第 2 個定義,按照規則我們可以畫一棵紅黑樹。

image

那紅黑樹的平均高度是多少?我們可以先把紅色節點移除,使黑色節點形成四叉樹,將四叉樹節點移動轉換為完全二叉樹時可以看出,四叉樹高度是低于相同節點數量下的完全二叉樹的,也就是只由黑色節點組成的四叉樹高度是低于 \log_2{n} 的,因為相鄰紅色節點需要被黑色節點隔開,加入紅色后的紅黑樹高度是低于 2\log_2{n} 的,其實也是相對平衡的,并且在保持平衡的情況下使插入和刪除操作都保持了較高的執行效率,如果紅黑樹碰到破壞平衡的情況,也就是破壞紅黑樹的第 3,4 條定義時可以按照紅黑樹的對應操作步驟使用左右旋使其重新符合定義。

image

[圖片上傳失敗...(image-d39c98-1569162501263)]

通過紅黑樹我們可以看出其實平衡的定義可以比較寬泛,我們希望解決的二叉查找樹平衡性問題其實是防止其退化為鏈表,也就是左右子樹的高度差距極大,而保持平衡其實是在保持二叉查找樹的對稱性,避免高度差距極大的左右子樹情況,降低二叉查找樹的整體高度,以此保證二叉查找樹的執行效率。

總結

由于平衡性問題會極大影響二叉查找樹的執行效率,業界以左右子樹高度差不大于 1 作為二叉查找樹的界定標準,通過左旋或者右旋的方式來維護樹的平衡,AVL 樹是這種嚴格定義平衡二叉查找樹代表,不過為了保持平衡導致操作的效率受到影響,為了平衡這種影響,業界采用更加廣泛的平衡定義,同時也衍生出紅黑樹這樣的非嚴格定義的平衡二叉查找樹代表,在操作性能和平衡性之間找到了平衡點。


文章中如有問題歡迎留言指正
本章節代碼已經上傳GitHub,可點擊跳轉查看代碼詳情。
數據結構與算法之美筆記系列將會做為我對王爭老師此專欄的學習筆記,如想了解更多王爭老師專欄的詳情請到極客時間自行搜索。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容