1. 程式人生 > >平衡二叉樹的插入與刪除

平衡二叉樹的插入與刪除

定義

AVL樹是帶有平衡條件的二叉查詢樹。它要求在AVL樹中任何節點的兩個子樹的高度(高度是指節點到一片樹葉的最長路徑的長) 最大差別為1,如下圖所示:

AVL

為什麼有AVL樹

大多數BST操作,例如查詢,找最大,最小值,插入,刪除等操作,基本上消耗O(h)時間,h是BST的高度。對於傾斜的二叉樹,這些操作的成本可能會變成O(n)。

如果我們在每次插入和刪除之後確保樹的高度保持O(logn),那麼我們就可以保證所有這些操作的O(logn)的上界。而AVL樹的高度總是O(logn),其中n是樹中節點的數量。

所以AVL樹很好的解決了二叉搜尋樹退化成連結串列的問題。把插入,查詢,刪除的時間複雜度的最好情況和最壞情況都維持在O(logn)

插入操作

當我們執行插入一個節點時,很可能會破壞AVL樹的平衡特性,所以我們需要調整AVL樹的結構,使其重新平衡,而調整的方式稱之為旋轉

這裡我們針對父節點的位置分為左-左左-右右-右右-左這4類情況分析,而對左-左右-右情況進行單旋轉,也就是一次旋轉,對左-右右-左情況進行雙旋轉,兩次旋轉,最終恢復其平衡特性。

1.左-左情況

AVL1

2.左-右情況

AVL2

3.右-右情況

AVL3

4.右-左情況

AVL4

程式碼實現

package com.pjmike.tree.AVL;

/**
 * AVL樹
 *
 * @author pjmike
 * @create
2018-08-09 17:57 */
public class AVLTree { private Node root; /** * 計算AVL節點的高度的方法 * * @param node * @return */ private int height(Node node) { //如果為空,返回height為0 return node == null ? 0 : node.height; } /** * 計算兩個的最大值 * * @param a * @param
b * @return */
private int max(int a, int b) { return a > b ? a : b; } /** * 右旋轉 * * @param y * @return */ Node rightRotate(Node y) { Node x = y.left; Node T1 = x.right; x.right = y; y.left = T1; //更新高度 y.height = max(height(y.left), height(y.right)) + 1; x.height = max(height(x.left), height(x.right)) + 1; return x; } /** * 左旋轉 * * @param x * @return */ Node leftRotate(Node x) { Node y = x.right; Node T2 = y.left; y.left = x; x.right = T2; //更新高度 x.height = max(height(x.left), height(x.right)) + 1; y.height = max(height(y.left), height(y.right)) + 1; return y; } /** * 獲取平衡因子 * * @param node * @return */ int getBalance(Node node) { return node == null ? 0 : (height(node.left) - height(node.right)); } /** * 插入操作 * * @param node * @param val * @return */ Node insert(Node node, int val) { if (node == null) { return new Node(val); } if (val < node.val) { node.left = insert(node.left, val); } else if (val > node.val) { node.right = insert(node.right, val); } else { return node; } //更新節點高度 node.height = 1 + max(height(node.left), height(node.right)); //這是插入完畢後的 int balance = getBalance(node); if (balance > 1 && val < node.left.val) { //右旋 return rightRotate(node); } if (balance < -1 && val > node.right.val) { //左旋 return leftRotate(node); } if (balance > 1 && val > node.left.val) { //先左旋,再右旋 node.left = leftRotate(node.left); return rightRotate(node); } if (balance < -1 && val < node.right.val) { //先右旋再左旋 node.right = rightRotate(node.right); return leftRotate(node); } return node; } public static void main(String[] args) { AVLTree avlTree = new AVLTree(); //創造AVL樹 avlTree.root = avlTree.insert(avlTree.root, 10); avlTree.root = avlTree.insert(avlTree.root, 20); avlTree.root = avlTree.insert(avlTree.root, 30); avlTree.root = avlTree.insert(avlTree.root, 40); avlTree.root = avlTree.insert(avlTree.root, 50); avlTree.root = avlTree.insert(avlTree.root, 23); avlTree.Preorder(avlTree.root); } void Preorder(Node node) { if (node != null) { System.out.println(node.val+" "); Preorder(node.left); Preorder(node.right); } } } class Node { int val; Node left; Node right; /** * 節點高度,高度是指節點到一片樹葉的最長路徑的長 */ int height; public Node(int val) { this.val = val; height = 1; } }

刪除操作

對於二叉查詢樹,我們都知道它的刪除要分三種情況:
- 刪除的節點為葉子節點
- 刪除的節點左子樹或右子樹有一個為空
- 刪除的節點有兩個子樹

AVL本身是帶有平衡性質的二叉搜尋樹,所以它的刪除策略是與二叉查詢樹相似的,只不過刪除節點後可能會造成樹失去平衡性,所以需要做平衡處理

程式碼實現

  Node deleteNode(Node root, int val) {
        if (root == null) {
            return root;
        }
        if (val < root.val) {
            root.left = deleteNode(root.left, val);
        } else if (val > root.val) {
            root.right = deleteNode(root.right, val);
        } else {
            //刪除節點有兩個孩子
            if (root.left != null && root.right != null) {
                root.val = findMin(root.right);
                root.right = deleteNode(root.right, root.val);
            } else {
            //刪除節點只有一個孩子或者沒有孩子
                root = (root.left != null) ? root.left : root.right;
            }
        }
        //以下操作是為了恢復AVL樹的平衡性
        if (root == null) {
            return root;
        }
        root.height = max(height(root.left), height(root.right)) + 1;
        int balance = getBalance(root);
        //左-左情況,這裡使用>=而不是>就是為了保證這些情形下使用的是單旋轉而不是雙旋轉
        if (balance > 1 && getBalance(root.left) >= 0) {
            return rightRotate(root);
        }
        //左-右情況
        if (balance > 1 && getBalance(root.left) < 0)
        {
            root.left = leftRotate(root.left);
            return rightRotate(root);
        }

        //右-右情況
        if (balance < -1 && getBalance(root.right) <= 0) {
            return leftRotate(root);
        }
        //右-左情況
        if (balance < -1 && getBalance(root.right) > 0)
        {
            root.right = rightRotate(root.right);
            return leftRotate(root);
        }
        return root;
    }

    private int findMin(Node root) {
        if (root.left == null) {
            return root.val;
        } else {
            return findMin(root.left);
        }
    }

這裡的刪除後的平衡操作實際上與插入後的平衡操作是不一樣的,刪除可能造成樹的一邊比另一邊淺的情況,還可能造成整個二叉樹中有兩個子樹一樣深的情況。

小結

顯然,AVL樹的優勢在於插入,刪除,查詢操作的平均時間複雜度都為O(logn),但是它的缺陷是為了保持AVL樹的平衡性質,動態插入和刪除的代價也是很大的

參考資料