1. 程式人生 > >資料機構與演算法:二叉查詢樹(Binary Search Tree)Java實現

資料機構與演算法:二叉查詢樹(Binary Search Tree)Java實現

個人總結,如有錯誤,感謝指正

二叉查詢樹(Binary Search Tree)

一、簡介

二叉樹(Binary Tree):每個節點最多有兩個子節點的樹。

二叉查詢樹(binary srarch tree):具有如下性質的二叉樹稱為二叉查詢樹:

  • 對樹中任意節點A,若左子樹不空,則左子樹中所有節點的值比A的值小
  • 對樹中任意節點A,若右子樹不空,則右子樹中所有節點的值比A的值大
  • 沒有鍵值相等的的節點

    如下結構是一個二叉查詢樹:
    二叉查詢樹

對於動態陣列(ArrayList)與連結串列(LinkedList)結構,動態陣列在查詢方面效率較高,連結串列結構在插入方面效率較高,而二叉查詢樹融合了上面兩者的優勢。

二、實現

下面用Java實現二叉查詢樹,包含二叉查詢樹定義、前序/中序/後序遍歷、查詢、最大值、最小值、插入、刪除

定義

用一個內部類定義二叉樹節點BinaryNode,它包含指向左右兩個子節點的left、right,以及表示節點值的key,key需要支援排序。

public class BinarySearchTree<T extends Comparable<T>> {
    //二叉查詢樹根節點
    private BinaryNode<T> root;

    //二叉樹節點定義
    private static class
BinaryNode<T extends Comparable<T>> {
T key; //節點值 BinaryNode<T> left; //左節點 BinaryNode<T> right; //右節點 public BinaryNode(T key){ this(key, null, null); } public BinaryNode(T key, BinaryNode<T> left, BinaryNode<T> right) { this
.key = key; this.left = left; this.right = right; } } ... }

遍歷

二叉查詢樹遍歷包含前序、中序、後序遍歷。這裡的“前、中、後”都是針對“當前節點”而言,遍歷順序分別如下:
前序遍歷:當前節點、左節點、右節點;
中序遍歷:左節點、當前節點、右節點;
後序遍歷:左節點、右節點、當前節點。
遍歷每個節點都按上述順序進行。

/**
 * 前序遍歷
 * @param node 待遍歷二叉查詢樹BST根節點
 */
private void preOrder(BinaryNode<T> node) {
    if (node != null) {
        System.out.print(node.key + " ");
        preOrder(node.left);
        preOrder(node.right);
    }
}

/**
 * 前序遍歷
 */ 
public void preOrder() {
    preOrder(this.root);
}

/**
 * 中序遍歷
 * @param node 待遍歷BST根節點
 */
private void midOrder(BinaryNode<T> node) {
    if (node != null) {
        midOrder(node.left);
        System.out.print(node.key + " ");
        midOrder(node.right);
    }
}

/**
 * 中序遍歷
 */
public void midOrder() {
    midOrder(this.root);
}

/**
 * 後序遍歷
 * @param node 待遍歷BST根節點
 */
private void postOrder(BinaryNode<T> node) {
    if (node != null) {
        postOrder(node.left);
        postOrder(node.right);
        System.out.print(node.key + " ");
    }
}

/**
 * 後序遍歷
 */
public void postOrder() {
    postOrder(this.root);
}

查詢

在二叉查詢樹BST中查詢值為key的節點,將key和節點的key進行比較,如果小於,則在左子樹中查詢,如果大於,則在右子樹中查詢,如果等於,則查詢成功。下面是遞迴和非遞迴兩種實現方式:

/**
 * (遞迴實現)在root為根節點的二叉樹中查詢節點值為key的節點
 * @param root
 * @param key
 * @return
 */
private BinaryNode<T> search(BinaryNode<T> root, T key) {
    if (root == null) {
        return null;
    }
    //如果key小於當前節點值,則在左樹中查詢
    if (key.compareTo(root.key) < 0) {
        return search(root.left, key);
        //如果key小於當前節點值,則在左樹中查詢
    } else if (key.compareTo(root.key) > 0) {
        return search(root.right, key);
        //如果key等於當前節點值,直接返回
    } else {
        return root;
    }
}

/**
 * (非遞迴實現)在root為根節點的二叉樹中查詢節點值為key的節點
 * @param root
 * @param key
 * @return
 */
private BinaryNode<T> cycleSearch(BinaryNode<T> root, T key) {
    while (root != null) {
        //如果key小於當前節點值,則在左樹中查詢
        if (key.compareTo(root.key) < 0) {
            root = root.left;
        //如果key小於當前節點值,則在左樹中查詢
        } else if (key.compareTo(root.key) > 0) {
            root = root.right;
        //如果key等於當前節點值,直接返回
        } else {
            return root;
        }
    }
    return root;
}

/**
 * 查詢
 */
public BinaryNode<T> search(T key) {
    return search(this.root, key);
    //return cycleSearch(this.root, key);
}   

最小值、最大值

因為二叉查詢樹左子樹的值都比根節點小,右子樹的值都比根節點大,所以最小值即為最左邊節點的值,最大值即為最右邊節點的值

/**
 * 查詢最小值,即最左邊節點值
 */
private T findMin(BinaryNode<T> root){
    if(root == null){
        return null;
    }
    while(root.left != null){
        root = root.left;
    }
    return root.key;
}

/**
 * 最小值
 */
public T findMin(){
    return findMin(this.root);
}

/**
 * 查詢最大值,即查詢最右節點值
 */
private T findMax(BinaryNode<T> root){
    if(root == null){
        return null;
    }
    while(root.right != null){
        root = root.right;
    }
    return root.key;
}

/**
 * 最大值
 */    
public T findMax(){
    return findMax(this.root);
}

插入

在二叉查詢樹中插入新節點newNode,將新節點的值和當前節點值進行比較,如果小於,在左邊插入,如果大於,在右邊插入。

/**
 * 在二叉查詢樹bst中插入新節點newNode
 * @param bst
 * @param newNode
 * @return
 */
private boolean insert(BinarySearchTree<T> bst, BinaryNode<T> newNode){
    //如果root為空,則新節點作為根節點
    BinaryNode<T> root = bst.root;
    if(root == null){
        bst.root = newNode;
        return true;
    }
    while(root != null){
        int cmp = newNode.key.compareTo(root.key);
        //小於,在左邊插入
        if(cmp < 0){
            if(root.left == null){
                root.left = newNode;
                return true;
            }else{
                root = root.left;
            }
        //大於,在右邊插入
        }else if(cmp > 0){
            if(root.right == null){
                root.right = newNode;
                return true;
            }else{
                root = root.right;
            }
        //等於,則插入失敗,返回false
        }else{
            return false;
        }
    }
    return false;
}

/**
 * 插入
 */
public boolean insert(BinaryNode<T> newNode){
    return insert(this, newNode);
}

刪除

從二叉查詢樹中刪除節點時,分三種情況:

  • 待刪除節點為葉節點,則直接刪除
  • 待刪除節點只有一個子節點,則用子節點的值代替該節點的值,然後刪除該子節點
  • 待刪除節點有兩個子節點,這種情況相對比較負責,一般用其右子樹中最小節點的值替代該節點值,然後刪除該最小節點。

懶惰刪除:當需要從樹中刪除某個節點時,並不會真的將該節點從樹中移除,而是仍留在樹中,只是被標記為已刪除,這樣在對樹進行操作時,遇到被標記為已刪除的節點時,直接跳過。這樣,可以提高刪除操作效率。

/**
 * 從二叉查詢樹bst中刪除值為key的節點
 * @param bst
 * @param key
 * @return
 */
private boolean remove(BinarySearchTree<T> bst, T key){
    BinaryNode<T> tRoot = bst.root;
    if(tRoot == null){
        return false;
    }
    BinaryNode<T> pNode = tRoot;
    BinaryNode<T> rmNode = tRoot;
    //是否左節點
    boolean isLeft = false;
    while(rmNode != null){
        int cmp = key.compareTo(rmNode.key);
        if(cmp < 0){
            isLeft = true;
            pNode = rmNode;
            rmNode = rmNode.left;
        } else if(cmp > 0){
            isLeft = false;
            pNode = rmNode;
            rmNode = rmNode.right;
        } else{
            //葉節點,直接刪除
            if(rmNode.left == null && rmNode.right == null){
                if(rmNode == tRoot){
                    tRoot = null;
                }else if(isLeft){
                    pNode.left = null;
                }else{
                    pNode.right = null;
                }
            }
            //只有左節點
            else if(rmNode.left != null && rmNode.right == null){
                if(rmNode == tRoot){
                    tRoot = null;
                }else if(isLeft){
                    pNode.left = rmNode.left;
                }else{
                    pNode.right = rmNode.left;
                }
            }
            //只有右節點
            else if(rmNode.left == null && rmNode.right != null){
                if(rmNode == tRoot){
                    tRoot = null;
                }else if(isLeft){
                    pNode.left = rmNode.right;
                }else{
                    pNode.right = rmNode.right;
                }
            }
            //兩個節點
            else if(rmNode.left != null && rmNode.right != null){
                //被刪除節點右子樹沒有左節點,直接用右節點替換
                if(rmNode.right.left == null){
                    rmNode.key = rmNode.right.key;
                    rmNode.right = rmNode.right.right;
                    return true;
                }
                //右子樹最小節點
                BinaryNode<T> currNode = rmNode.right;
                //替代節點
                BinaryNode<T> replaceNode = rmNode.right;
                //替代節點父節點
                BinaryNode<T> repParent = rmNode.right;
                while(currNode != null){
                    repParent = replaceNode;
                    replaceNode = currNode;
                    currNode = currNode.left;
                }

                repParent.left = replaceNode.right;
                rmNode.key = replaceNode.key;
                if(rmNode == tRoot){
                    tRoot = null;
                }
            }
            return true;
        }
    }
    return false;
}

/**
 * 刪除
 */
public boolean remove(T key){
    return this.remove(this, key);
}

測試

呼叫上面的實現方法進行測試,使用陣列 [8,3,10,6,7,9,17,2,53,23] 作為二叉查詢樹資料:

public static void main(String[] args){
     Integer[] intArr = new Integer[]{8,3,10,6,7,9,17,2,53,23};
     BinarySearchTree<Integer> bst = new BinarySearchTree<Integer>();
     //構造二叉樹,迴圈插入資料
     for(int num : intArr){
         bst.insert(new BinaryNode<Integer>(num));
     }
     System.out.println("前序遍歷:");
     bst.preOrder();
     System.out.println("\n中序遍歷:");
     bst.midOrder();
     System.out.println("\n後序遍歷:");
     bst.postOrder();

     System.out.println("\n最大值:" + bst.findMax());
     System.out.println("最小值:" + bst.findMin());
     System.out.println("查詢6:" );
     BinaryNode<Integer> node6 = bst.search(6);
     System.out.println(node6 == null ? "" : node6.key);
     System.out.println("查詢33:" );
     BinaryNode<Integer> node33 = bst.search(33);
     System.out.println(node33 == null ? "" : node33.key);

     System.out.println("刪除2:" );
     bst.remove(2);
     //System.out.println("刪除6:" );
     //bst.remove(6);
     //System.out.println("刪除10:" );
     //bst.remove(10);
     bst.midOrder();
}

使用陣列[8,3,10,6,7,9,17,2,53,23] 構造二叉查詢樹如下:
這裡寫圖片描述

測試結果

前序遍歷:
8 3 2 6 7 10 9 17 53 23 
中序遍歷:
2 3 6 7 8 9 10 17 23 53 
後序遍歷:
2 7 6 3 9 23 53 17 10 8 
最大值:53
最小值:2
查詢6:
6
查詢33:
沒有找到值為33的節點

刪除2:
3 6 7 8 9 10 17 23 53 
刪除6:
2 3 7 8 9 10 17 23 53
刪除10:
2 3 6 7 8 9 17 23 53 

以上就是二叉查詢樹及其Java實現,如有錯誤,感謝指正。