1. 程式人生 > >用 Java 手把手寫一個“二叉搜尋樹”,支援泛型

用 Java 手把手寫一個“二叉搜尋樹”,支援泛型

一、二叉搜尋樹

先說一下二叉樹,二叉樹是一種至多隻有左右兩個子結點的樹形結構。

二叉搜尋樹是二叉樹的一種,對於任意一個結點 x,其左子樹的任一結點的值都不大於 x 的值,其右子樹的任一結點的值都不小於 x 的值。

二叉搜尋樹上的基本操作有 查詢 (search)最小值 (minimum)最大值 (maximum)前驅值 (predecessor)後繼值 (successor)插入 (insert)刪除 (delete) 等等。其花費時間與樹的高度成正比。這些操作對於一個有 n 個結點的完全二叉樹而言,最壞執行時間為 O(lg n),而連結串列結構則需花費 O(n) 時間。

二、查詢

這裡的資料我們使用泛型 T 來表示。T 需要滿足一個條件,即它是可比較的,所以泛型 T 需要實現 Comparable 介面。

1. 查詢

給出待查詢的資料,返回一個指向該資料的結點的物件,若資料不存在於樹中,返回 null。

由於二叉搜尋樹的性質,我們可以從根結點開始查詢,如果資料等於結點處的值,則返回該結點,如果小於結點處的值,則我們繼續在其左子樹中查詢,如果大於結點處的值,則我們繼續在其右子樹中查詢。

程式碼如下:

    /**
     * @param data 待查詢的資料
     * @return 返回指向 data 資料的結點物件,不存在時返回 null
     */
    public TreeNode<T> get(T data) {
        TreeNode<T> current = root;
        while (current != null && data.compareTo(current.getData()) != 0) {
            if (data.compareTo(current.getData()) < 0) {
                current = current.getLeft();
            } else {
                current = current.getRight();
            }
        }
        return current;
    }

2. 最大值和最小值

最大值和最小值總是出現在二叉搜尋樹的最左邊和最右邊,所以我們沿著左邊或者右邊一直遍歷下去,直到碰到 null 時停止即可。

最小值程式碼如下(最大值類似):

    /**
     * @return 返回最小資料的結點物件
     */
    public TreeNode<T> getMin() {
        return getMin(root);
    }

    private TreeNode<T> getMin(TreeNode<T> treeNode) {
        while (treeNode.getLeft() != null) {
            treeNode = treeNode.getLeft();
        }
        return treeNode;
    }

3. 前驅值和後繼值

前驅值即樹中比給定資料小的第一個值,後繼值即樹中比給定資料大的第一個值。

例如查詢後繼值,分兩種情況:第一種是該結點的右子樹存在,則後繼者一定是在右子樹的最左結點處;第二種是該節點的右子樹不存在,則需要沿著父結點往上尋找,當某個結點不再是其父結點的右子結點時,其父結點就是我們需要尋找的後繼者。

如果你沒聽懂,那麼可以參考下圖:

第一種情況:例如查詢 6 的後繼者,由於 6 這個結點存在右子樹,所以後繼者即右子樹的最左結點,即 9 這個結點。

第二種情況:例如查詢 13 的後繼者,由於 13 這個結點不存在右子樹,所以我們沿著父結點往上尋找,當某個結點不再是其父結點的右子結點時,其父結點就是我們需要尋找的後繼者。比如 13 的父結點 7 是一個右子結點,繼續往上,父結點 6 是一個左子結點,停止查詢,結點 6 的父結點 15 就是我們需要尋找的後繼者。

後繼值程式碼如下(前驅值類似):

    /**
     * @param data 待查詢的資料
     * @return 返回指向 data 後繼者的結點物件
     */
    public TreeNode<T> getSuccessor(T data) {
        TreeNode<T> current = get(data);
        if (current == null) {
            return null;
        }
        // 如果結點右子樹非空,則後繼者一定在右子樹最左結點
        if (current.getRight() != null) {
            return getMin(current.getRight());
        }
        // 否則沿上遍歷,當 curent不再是parent的右子結點時,後繼者一定出現在parent的父節點
        TreeNode<T> parent = current.getParent();
        while (parent != null && current.isRightChild()) {
            current = parent;
            parent = parent.getParent();
        }
        return parent;
    }

三、插入

插入操作和搜尋操作比較類似,我們也是從根節點開始尋找要插入的位置,如果資料小於結點處的值,則我們繼續在其左子樹中尋找要插入的位置,如果大於結點處的值,則我們繼續在其右子樹中尋找要插入的位置,直到碰到 null 為止,此時 null 的位置即是我們需要插入資料的節點位置。

程式碼如下:

    /**
     * @param data 待插入的資料
     */
    public void insert(T data) {
        TreeNode<T> insertNode = new TreeNode<>(data);
        TreeNode<T> parent = null;
        TreeNode<T> current = root;
        while (current != null) { // 先找到要插入的位置
            parent = current;
            if (data.compareTo(current.getData()) < 0) {
                current = current.getLeft();
            } else {
                current = current.getRight();
            }
        }
        if (parent == null) {
            root = insertNode; // tree is empty
        } else if (insertNode.getData().compareTo(parent.getData()) < 0) {
            parent.setLeft(insertNode);
        } else {
            parent.setRight(insertNode);
        }
    }

四、刪除

刪除操作要麻煩很多,首先大致有三種情況:

  • 待刪除的節點沒有子結點,那麼我們直接刪掉這個結點即可。
  • 待刪除的結點只有一個子結點,那麼我們直接將子結點替換掉刪除節點即可。
  • 待刪除的結點有兩個子結點,那麼我們需要先找到待刪除結點的後繼者,這個後繼者又存在兩種情況,即:

待刪除結點 z 的後繼者為 y,則 y 一定是 z 右子樹的最小結點。如果 y 是 z 的右子結點,那麼直接使用子結點 y 替換 z 即可;如果 y 不是 z 的右子結點,則先用 y 的右子結點 x 替換 y,再用 y 替換 z 即可。

程式碼如下:

    /**
     * @param data 待刪除的資料
     * @return 刪除是否成功。當資料不存在樹中時刪除失敗
     */
    public boolean delete(T data) {
        TreeNode<T> deleteNode = get(data);
        if (deleteNode == null) {
            return false; // 不存在的結點
        }
        if (deleteNode.getLeft() == null) {
            transplant(deleteNode, deleteNode.getRight());
        } else if (deleteNode.getRight() == null) {
            transplant(deleteNode, deleteNode.getLeft());
        } else { // 存在兩個子結點的情況
            // 先找到後繼者
            TreeNode<T> successor = getMin(deleteNode.getRight());
            if (successor.getParent() != deleteNode) {
                transplant(successor, successor.getRight());
                successor.setRight(deleteNode.getRight());
            }
            transplant(deleteNode, successor);
            successor.setLeft(deleteNode.getLeft());
        }
        return true;
    }

    /**
     * 使用新結點替換舊結點
     * @param oldNode 舊結點
     * @param newNode 新結點
     */
    private void transplant(TreeNode<T> oldNode, TreeNode<T> newNode) {
        if (oldNode.getParent() == null) {
            root = newNode; // 舊結點是根節點的情況
        } else if (oldNode.isLeftChild()) {
            oldNode.getParent().setLeft(newNode);
        } else {
            oldNode.getParent().setRight(newNode);
        }
    }

五、完整程式碼

1. TreeNode 結點類

public class TreeNode<T extends Comparable<T>> {

    private T data; // 資料必須是可比較的
    private TreeNode<T> parent; // 父結點
    private TreeNode<T> left; // 左子結點
    private TreeNode<T> right; // 右子結點

    TreeNode(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public TreeNode<T> getParent() {
        return parent;
    }

    public void setParent(TreeNode<T> parent) {
        this.parent = parent;
    }

    public TreeNode<T> getLeft() {
        return left;
    }

    public void setLeft(TreeNode<T> left) {
        this.left = left;
        if (this.left != null) {
            this.left.parent = this;
        }
    }

    public TreeNode<T> getRight() {
        return right;
    }

    public void setRight(TreeNode<T> right) {
        this.right = right;
        if (this.right != null) {
            this.right.parent = this;
        }
    }

    public boolean isRoot() {
        // 是否是根節點
        return parent == null;
    }

    public boolean isLeaf() {
        // 是否是葉子節點,即沒有子結點
        return left == null && right == null;
    }

    public boolean isLeftChild() {
        // 是否是其父結點的左子結點
        if (parent == null) {
            return false;
        }
        return this == parent.left;
    }

    public boolean isRightChild() {
        // 是否是其父結點的右子結點
        if (parent == null) {
            return false;
        }
        return this == parent.right;
    }

    @Override
    public String toString() {
        return "TreeNode [data=" + data +
                ", parent=" + (parent == null ? null : parent.data) +
                ", left=" + (left == null ? null : left.data) +
                ", right=" + (right == null ? null : right.data) + "]";
    }

}

2. BinarySearchTree 二叉搜尋樹類

public class BinarySearchTree<T extends Comparable<T>> {

    private TreeNode<T> root;
    
    /**
     * 將陣列構建為樹
     * @param datas 輸入資料陣列
     */
    public void buildTree(T[] datas) {
        for (int i = 0; i < datas.length; i++) {
            insert(datas[i]);
        }
    }

    /**
     * @param data 待查詢的資料
     * @return 返回指向 data 資料的結點物件,不存在時返回 null
     */
    public TreeNode<T> get(T data) {
        TreeNode<T> current = root;
        while (current != null && data.compareTo(current.getData()) != 0) {
            if (data.compareTo(current.getData()) < 0) {
                current = current.getLeft();
            } else {
                current = current.getRight();
            }
        }
        return current;
    }

    /**
     * @return 返回最小資料的結點物件
     */
    public TreeNode<T> getMin() {
        return getMin(root);
    }

    private TreeNode<T> getMin(TreeNode<T> treeNode) {
        while (treeNode.getLeft() != null) {
            treeNode = treeNode.getLeft();
        }
        return treeNode;
    }

    /**
     * @return 返回最大資料的結點物件
     */
    public TreeNode<T> getMax() {
        return getMax(root);
    }

    private TreeNode<T> getMax(TreeNode<T> treeNode) {
        while (treeNode.getRight() != null) {
            treeNode = treeNode.getRight();
        }
        return treeNode;
    }

    /**
     * @param data 待查詢的資料
     * @return 返回指向 data 後繼者的結點物件
     */
    public TreeNode<T> getSuccessor(T data) {
        TreeNode<T> current = get(data);
        if (current == null) {
            return null;
        }
        // 如果結點右子樹非空,則後繼者一定在右子樹最左結點
        if (current.getRight() != null) {
            return getMin(current.getRight());
        }
        // 否則沿上遍歷,當 curent不再是parent的右子結點時,後繼者一定出現在parent的父節點
        TreeNode<T> parent = current.getParent();
        while (parent != null && current.isRightChild()) {
            current = parent;
            parent = parent.getParent();
        }
        return parent;
    }

    /**
     * @param data 待查詢的資料
     * @return 返回指向 data 前驅者的結點物件
     */
    public TreeNode<T> getPredecessor(T data) {
        TreeNode<T> current = get(data);
        if (current == null) {
            return null;
        }
        // 如果結點左子樹非空,則前驅者一定在左子樹最右結點
        if (current.getLeft() != null) {
            return getMax(current.getLeft());
        }
        // 否則沿上遍歷,當 curent不再是parent的左子結點時,前驅者一定出現在parent的父節點
        TreeNode<T> parent = current.getParent();
        while (parent != null && current.isLeftChild()) {
            current = parent;
            parent = parent.getParent();
        }
        return parent;
    }

    /**
     * @param data 待插入的資料
     */
    public void insert(T data) {
        TreeNode<T> insertNode = new TreeNode<>(data);
        TreeNode<T> parent = null;
        TreeNode<T> current = root;
        while (current != null) { // 先找到要插入的位置
            parent = current;
            if (data.compareTo(current.getData()) < 0) {
                current = current.getLeft();
            } else {
                current = current.getRight();
            }
        }
        if (parent == null) {
            root = insertNode; // tree is empty
        } else if (insertNode.getData().compareTo(parent.getData()) < 0) {
            parent.setLeft(insertNode);
        } else {
            parent.setRight(insertNode);
        }
    }

    /**
     * @param data 待刪除的資料
     * @return 刪除是否成功。當資料不存在樹中時刪除失敗
     */
    public boolean delete(T data) {
        TreeNode<T> deleteNode = get(data);
        if (deleteNode == null) {
            return false; // 不存在的結點
        }
        if (deleteNode.getLeft() == null) {
            transplant(deleteNode, deleteNode.getRight());
        } else if (deleteNode.getRight() == null) {
            transplant(deleteNode, deleteNode.getLeft());
        } else { // 存在兩個子結點的情況
            // 先找到後繼者
            TreeNode<T> successor = getMin(deleteNode.getRight());
            if (successor.getParent() != deleteNode) {
                transplant(successor, successor.getRight());
                successor.setRight(deleteNode.getRight());
            }
            transplant(deleteNode, successor);
            successor.setLeft(deleteNode.getLeft());
        }
        return true;
    }
    
    /**
     * 使用新結點替換舊結點
     * @param oldNode 舊結點
     * @param newNode 新結點
     */
    private void transplant(TreeNode<T> oldNode, TreeNode<T> newNode) {
        if (oldNode.getParent() == null) {
            root = newNode; // 舊結點是根節點的情況
        } else if (oldNode.isLeftChild()) {
            oldNode.getParent().setLeft(newNode);
        } else {
            oldNode.getParent().setRight(newNode);
        }
    }
    
    public void printTree() {
        inorderTraversal(root); // 二叉搜尋樹的中序遍歷將會從小到大輸出
    }
    
    /**
     * 中序遍歷
     * @param root 根結點
     */
    public void inorderTraversal(TreeNode<T> root) {
        if (root == null) return;
        inorderTraversal(root.getLeft());
        System.out.print(root.getData() + " ");
        inorderTraversal(root.getRight());
    }

}

3. 呼叫場景

public class Main {

    public static void main(String[] args) {
        BinarySearchTree<Integer> tree = new BinarySearchTree<>();
        Integer[] arr = new Integer[] { 15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9 };
        tree.buildTree(arr);
        System.out.print("二叉搜尋數當前狀態為:");
        tree.printTree();
        System.out.println("\n最大值為: " + tree.getMax().getData());
        System.out.println("最小值為: " + tree.getMin().getData());
        System.out.println("元素 6 的資訊為:" + tree.get(7));
        System.out.println("樹中比 6 小的數為: " + tree.getPredecessor(6).getData());
        System.out.println("樹中比 13 大的數為: " + tree.getSuccessor(13).getData());
        System.out.println("刪除 6 元素:" + tree.delete(6));
        System.out.print("二叉搜尋數當前狀態為:");
        tree.printTree();
    }

}

執行結果:

二叉搜尋數當前狀態為:2 3 4 6 7 9 13 15 17 18 20 
最大值為: 20
最小值為: 2
元素 6 的資訊為:TreeNode [data=7, parent=6, left=null, right=13]
樹中比 6 小的數為: 4
樹中比 13 大的數為: 15
刪除 6 元素:true
二叉搜尋數當前狀態為:2 3 4 7 9 13 15 17 18 20 

六、總結

以上,我們已經完成了一個二叉搜尋樹的基本功能了。但是細心的同學可能已經發現了,我們的二叉搜尋樹的構建部分極其依賴於插入元素的順序,雖然大多數情況下樹的高度都是接近 lg n 的,但是當插入資料的順序不理想時,該樹將嚴重失衡。例如依次插入 1,2,3,4,5,6,7,8,9 … 這樣依次增大的資料,將導致構建的樹高度 h 和結點數 n 相同。而二叉搜尋樹的操作耗時均和樹的高度成正比。

所以,我們可以增加一些機制,使得二叉搜尋樹是一個平衡二叉樹,這樣將大大增強二叉搜尋樹的效能。