1. 程式人生 > >二叉排序樹的java實現

二叉排序樹的java實現

最近在看JDK原始碼時,發現JDK的J.U.C包下面的很多類都用到了二叉排序樹和紅黑樹,就想著這一塊還是考研的時候看了的,順便就在理論基礎上覆習加用Java做個實現。

二叉排序樹

先來說說這個二叉排序樹的定義和性質:

定義:二叉排序樹或者是一棵空樹,或者是具有下列性質的二叉樹:
(1)若左子樹不空,則左子樹上所有結點的鍵值均小於或等於它的根結點的鍵值;
(2)若右子樹不空,則右子樹上所有結點的鍵值均大於或等於它的根結點的鍵值;
(3)左、右子樹也分別為二叉排序樹;

下面的程式碼將用java實現,並且全部基於遞迴實現(非遞迴演算法複雜一些且效率高)。主要討論BST的如下操作:
查詢、插入、最大鍵、最小鍵、向上取整、向下取整、排名k的鍵、獲取鍵key的排名、刪除最大最小鍵、刪除操作、範圍查詢。

1.結點的資料結構的定義

下面是BST(後文都用BST表示二叉排序樹)中結點的資料結構的定義。

private class Node{
    private Key key;//鍵
    private Value value;//值
    private Node left, right;//指向子樹的連結:包括左子樹和右子樹.
    private int N;//以當前節點為根的子樹的結點總數
//構造器
    public Node(Key key, Value value, int N) {
        this.key = key;
        this.value = value
; this.N = N; } }

此外,對於整個二叉查詢樹來說,有一個根節點,所以在BST類中定義了一個根結點:

private Node root;//二叉查詢樹的根節點

2. 計算二叉排序樹的size

思想: 根據我們資料結構Node中的定義,裡面有一個屬性 N 表示的就是以當前節點為根的子樹的結點總數。所以原始碼如下:

/**
 * 獲取整個二叉查詢樹的大小
 * @return
 */
public int size(){
    return size(root);
}
/**
 * 獲取某一個結點為根結點的二叉查詢樹的大小
 * @param x
 * @return
*/
private int size(Node x){ if(x == null){ return 0; } else { return x.N; } }

3. 查詢和插入

在實現BST類的時候,BST繼承自Comparable介面的,實現compareTo()函式。因為我們知道二叉查詢樹的鍵值是有有序的,左子樹小於根節點,右子樹大於根節點。所以實現Comparable介面,那麼我們就很容易根據key找到插入的位置,而且對於BTS來說,插入的位置都是在葉子節點處。對於插入和查詢都是基於鍵值的比較。下面是原始碼:

/**
 * 查詢:通過key獲取value
 * @param key
 * @return
 */
public Value get(Key key){
    return get(root, key);
}
/**
 * 在以 x 為根節點的子樹中查詢並返回Key所對應的值,如果找不到就返回null
 * 遞迴實現
 * @param x
 * @param key
 * @return
 */
private Value get(Node x, Key key){
    if(x == null){
        return null;
    }
    //鍵值的比較
    int cmp = key.compareTo(x.key);
    if(cmp<0){
        return get(x.left, key);
    }else if(cmp>0){
        return get(x.right, key);
    } else{
        return x.value;
    }
}


/**
 * 插入:設定鍵值對
 * @param key
 * @param value
 */
public void put(Key key, Value value){
    root = put(root, key, value);
}
/**
 * key如果存在以 x 為根節點的子樹中,則更新它的值;
 * 否則將key與value鍵值對插入並建立一個新的結點.
 * @param x
 * @param key
 * @param value
 * @return
 */
private Node put(Node x, Key key, Value value){
    if( x==null ){
        x = new Node(key, value, 1);
        return x;
    }
    int cmp = key.compareTo(x.key);
    if(cmp<0){
        x.left = put(x.left, key, value);
    }else if(cmp>0){
        x.right = put(x.right, key, value);
    } else{
        x.value=value;//更新value的值
    }
    //設定根節點的N屬性
    x.N = size(x.left) + size(x.right) + 1;

    return x;
}

4. 最大鍵和最小鍵的實現

求BST中的最大鍵值和最小鍵值。根據BSt的特性,其實原理很簡單:最小值就是最左下邊的一個節點。最大鍵值就是最右下邊的結點。原始碼如下:

/**
 * 最小鍵
 */
public Key min(){
    return min(root).key;
}
/**
 * 返回結點x為root的二叉排序樹中最小key值的Node
 * @param x
 * @return 返回樹的最小key的結點
 */
private Node min(Node x){
    if(x.left == null){
        return x;
    }else{
        return min(x.left);
    }
}

/**
 * 最大鍵
 */
public Key max(){
    return max(root).key;
}
/**
 * 返回結點x為root的二叉排序樹中最大key值的Node
 * @param x
 * @return
 */
private Node max(Node x){
    if(x.right == null){//右子樹為空,則根節點是最大的
        return x;
    }else{
        return max(x.right);
    }
}

5. Key值向下取整和向上取整

所謂的向下取整和向上取整,就是給定鍵值key,向下取整的意思就是找出小於等於當前key的的Key值。向上取整的意思就是找出大於等於當前key的的Key值。實現也是基於Comparable介面的,具體的原始碼如下:

/**
 * key向下取整
 */
public Key floor(Key key){
    Node x = floor(root, key);
    if(x == null){
        return null;
    }
    return x.key;
}
/**
 * 以x 為根節點的二叉排序樹,查詢以引數key的向下取整的Node
 * @param x
 * @param key
 * @return
 */
private Node floor(Node x, Key key){
    if(x == null){
        return null;
    }

    int cmp = key.compareTo(x.key);
    if(cmp == 0){
        return x;
    }
    if(cmp < 0){//說明key引數小於x結點的key,所以向下取整結點在左子樹
        return floor(x.left, key);
    }
    //向下取整在右子樹,
    Node t = floor(x.right, key);
    if( t!= null){
        return t;
    }else {
        return x;
    }
}

/**
 * key向上取整
 */
public Key ceiling(Key key){
    Node x = ceiling(root, key);
    if(x == null){
        return null;
    }
    return x.key;
}
/**
 * 以x 為根節點的二叉排序樹,查詢以引數key的向上取整的Node
 * @param x
 * @param key
 * @return
 */
private Node ceiling(Node x, Key key){
    if(x == null){
        return null;
    }

    int cmp = key.compareTo(x.key);
    if(cmp == 0){
        return x;
    }
    if(cmp > 0){//說明key引數大於x結點的key,所以向上取整結點在右子樹
        return ceiling(x.right, key);
    }
    //向上取整在左子樹,
    Node t = ceiling(x.left, key);
    if( t!= null){
        return t;
    }else {
        return x;
    }
}

6. 獲取排名

我們經常可能會遇到需要獲取到排名為k的結點或則獲取某一個結點的排名,具體的實現也是基於Comparable介面的比較。

/**
 * 排名為k的結點的key
 */
public Key select(int k){
    Node x = select(root, k);
    if(x == null){
        return null;
    }
    return x.key;
}
/**
 * 返回排名為k的結點
 * @param x 根節點
 * @param k 排名
 * @return
 */
private Node select(Node x, int k){
    if(x == null){
        return null;
    }
    int t = size(x.left);//獲取左子樹的節點數
    if(t == k) {//左子樹節點數和k相同
        return x.left ;
    } else if( t+1 == k ){//左子樹結點數比k小一.
        return x;
    } else if(t>k){//排名k的結點在左子樹
        return select(x.left, k);
    }else{
        //排名k的在右子樹
        return select(x.right, k-t-1);
    }
}

/**
 * 返回給定鍵key的排名
 */
public int rank(Key key){
    return rank(root, key);
}
/**
 * 在二叉排序樹x上返回key的排名
 * @param x
 * @param key
 * @return
 */
private int rank(Node x, Key key){
    if(x == null){
        return 0;
    }
    int cmp = key.compareTo(x.key);
    if(cmp < 0){
        //key鍵小於root的key,所以key在左子樹中
        return rank(x.left, key);
    } else if(cmp>0){
        //key大於root的key,所以key在右子樹中
        return 1+size(x.left)+rank(x.right, key);
    } else{
        return size(x.left)+1;
    }
}

7. 刪除最小鍵值和最大鍵值的結點。

刪除二叉排序樹中的最大鍵值和最小鍵值的結點。這裡的思想和找到最大鍵值和最小鍵值結點幾乎一樣。只是這裡需要刪除該結點。由於最大鍵值和最小鍵值的位置的特殊性,都在葉子結點,所以這裡的刪除都是比較簡單的,不涉及到子樹的移動。原始碼如下:

/**
 * 刪除鍵值最小結點
 */
public void deleteMin(){
    //刪除root二叉查詢樹中的最小key的結點,其實也就是最左邊的結點
    root = deleteMin(root);
}
/**
 * 刪除鍵值最小結點
 * @param x
 * @return 返回新的二叉查詢樹的根節點
 */
private Node deleteMin(Node x){
    if(x.left == null){
        return x.right;//刪除根節點,這時返回的是新的二叉查詢樹的根節點
    }
    x.left = deleteMin(x.left);
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

/**
 * 刪除:鍵值最大結點
 */
public void deleteMax(){
    //刪除root二叉查詢樹中的最小key的結點,其實也就是最左邊的結點
    root = deleteMax(root);
}
/**
 * 刪除
 * @param x
 * @return 返回新的二叉查詢樹的根節點
 */
private Node deleteMax(Node x){
    if(x.right == null){//右子樹為空
        return x.left;//刪除根節點,這時返回的是新的二叉查詢樹的根節點
    }
    x.right = deleteMax(x.right);
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

8. 刪除任意結點

這裡的刪除指刪除二叉排序樹中任意位置的結點x,這個時候的結點x就有如下4種情況:
首先做個約定:待刪除的結點是結點x,其父結點是結點p,左子樹結點是l,右子樹結點是r,兄弟結點為b。其中NIL均為虛擬的null結點。(以上結點均在存在的情況下)
1、當待刪除結點x位於葉子結點位置:
這時直接刪除該葉子結點x,並將其父結點p指向該結點的域設定為null。
待刪除結點x位於葉子結點
這時只要刪除葉子結點x,然後將p的左結點指向NIL 結點:
刪除葉子結點之後

2、當待刪除結點x只有一個子結點時(該結點只有左子樹或則右子樹:即左子樹和右子樹中有一個為空),這時將x結點的父結點p指向待刪除的結點x的指標直接指向x結點的唯一兒子結點(l或則是r),然後刪除x結點就OK了。下圖只演示只有左子樹的時候,只有右子樹時候相同:
待刪除結點x只有一個左子結點時
這時只用刪除x結點,並將p結點左子樹指向x的子結點L. 如下圖:
刪除結點x之後

3、當待刪除結點x有兩個子結點的時候,這時候刪除待刪除結點x是最麻煩的。一個最經典的方案是:用刪除結點x的後繼結點填補它的位置。因為x有一個右子結點,因此它的後繼結點就是右子樹中的最小結點,這樣替換就能保證有序性,因為x.key和它的後繼之間不存在其他的鍵。我們用下面的4個步驟解決x替換為它後繼結點的任務:
1)將指向即將被刪除的結點x的連結儲存為t;

2)將x指向它的後繼結點min(x.right);

3)將x的右連結指向deleteMin(t.right) (這裡也就是右子樹刪除最小結點之後的),也就是刪除最小結點後,其右子樹所有節點都仍大於x.key的右子二叉查詢樹。

4)將x的左連結(本為空)設為t.left(其下所有的鍵都小於被刪除的結點和它的後繼結點)。

原始碼如下:

/**
 * 刪除鍵key結點.
 * @param key
 */
public void delete(Key key){
    root = delete(root, key);
}
/**
 * 刪除以x為根結點的二叉查詢樹的key鍵的結點
 * @param x
 * @param key
 * @return 新的二叉查詢樹的根節點
 */
private Node delete(Node x, Key key){
    if( x == null ){
        return null;
    }
    int cmp = key.compareTo(x.key);
    if (cmp < 0) {
        x.left = delete(x.left, key);
    } else if(cmp > 0){
        x.right = delete(x.right, key);
    } else {//這時刪除根節點x
        if(x.left == null){
            return x.right;
        }
        if(x.right == null){
            return x.left;
        }
        //根節點有左右子樹
        Node t = x;
        //1. 先求出x的右子樹中最小鍵值的結點並讓x指向它.
        x = min(t.right);
        //2. 將t的右子樹刪除最小的結點之後的根節點返回
        x.right = deleteMin(t.right);
        //3. 將t的左子樹給x的左子樹
        x.left = t.left;
    }
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

整個實現的原始碼: