1. 程式人生 > >樹篇1-二叉查詢樹

樹篇1-二叉查詢樹

一、概述

        在看JDK原始碼時,看到關於map相關的,看到HashMap時,一部分原始碼與紅黑樹有關。索性就將樹這種資料結構從頭學一遍,記錄下來,給自己還有他人留個參考和學習的資料。

        涉及到使用二叉樹進行排序、查詢節點值、前序遍歷、中序遍歷、後續遍歷(它們的遞迴實現和非遞迴實現)、刪除節點、查詢最大節點、查詢最小節點。

二、開講

下面為程式碼片段,在文件最後面有完整程式碼

1. 定義二叉樹節點(我是用的是靜態內部類)

    /**
     * 二叉樹節點
     */
    static class Node {
        int value;
        Node leftChild;
        Node rightChild;

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

        public Node(int value, Node leftChild, Node rightChild) {
            this.value = value;
            this.leftChild = leftChild;
            this.rightChild = rightChild;
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Node node = (Node) o;

            return value == node.value;
        }
    }

2.類的整體結構和定義操作

/**
 * 二叉樹(根據大小排序)
 */
public class BinaryTree {
    private Node root;
    private int treeDepth;
    private int treeWidth;

    public BinaryTree(int value) {
        this.root = new Node(value);
    }

    public boolean insert(int t)	// 插入節點
	public int getTreeDepth()	// 獲取樹的深度
    public int getTreeWidth()	// 樹的邏輯最大寬度
	public void preOrderTtaverse()	// 前序遍歷
	public void preOrderByStack()	// 前序遍歷非遞迴實現
	public void inOrderTraverse()	// 中序遍歷
	public void inOrderByStack()	// 中序遍歷非遞迴實現
	public void postOrderTraverse()	// 後續遍歷
	public void postOrderByStack()	// 後續遍歷非遞迴實現
	public void printBinaryTreeByRow()	// 行式列印二叉樹
	public void printBinaryTreeByColumn()	// 列式列印二叉樹
	public Node findKey(int value)	// 查詢指定值
	public boolean removeNode(int value)	// 移除指定節點
	public int getMinValue()	// 獲取最小值
	public int getMaxValue()	// 獲取最大值
}

3.插入資料

    /**
     * 插入新節點
     *
     * @param t 節點的值域
     * @return 操作是否成功
     * @throws Exception
     */
    public boolean insert(int t) throws Exception {
        Node newNode = new Node(t); // 建立新的節點
        // 計數器
        int count = 0;

        if (root == null) { // 如果根節點為空,將新節點賦值給根節點
            root = newNode;
            count++;
        } else {
            Node current = root;    // 節點指標
            Node parent;    // 節點指標的父節點
            for (; ; ) {    // 迴圈遍歷
                count++;

                if (t < current.value) {    // 小於當前節點,插入在左邊
                    parent = current;
                    current = current.leftChild;
                    if (current == null) {  // 如果節點指標為空,此時父節點為葉子節點,此時該插入元素
                        parent.leftChild = newNode;
                        break;  // 插入完成,打破迴圈
                    }
                } else if (t > current.value) { // 大於當前節點,插入在右邊
                    parent = current;
                    current = current.rightChild;
                    if (current == null) {  // 如果節點指標為空,此時父節點為葉子節點,此時該插入元素
                        parent.rightChild = newNode;
                        break;  // 插入完成,打破迴圈
                    }
                } else {
                    throw new Exception("comparsion Exception for compare value big or small(insert() method!)");
                }
            }
        }

        if (treeDepth < count) {    // 計算二叉樹深度和邏輯邏輯寬度
            treeDepth = count;
            treeWidth = (int)Math.pow(2, treeDepth);
        }

        return true;
    }

4.獲取二叉排序樹的最大深度、最大邏輯寬度

    // 返回深度
    public int getTreeDepth() {
        return treeDepth;
    }

    // 返回寬度
    public int getTreeWidth() {
        return treeWidth;
    }

5.前序遍歷

遞迴實現:

    /**
     * 前序遍歷二叉樹(遞迴實現)
     */
    public void preOrderTtaverse() {
        System.out.print("二叉樹它的前序遍歷: ");
        preOrderTtaverse(root);
        System.out.println();
    }

    /**
     * 前序遍歷:
     * 若二叉樹為空,則空操作返回;
     * 若二叉樹不為空,則執行下述操作:
     * (1) 訪問根節點;
     * (2) 前序遍歷根節點的左子樹
     * (3) 前序遍歷根節點的右子樹
     */
    private void preOrderTtaverse(Node node) {
        if (node == null)
            return;

        System.out.print(node.value + " ");
        preOrderTtaverse(node.leftChild);
        preOrderTtaverse(node.rightChild);
    }

非遞迴實現:

    /**
     * 前序非遞迴遍歷:
     * 1)對於任意節點current,若該節點不為空則訪問該節點後再將節點壓棧,並將左子樹節點置為current,重複此操作,直到current為空。
     * 2)若左子樹為空,棧頂節點出棧,將該節點的右子樹置為current
     * 3) 重複1、2步操作,直到current為空且棧內節點為空。
     */
    public void preOrderByStack() {
        System.out.print("前序遍歷非遞迴實現: ");
        Stack<Node> stack = new Stack<>();  //棧,用於儲存節點
        Node current = root;    // 節點指標

        while (current != null || !stack.isEmpty()) {   // 節點指標不為空或棧不為空時迴圈
            while (current != null) {
                stack.push(current);
                System.out.print(current.value + " ");
                current = current.leftChild;
            }

            if (!stack.isEmpty()) {
                current = stack.pop();
                current = current.rightChild;
            }
        }

        System.out.println();
    }

6.中序遍歷

遞迴實現:

    /**
     * 中序遍歷:
     * 若二叉樹為空,則空操作返回;否者執行下列操作:
     * 1.中序遍歷訪問根節點左子樹
     * 2.訪問根節點
     * 3.中序遍歷訪問根節點右子樹
     */
    public void inOrderTraverse() {
        System.out.print("二叉樹它的中序遍歷: ");
        inOrderTraverse(root);
        System.out.println();
    }

    // 遞迴實現
    private void inOrderTraverse(Node node) {
        if (node == null)
            return;

        inOrderTraverse(node.leftChild);
        System.out.print(node.value + " ");
        inOrderTraverse(node.rightChild);
    }

非遞迴實現:

    /**
     * 中序非遞迴遍歷:
     * 1)對於任意節點current,若該節點不為空則將該節點壓棧,並將左子樹節點置為current,重複此操作,直到current為空。
     * 2)若左子樹為空,棧頂節點出棧,訪問節點後將該節點的右子樹置為current
     * 3) 重複1、2步操作,直到current為空且棧內節點為空。
     */
    public void inOrderByStack() {
        System.out.print("中序遍歷非遞迴實現: ");
        Stack<Node> stack = new Stack<>();
        Node current = root;

        while (current != null || !stack.isEmpty()) {
            while (current != null) {
                stack.push(current);
                current = current.leftChild;
            }

            if (!stack.isEmpty()) {
                current = stack.pop();
                System.out.print(current.value + " ");
                current = current.rightChild;
            }
        }

        System.out.println();
    }

7.後續遍歷

遞迴實現:

    /**
     * 後序遍歷:
     * 若二叉樹為空,則空操作返回;否則執行下列操作:
     * 1.後續遍歷根節點的左子樹
     * 2.後續遍歷根節點的右子樹
     * 3.訪問根節點
     */
    public void postOrderTraverse() {
        System.out.print("二叉樹它的後序遍歷: ");
        postOrderTraverse(root);
        System.out.println();
    }

    // 遞迴實現
    private void postOrderTraverse(Node node) {
        if (node == null)
            return;

        postOrderTraverse(node.leftChild);
        postOrderTraverse(node.rightChild);
        System.out.print(node.value + " ");
    }

非遞迴實現:

    /**
     * 後序非遞迴遍歷:
     * 1)對於任意節點current,若該節點不為空則訪問該節點後再將節點壓棧,並將左子樹節點置為current,重複此操作,直到current為空。
     * 2)若左子樹為空,取棧頂節點的右子樹,如果右子樹為空或右子樹剛訪問過,則訪問該節點,並將preNode置為該節點
     * 3) 重複1、2步操作,直到current為空且棧內節點為空。
     */
    public void postOrderByStack() {
        System.out.print("後序遍歷非遞迴實現: ");
        Stack<Node> stack = new Stack<>();
        Node current = root;
        Node preNode = null;

        while (current != null || !stack.isEmpty()) {
            while (current != null) {
                stack.push(current);
                current = current.leftChild;
            }

            if (!stack.isEmpty()) {
                current = stack.peek().rightChild;
                if (current == null || current == preNode) {
                    current = stack.pop();
                    System.out.print(current.value + " ");
                    preNode = current;
                    current = null;
                }
            }
        }

        System.out.println();
    }

8.刪除節點(是這裡相對來說比較複雜的內容)

    1) 刪除節點為葉子節點

        // 分情況討論刪除節點的情況:        
        // 1.需要刪除的節點為葉子節點
        if (waitRemoveNode.leftChild == null && waitRemoveNode.rightChild == null) {
            // 如果為根節點(樹中只有做一個節點-節點)
            if (waitRemoveNode == root) {
                root = null;
            } else {
                if (isLeftChild)    //如果該葉節點是父節點的左子節點,將父節點的左子節點置為null
                    parent.leftChild = null;
                else     //如果該葉節點是父節點的右子節點,將父節點的右子節點置為null
                    parent.rightChild = null;
            }
        }

2)刪除的節點只有一個節點:只有一個左子節點、只有一個右子節點

        // 2.1 需要刪除的節點有一個節點,且該節點為左子節點
        else if (waitRemoveNode.rightChild == null) {
            // 如果該節點為根節點,將根節點的左子節點變為根節點
            if (waitRemoveNode == root) {
                root = waitRemoveNode.leftChild;
            } else {
                // 如果該節點是父節點的左子節點,將待刪除節點的左子節點變為父節點的左子節點
                if (isLeftChild)
                    parent.rightChild = waitRemoveNode.leftChild;
                else    // 如果該節點是父節點的右子節點,將待刪除節點的左子節點變為父節點的右子節點
                    parent.rightChild = waitRemoveNode.leftChild;
            }
        }
        // 2.2 需要刪除的節點有一個節點,且該節點為右子節點
        else if (waitRemoveNode.leftChild == null) {
            // 如果該節點為根節點,將根節點的右子節點變為根節點
            if (waitRemoveNode == root) {
                root = waitRemoveNode.rightChild;
            } else {
                // 如果該節點是父節點的左子節點,將待刪除節點的右子節點變為父節點的左子節點
                if (isLeftChild)
                    parent.leftChild = waitRemoveNode.rightChild;
                else    // 如果該節點是父節點的右子節點,將待刪除節點的右子節點變為父節點的右子節點
                    parent.rightChild = waitRemoveNode.rightChild;
            }
        }

3)刪除的節點有兩個子節點:這種情況,需要找到一個節點去替代待刪除的節點;在二叉排序樹中,替代待刪除節點的最好節點是該節點次小於(大小僅次於它)它的節點或者次大於(比它大的所有元素集合中最小的)它的節點。這裡,我們選擇次大於它的節點,次大於它的節點的專業術語叫後繼節點。

      後繼節點:比要刪除的節點的關鍵值次高的節點是它的後繼節點。說得簡單一些,後繼節點就是比要刪除的節點的關鍵值要大的節點集合中的最小值。

得到後繼節點程式碼如下:

    /**
     * 獲取待刪除節點的後繼節點
     */
    private Node getSuccessorNode(Node waitRemoveNode) {
        Node successorParent = null;    // 後繼節點的父節點
        Node successor = waitRemoveNode;    // 後繼節點
        Node current = waitRemoveNode.rightChild;   // 現在的節點指標

        // 尋找後繼節點及其父節點
        while (current != null) {
            successorParent = successor;
            successor = current;
            current = current.leftChild;
        }

        // 如果後繼節點不是待刪除節點的右子樹時
        if (successor != waitRemoveNode.rightChild) {
            // 將後繼節點的右子節點指向後繼節點父節點的左子節點
            successorParent.leftChild = successor.rightChild;
            // 將待刪除節點的右子節點指向後繼節點的右子節點
            successor.rightChild = waitRemoveNode.rightChild;
        }
        // 任何情況下都需要將待刪除節點的左子節點指向後繼節點的左子節點
        successor.leftChild = waitRemoveNode.leftChild;

        return successor;
    }

a) 如果後繼節點是剛好要刪除的節點的右子節點(這個右子節點沒有左孩子,如果有,它不可能是後繼節點)

        //刪除的節點為父節點的左子節點時:
         parent.leftChild = successor;
         successor.leftChild = delNode.leftChild;
              
         //刪除的節點為父節點的右子節點時:
         parent.rightChild = successor;
         successor.leftChild = delNode.leftChild

b) 如果後繼節點為要刪除節點的右子節點的左後代:

        //刪除的節點為父節點的左子節點時:
        successorParent.leftChild = successor.rightChild;
        successor.rightChild = delNode.rightChild;
        parent.leftChild = successor;
        successor.leftChild = delNode.leftChild;
        //刪除的節點為父節點的右子節點時:
        successorParent.leftChild = successor.rightChild;
        successor.rightChild = delNode.rightChild;
        parent.rightChild = successor;
        successor.leftChild = delNode.leftChild;

綜上,完整的刪除程式碼如下:

    /**
     * 刪除節點
     */
    public boolean removeNode(int value) {
        Node waitRemoveNode = root;    // 需要刪除的節點
        Node parent = null;     // 需要刪除節點的父節點
        boolean isLeftChild = true; // 需要刪除的節點是否是父節點的左子樹

        // 在二叉樹中尋找待刪除節點和待刪除節點的父節點並確定待刪除節點是否是左子樹
        // (簡單的來說就是初始化current、parent、isLeftChild)
        while (true) {
            if (value == waitRemoveNode.value) {
                break;
            } else if (value < waitRemoveNode.value) {
                isLeftChild = true;
                parent = waitRemoveNode;
                waitRemoveNode = waitRemoveNode.leftChild;
            } else {
                isLeftChild = false;
                parent = waitRemoveNode;
                waitRemoveNode = waitRemoveNode.rightChild;
            }

            // 找不到需要刪除的節點,直接返回
            if (waitRemoveNode == null)
                return false;
        }

        // 分情況討論刪除節點的情況:
        // 1.需要刪除的節點為葉子節點
        if (waitRemoveNode.leftChild == null && waitRemoveNode.rightChild == null) {
            // 如果為根節點(樹中只有做一個節點-節點)
            if (waitRemoveNode == root) {
                root = null;
            } else {
                if (isLeftChild)    //如果該葉節點是父節點的左子節點,將父節點的左子節點置為null
                    parent.leftChild = null;
                else     //如果該葉節點是父節點的右子節點,將父節點的右子節點置為null
                    parent.rightChild = null;
            }
        }
        // 2.1 需要刪除的節點有一個節點,且該節點為左子節點
        else if (waitRemoveNode.rightChild == null) {
            // 如果該節點為根節點,將根節點的左子節點變為根節點
            if (waitRemoveNode == root) {
                root = waitRemoveNode.leftChild;
            } else {
                // 如果該節點是父節點的左子節點,將待刪除節點的左子節點變為父節點的左子節點
                if (isLeftChild)
                    parent.rightChild = waitRemoveNode.leftChild;
                else    // 如果該節點是父節點的右子節點,將待刪除節點的左子節點變為父節點的右子節點
                    parent.rightChild = waitRemoveNode.leftChild;
            }
        }
        // 2.2 需要刪除的節點有一個節點,且該節點為右子節點
        else if (waitRemoveNode.leftChild == null) {
            // 如果該節點為根節點,將根節點的右子節點變為根節點
            if (waitRemoveNode == root) {
                root = waitRemoveNode.rightChild;
            } else {
                // 如果該節點是父節點的左子節點,將待刪除節點的右子節點變為父節點的左子節點
                if (isLeftChild)
                    parent.leftChild = waitRemoveNode.rightChild;
                else    // 如果該節點是父節點的右子節點,將待刪除節點的右子節點變為父節點的右子節點
                    parent.rightChild = waitRemoveNode.rightChild;
            }
        }
        // 3.待刪除節點有兩個節點,需要找該節點的後續節點作為替代節點
        else {
            Node successor = getSuccessorNode(waitRemoveNode);

            // 如果待刪除節點為根節點,將後繼節點變為根節點,並將根節點的左子節點變為後繼節點的左子節點
            if (waitRemoveNode == root) {
                root = successor;
            } else {
                // 如果待刪除節點是父節點的左子節點,將該節點的後繼節點變為父節點的左子節點
                if (isLeftChild)
                    parent.leftChild = successor;
                else    // 如果待刪除接點是父節點的右子節點,將該節點的後繼節點變成節點的右子節點
                    parent.rightChild = successor;
            }
        }

        waitRemoveNode = null;
        return true;
    }

        上述實現中的刪除比較複雜。有一種簡單的替代操作,稱為懶惰刪除(lazy deletion)。在懶惰刪除時,我們並不真正從二叉搜尋樹中刪除該節點,而是將該節點標記為“已刪除”。這樣,我們只用找到元素並標記,就可以完成刪除元素了。如果有相同的元素重新插入,我們可以將該節點找到,並取消刪除標記。

        懶惰刪除的實現比較簡單,可以嘗試一下。樹所佔據的記憶體空間不會因為刪除節點而減小。懶惰節點實際上是用記憶體空間換取操作的簡便性。

9.查詢指定節點

    /**
     * 查詢指定的值
     */
    public Node findKey(int value) {
        Node current = root;
        while (true) {
            if (value == current.value)
                return current;
            else if (value < current.value)
                current = current.leftChild;
            else if (value > current.value)
                current = current.rightChild;

            if (current == null)
                return null;
        }
    }

10. 獲取最大值和最小值

    /**
     * 獲取最小值
     */
    public int getMinValue() {
        Node current = root;

        while (true) {
            if (current.leftChild == null)
                return current.value;

            current = current.leftChild;
        }
    }

    /**
     * 獲取最大值
     */
    public int getMaxValue() {
        Node current = root;

        while (true) {
            if (current.rightChild == null)
                return current.value;

            current = current.rightChild;
        }
    }

11. 列印二叉排序樹

    public void printBinaryTreeByRow() {
        printBinaryTreeByRow(root);
    }

    public void printBinaryTreeByColumn() {
        printBinaryTreeByColumn(0, root);
    }

    private void printBinaryTreeByColumn(int space, Node node) {
        System.out.println();
        for (int i = 0; i < space; i++) {
            System.out.print("\t");
        }

        System.out.print("【" + node.value + "】");
        if (node.leftChild != null) {
            for (int i = 0; i < space + 2; i++) {
                System.out.print("\t");
            }
            printBinaryTreeByColumn(space + 3, node.leftChild);
        }
        if (node.rightChild != null) {
            for (int i = 0; i < space + 2; i++) {
                System.out.print("\t");
            }
            printBinaryTreeByColumn(space + 3, node.rightChild);
        }


    }

    private void printBinaryTreeByRow(Node node) {
        // 建立一個佇列用來存放節點
        Queue<Node> queue = new LinkedList<>();
        // 當前行最右節點(最右一個元素)
        Node lastNode = node;
        // 下一行最右節點(最右一個元素)
        Node nextLastNode = null;

        // 將當節點放入佇列中
        queue.add(node);
        while (queue.size() > 0) {
            // 出佇列
            Node nowNode = queue.poll();
            // 如果當前節點有左節點,將左節點壓入佇列中
            if ((nextLastNode = nowNode.leftChild) != null) {
                queue.add(nextLastNode);
            }
            // 如果當前節點有右節點,將左節點壓入佇列中
            if ((nextLastNode = nowNode.rightChild) != null) {
                queue.add(nextLastNode);
            }

            System.out.print(nowNode.value + " ");
            if (nowNode.equals(lastNode)) {
                System.out.println();
                lastNode = nextLastNode;
            }
        }

    }

開講完畢!

三、測試程式碼

/**
 * 二叉樹測試類
 */
public class BinaryTreeDemo {
    public static void main(String[] args) throws Exception {
        BinaryTree bt = new BinaryTree(50);
        bt.insert(30);
        bt.insert(80);
        bt.insert(10);
        bt.insert(40);
        bt.insert(35);
        bt.insert(60);
        bt.insert(90);
        bt.insert(70);
        bt.insert(83);
        bt.insert(95);
        bt.insert(75);
        bt.insert(88);

        bt.preOrderTtaverse();
        bt.preOrderByStack();

        bt.inOrderTraverse();
        bt.inOrderByStack();

        bt.postOrderTraverse();
        bt.postOrderByStack();

        System.out.println("查詢的777 : " + bt.findKey(777));
        System.out.println("最小值: " + bt.getMinValue());
        System.out.println("最大值: " + bt.getMaxValue());

        System.out.println();
        System.out.println("行式列印:");
        bt.printBinaryTreeByRow();
        System.out.println();
        System.out.println("列式列印:");
        bt.printBinaryTreeByColumn();
        System.out.println();

        bt.removeNode(32);      //刪除葉子節點//
        bt.removeNode(50);      //刪除只有一個左子節點的節點
        bt.removeNode(248);      //刪除只有一個右子節點的節點
        bt.removeNode(248);      //刪除只有一個右子節點的節點
        bt.removeNode(580);      //刪除有兩個子節點的節點,且後繼節點為刪除節點的右子節點的左後代
        bt.removeNode(888);      //刪除有兩個子節點的節點,且後繼節點為刪除節點的右子節點
        bt.removeNode(52);       //刪除有兩個子節點的節點,且刪除節點為根節點

        System.out.println("行式列印:");
        bt.printBinaryTreeByRow();
        System.out.println();
        System.out.println("列式列印:");
        bt.printBinaryTreeByColumn();

    }
}

執行結果:

二叉樹它的前序遍歷: 50 30 10 40 35 80 60 70 75 90 83 88 95 
前序遍歷非遞迴實現: 50 30 10 40 35 80 60 70 75 90 83 88 95 
二叉樹它的中序遍歷: 10 30 35 40 50 60 70 75 80 83 88 90 95 
中序遍歷非遞迴實現: 10 30 35 40 50 60 70 75 80 83 88 90 95 
二叉樹它的後序遍歷: 10 35 40 30 75 70 60 88 83 95 90 80 50 
後序遍歷非遞迴實現: 10 35 40 30 75 70 60 88 83 95 90 80 50 
查詢的777 : null
最小值: 10
最大值: 95

行式列印:
50 
30 80 
10 40 60 90 
35 70 83 95 
75 88 
列式列印:

【50】        
            【30】                    
                        【10】                    
                        【40】                                
                                    【35】        
            【80】                    
                        【60】                                
                                    【70】                                            
                                                【75】                    
                        【90】                                
                                    【83】                                            
                                                【88】                                
                                    【95】
行式列印:
60 
30 80 
10 40 70 90 
35 75 83 95 
88 
列式列印:

【60】        
            【30】                    
                        【10】                    
                        【40】                                
                                    【35】        
            【80】                    
                        【70】                                
                                    【75】                    
                        【90】                                
                                    【83】                                            
                                                【88】                                
                                    【95】