二叉排序樹的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,然後將p的左結點指向NIL 結點:
2、當待刪除結點x只有一個子結點時(該結點只有左子樹或則右子樹:即左子樹和右子樹中有一個為空),這時將x結點的父結點p指向待刪除的結點x的指標直接指向x結點的唯一兒子結點(l或則是r),然後刪除x結點就OK了。下圖只演示只有左子樹的時候,只有右子樹時候相同:
這時只用刪除x結點,並將p結點左子樹指向x的子結點L. 如下圖:
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;
}