關於二叉樹的幾個必須掌握的實現

弒曉風發表於2019-03-06
The best way to end your fear is to face it yourself. 
結束恐懼的最佳辦法就是自己面對。

本分分享了二叉搜尋樹的幾種實現,由簡入繁。說句題外話,馬上又是金三銀四的季節了,無論跳不跳槽,增加自己的知識儲備總是沒錯的。從程式碼裡讀原理,讀思想。

所有原始碼均已上傳至github:連結

日誌

2019年3月28日 AVL樹

準備

public class Node {
    /**
     * data
     */
    public int data;
    /**
     * 左節點
     */
    public Node left;
    /**
     * 右節點
     */
    public Node right;

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

}複製程式碼

實現一個支援插入、刪除、查詢操作的二叉查詢樹,並且找二叉樹的最大值和最小值操作

插入操作

傳入一個data值,當tree為null,初始化tree,否則遍歷tree,比較data與node.data的值,大於的話插入右節點,否則插入左節點。

    private void insert(int data) {
        if (null == tree) {
            tree = new Node(data);
            return;
        }
        Node node = tree;
        while (null != node) {
            if (data > node.data) {
                if (null == node.right) {
                    node.right = new Node(data);
                    return;
                }
                node = node.right;
            } else {
                if (null == node.left) {
                    node.left = new Node(data);
                    return;
                }
                node = node.left;
            }
        }
    }複製程式碼

查詢

查詢的實現也比較簡單,遍歷tree的左右節點,直到找到data == node.data

    private Node find(int data) {
        Node node = tree;
        while (null != node) {
            if (data > node.data) {
                node = node.right;
            } else if (data < node.data) {
                node = node.left;
            } else {
                return node;
            }
        }
        return null;
    }複製程式碼

找二叉樹的最大值

找二叉樹的最大值最小值比較簡單,按照樹的結構遍歷即可

    private Node findMaxNode() {
        if (null == tree) return null;
        Node node = tree;
        while (null != node.right) {
            node = node.right;
        }
        return node;
    }複製程式碼

找二叉樹的最小值

    private Node findMinNode() {
        if (null == tree) return null;
        Node node = tree;
        while (null != node.left) {
            node = node.left;
        }
        return node;
    }複製程式碼

刪除操作

刪除是這裡面最複雜的操作了,這裡詳細講一下。

針對刪除的節點的位置不同需要考慮以下三種情況:

  1. 如果要刪除的節點沒有子節點,只需要直接將父節點中,指向要刪除節點的指標置為 null。
  2. 如果要刪除的節點只有一個子節點(左子節點或者右子節點),只需要更新父節點中,指向要刪除節點的指標,讓它指向要刪除節點的子節點就可以了。
  3. 如果要刪除的節點有兩個子節點,這就比較複雜了。需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。然後再刪除掉這個最小節點,因為最小節點肯定沒有左子節點(如果有左子結點,那就不是最小節點了)。

    private void delete(int data) {
        Node node = tree;
        //node的父節點
        Node pNode = null;
        while (null != node && node.data != data) {
            pNode = node;
            if (data > node.data) {
                node = node.right;
            } else {
                node = node.left;
            }
        }
        if (null == node) return;
        if (null != node.left && null != node.right) {
            Node p = node.right;
            //p的父節點
            Node pp = node;
            while (null != p.left) {
                pp = p.left;
                p = p.left;
            }
            //刪除操作
            node.data = p.data;
            node = p;
            pNode = pp;
        }
        Node child;
        if (null != node.left) {
            child = node.left;
        } else if (null != node.right) {
            child = node.right;
        } else {
            child = null;
        }
        if (null == pNode) {
            tree = child;
        } else if (pNode.left == null) {
            pNode.left = child;
        } else {
            pNode.right = child;
        }
    }複製程式碼

實現查詢二叉查詢樹中某個節點的後繼、前驅節點

ps:因為這裡測試的節點都是根節點,因為比較簡單,不需要遍歷該節點的父節點。

後續如果需要,歡迎留言,我補充一下。

查詢前驅節點

    private Node findPreNode(Node node) {
        if (null == node) return null;
        if (null != node.left){
            Node left = node.left;
            while (null != left.right){
                left = left.right;
            }
            return left;
        }
        return null;
    }複製程式碼

查詢後繼節點

    private Node findNextNode(Node node) {
        if (null == node) return null;
        if (null != node.right) {
            Node right = node.right;
            while (null != right.left) {
                right = right.left;
            }
            return right;
        }
        return null;
    }複製程式碼

實現二叉查詢樹前、中、後序以及層級遍歷

前序遍歷

    public void preOrderTraversal(Node tree) {
        if (null == tree) return;
        System.out.print(tree.data + " ");
        preOrderTraversal(tree.left);
        preOrderTraversal(tree.right);
    }複製程式碼

中序遍歷

    public void inOrderTraversal(Node tree) {
        if (null == tree) return;
        inOrderTraversal(tree.left);
        System.out.print(tree.data + " ");
        inOrderTraversal(tree.right);
    }複製程式碼

後序遍歷

    public void postOrderTraversal(Node tree) {
        if (null == tree) return;
        postOrderTraversal(tree.left);
        postOrderTraversal(tree.right);
        System.out.print(tree.data + " ");
    }複製程式碼

層級遍歷

層級遍歷的思想是維護一個隊裡,每一次遍歷tree,將tree的節點列印,然後將tree的left和right節點存入佇列裡(每一次列印時從佇列裡取即可)。

    public void levelTraversal(Node tree) {
        if (null == tree) return;
        LinkedList<Node> linkedList = new LinkedList<>();
        Node node;
        linkedList.offer(tree);
        while (!linkedList.isEmpty()) {
            node = linkedList.poll();
            System.out.print(node.data + " ");
            if (null != node.left) {
                linkedList.offer(node.left);
            }
            if (null != node.right) {
                linkedList.offer(node.right);
            }
        }
    }複製程式碼

AVL樹

定義

AVL樹是一種平衡二叉樹。貌似就windows對程式地址空間的管理用到了AVL樹。

  1. 左右子樹的高度差小於等於 1。
  2. 其每一個子樹均為平衡二叉樹。

核心

AVL樹核心在於左旋和右旋上,當出現不平衡的情況時,它會自動調整。

宣告一個AVL樹的Node類

在普通二叉樹的基礎上新增了幾個屬性

  1. balance 是平衡標識,它的區間是[-1,1]
  2. height 是該節點的高度,自上而下計算,從0開始

    public class Node {
        /**
         * 資料
         */
        int data;
        /**
         * 高度
         */
        int height;
        /**
         * 平衡標識
         */
        int balance;
        /**
         * 左子樹
         */
        Node left;
        /**
         * 右子樹
         */
        Node right;
        /**
         * 根
         */
        Node parent;

        /**
         * 有參構造方法
         *
         * @param data   資料
         * @param parent 父節點
         */
        Node(int data, Node parent) {
            this.data = data;
            this.parent = parent;
        }
    }複製程式碼

插入

插入方法和常規插入的區別是需要將當前節點作為根節點插入,並且執行一下自平衡方法。

 private void insert(int data) {
        if (null == root) {
            root = new Node(data, null);
        } else {
            Node node = root;
            Node parent;
            while (node.data != data) {
                parent = node;
                boolean goleft = node.data > data;
                node = goleft ? node.left : node.right;
                if (null == node) {
                    if (goleft) {
                        parent.left = new Node(data, parent);
                    } else {
                        parent.right = new Node(data, parent);
                    }
                    rebalance(parent);
                    break;
                }
            }
        }
    }複製程式碼

刪除

這裡是根據節點刪除。和常規的刪除相比,也是多了一步自平衡。

    private void delete(Node node) {
        if (null == node.left && null == node.right) {
            if (null == node.parent) {
                root = null;
            } else {
                Node parent = node.parent;
                if (parent.left == node) {
                    parent.left = null;
                } else {
                    parent.right = null;
                }
                rebalance(parent);
            }
        }
        if (null != node.left) {
            Node child = node.left;
            while (null != child.right) {
                child = child.right;
            }
            node.data = child.data;
            delete(child);
        } else {
            Node child = node.right;
            if (null != child) {
                while (null != child.left) {
                    child = child.left;
                }
                node.data = child.data;
                delete(child);
            }
        }
    }複製程式碼

自平衡

有這麼四種情況。

avlæ æ转çå¾å½¢æè¿°ã

這裡是否需要自平衡,是根據balance來判斷的,當balance為-2的時候,(如果該節點的左子樹的左子樹的高度小於右子樹高度,左旋,)右旋;當balance為2的時候,(如果該節點的右子樹的右子樹的高度小於左子樹高度,右旋,)左旋;最後判斷根節點是否為null,是則遞迴,否則賦值,停止遞迴;平衡完畢。

    private void rebalance(Node node) {
        setBalance(node);
        if (node.balance == -2) {
            if (getHeight(node.left.left) < getHeight(node.left.right)) {
                node.left = rotateLeft(node.left);
            }
            node = rotateRight(node);
        } else if (node.balance == 2) {
            if (getHeight(node.right.right) < getHeight(node.right.left)) {
                node.right = rotateRight(node.right);
            }
            node = rotateLeft(node);
        }
        if (null != node.parent) {
            rebalance(node.parent);
        } else {
            root = node;
        }
    }複製程式碼

左右旋

左旋和右旋的原理是一樣的,只要瞭解一種即可。

    private Node rotateLeft(Node node) {
        Node right = node.right;
        right.parent = node.parent;
        node.right = right.left;
        if (null != node.right)
            node.right.parent = node;
        right.left = node;
        node.parent = right;
        if (null != right.parent) {
            if (right.parent.right == node) {
                right.parent.right = right;
            } else {
                right.parent.left = right;
            }
        }
        setBalance(node, right);
        return right;
    }複製程式碼

    private Node rotateRight(Node node) {
        Node left = node.left;
        left.parent = node.parent;
        node.left = left.right;
        if (null != node.left)
            node.left.parent = node;
        left.right = node;
        node.parent = left;
        if (null != left.parent) {
            if (left.parent.right == node) {
                left.parent.right = left;
            } else {
                left.parent.left = left;
            }
        }
        setBalance(node, left);
        return left;
    }複製程式碼

設定平衡點

首先是重置高度,取左右子樹高度的最大值加一作為該節點的高度。

其次是設定平衡點,用來判斷該節點是否平衡

    private void setBalance(Node... nodes) {
        for (Node node : nodes) {
            if (null != node) {
                //重置高度
                node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
                node.balance = getHeight(node.right) - getHeight(node.left);
            }
        }
    }複製程式碼

獲取樹的高度

    private int getHeight(Node node) {
        if (null != node)
            return node.height;
        return -1;
    }複製程式碼

測試程式碼

這裡列印用的中序遍歷(中序遍歷可以從小到大列印出來)

    private void print(Node node) {
        if (null != node) {
            print(node.left);
            System.out.println(node.data + " " + node.balance);
            print(node.right);
        }
    }

    public static void main(String[] args) {
        AVLTree avlTree = new AVLTree();
        for (int i = 1; i < 10; i++) {
            avlTree.insert(i);
        }
        avlTree.print();
    }複製程式碼

測試結果

               4

        2              6

1          3    5           8

                         7           9

關於二叉樹的幾個必須掌握的實現

data - balance

關於二叉樹的幾個必須掌握的實現

end

關於二叉樹的幾個必須掌握的實現

您的點贊和關注是對我最大的支援,謝謝!



相關文章