二叉搜索樹的java實現

 轉載請註明出處node

1、概念

  二叉搜索樹也成二叉排序樹,它有這麼一個特色,某個節點,若其有兩個子節點,則必定知足,左子節點值必定小於該節點值,右子節點值必定大於該節點值,對於非基本類型的比較,能夠實現Comparator接口,在本文中爲了方便,採用了int類型數據進行操做。算法

  要想實現一顆二叉樹,確定得從它的增長提及,只有把樹構建出來了,才能使用其餘操做。函數

2、二叉搜索樹構建

   談起二叉樹的增長,確定先得構建一個表示節點的類,該節點的類,有這麼幾個屬性,節點的值,節點的父節點、左節點、右節點這四個屬性,代碼以下性能

 1     static class Node{
 2         Node parent;
 3         Node leftChild;
 4         Node rightChild;
 5         int val;
 6         public Node(Node parent, Node leftChild, Node rightChild,int val) {
 7             super();
 8             this.parent = parent;
 9             this.leftChild = leftChild;
10             this.rightChild = rightChild;
11             this.val = val;
12         }
13         
14         public Node(int val){
15             this(null,null,null,val);
16         }
17         
18         public Node(Node node,int val){
19             this(node,null,null,val);
20         }
21 
22     }

        這裏採用的是內部類的寫法,構建完節點值後,再對整棵樹去構建,一棵樹,先得有根節點,再能延伸到餘下子節點,那在這棵樹裏,也有一些屬性,好比基本的根節點root,樹中元素大小size,這兩個屬性,若是採用了泛型,可能還得增長Comparator屬性,或提供其一個默認實現。具體代碼以下this

 public class SearchBinaryTree {
    
    private Node root;
    private int size;
    public SearchBinaryTree() {
        super();
    }
}

3、增長

    當要進行添加元素的時候,得考慮根節點的初始化,通常狀況有兩種、當該類的構造函數一初始化就對根節點root進行初始化,第二種、在進行第一次添加元素的時候,對根節點進行添加。理論上兩個均可以行得通,但一般採用的是第二種懶加載形式。spa

    在進行添加元素的時候,有這樣幾種狀況須要考慮3d

       1、添加時判斷root是否初始化,若沒初始化,則初始化,將該值賦給根節點,size加一。code

       2、由於二叉樹搜索樹知足根節點值大於左節點,小於右節點,須要將插入的值,先同根節點比較,若大,則往右子樹中進行查找,若小,則往左子樹中進行查找。直到某個子節點。blog

       這裏的插入實現,能夠採用兩種,1、遞歸、2、迭代(即經過while循環模式)。排序

  3.一、遞歸版本插入

 1    public boolean add(int val){
 2         if(root == null){
 3             root = new Node(val);
 4             size++;
 5             return true;
 6         }
 7         Node node = getAdapterNode(root, val);
 8         Node newNode = new Node(val);
 9         if(node.val > val){
10             node.leftChild = newNode;
11             newNode.parent = node;
12         }else if(node.val < val){
13             node.rightChild = newNode;
14             newNode.parent = node;
15         }else{
16             // 暫不作處理
17         }
18         size++;19         return true;
20     }
21     
22     /**
23      * 獲取要插入的節點的父節點,該父節點知足如下幾種狀態之一
24      *  一、父節點爲子節點
25      *  二、插入節點值比父節點小,但父節點沒有左子節點
26      *  三、插入節點值比父節點大,但父節點沒有右子節點
27      *  四、插入節點值和父節點相等。
28      *  五、父節點爲空
29      *  若是知足以上5種狀況之一,則遞歸中止。
30      * @param node
31      * @param val
32      * @return
33      */
34     private Node getAdapterNode(Node node,int val){
35         if(node == null){
36             return node;
37         }
38         // 往左子樹中插入,但沒左子樹,則返回
39         if(node.val > val && node.leftChild == null){
40             return node;
41         }
42         // 往右子樹中插入,但沒右子樹,也返回
43         if(node.val < val && node.rightChild == null){
44             return node;
45         }
46         // 該節點是葉子節點,則返回
47         if(node.leftChild == null && node.rightChild == null){
48             return node;
49         }
50         
51         if(node.val > val && node.leftChild != null){
52             return getAdaptarNode(node.leftChild, val);
53         }else if(node.val < val && node.rightChild != null){
54             return getAdaptarNode(node.rightChild, val);
55         }else{
56             return node;
57         }
58     }

 

   使用遞歸,先找到遞歸的結束點,再去把整個問題化爲子問題,在上述代碼裏,邏輯大體是這樣的,先判斷根節點有沒有初始化,沒初始化則初始化,完成後返回,以後經過一個函數去獲取適配的節點。以後進行插入值。

3.二、迭代版本

public boolean put(int val){
        return putVal(root,val);
    }
    private boolean putVal(Node node,int val){
        if(node == null){// 初始化根節點
            node = new Node(val);
            root = node;
            size++;
            return true;
        }
        Node temp = node;
        Node p;
        int t;
        /**
         * 經過do while循環迭代獲取最佳節點,
         */
        do{ 
            p = temp;
            t = temp.val-val;
            if(t > 0){
                temp = temp.leftChild;
            }else if(t < 0){
                temp = temp.rightChild;
            }else{
                temp.val = val;
                return false;
            }
        }while(temp != null);
        Node newNode = new Node(p, val);
        if(t > 0){
            p.leftChild = newNode;
        }else if(t < 0){
            p.rightChild = newNode;
        }
        size++;
        return true;
    }

 

原理其實和遞歸同樣,都是獲取最佳節點,在該節點上進行操做。

論起性能,確定迭代版本最佳,因此通常狀況下,都是選擇迭代版本進行操做數據。

4、刪除

    能夠說在二叉搜索樹的操做中,刪除是最複雜的,要考慮的狀況也相對多,在常規思路中,刪除二叉搜索樹的某一個節點,確定會想到如下四種狀況,

 

   一、要刪除的節點沒有左右子節點,如上圖的D、E、G節點

   二、要刪除的節點只有左子節點,如B節點

   三、要刪除的節點只有右子節點,如F節點

   四、要刪除的節點既有左子節點,又有右子節點,如 A、C節點

對於前面三種狀況,能夠說是比較簡單,第四種複雜了。下面先來分析第一種

 如果這種狀況,好比 刪除D節點,則能夠將B節點的左子節點設置爲null,若刪除G節點,則可將F節點的右子節點設置爲null。具體要設置哪一邊,看刪除的節點位於哪一邊。

第二種,刪除B節點,則只需將A節點的左節點設置成D節點,將D節點的父節點設置成A便可。具體設置哪一邊,也是看刪除的節點位於父節點的哪一邊。

第三種,同第二種。

第四種,也就是以前說的有點複雜,好比要刪除C節點,將F節點的父節點設置成A節點,F節點左節點設置成E節點,將A的右節點設置成F,E的父節點設置F節點(也就是將F節點替換C節點),還有一種,直接將E節點替換C節點。那採用哪種呢,若是刪除節點爲根節點,又該怎麼刪除?

 對於第四種狀況,能夠這樣想,找到C或者A節點的後繼節點,刪除後繼節點,且將後繼節點的值設置爲C或A節點的值。先來補充下後繼節點的概念。

一個節點在整棵樹中的後繼節點必知足,大於該節點值得全部節點集合中值最小的那個節點,即爲後繼節點,固然,也有可能不存在後繼節點。

可是對於第四種狀況,後繼節點必定存在,且必定在其右子樹中,並且還知足,只有一個子節點或者沒有子節點二者狀況之一。具體緣由能夠這樣想,由於後繼節點要比C節點大,又由於C節點左右子節必定存在,因此必定存在右子樹中的左子節點中。就好比C的後繼節點是F,A的後繼節點是E。

有了以上分析,那麼實現也比較簡單了,代碼以下

 1 public boolean delete(int val){
 2         Node node = getNode(val);
 3         if(node == null){
 4             return false;
 5         }
 6         Node parent = node.parent;
 7         Node leftChild = node.leftChild;
 8         Node rightChild = node.rightChild;
 9         //如下全部父節點爲空的狀況,則代表刪除的節點是根節點
10         if(leftChild == null && rightChild == null){//沒有子節點
11             if(parent != null){
12                 if(parent.leftChild == node){
13                     parent.leftChild = null;
14                 }else if(parent.rightChild == node){
15                     parent.rightChild = null;
16                 }
17             }else{//不存在父節點,則代表刪除節點爲根節點
18                 root = null;
19             }
20             node = null;
21             return true;
22         }else if(leftChild == null && rightChild != null){// 只有右節點
23             if(parent != null && parent.val > val){// 存在父節點,且node位置爲父節點的左邊
24                 parent.leftChild = rightChild;
25             }else if(parent != null && parent.val < val){// 存在父節點,且node位置爲父節點的右邊
26                 parent.rightChild = rightChild;
27             }else{
28                 root = rightChild;
29             }
30             node = null;
31             return true;
32         }else if(leftChild != null && rightChild == null){// 只有左節點
33             if(parent != null && parent.val > val){// 存在父節點,且node位置爲父節點的左邊
34                 parent.leftChild = leftChild;
35             }else if(parent != null && parent.val < val){// 存在父節點,且node位置爲父節點的右邊
36                 parent.rightChild = leftChild;
37             }else{
38                 root = leftChild;
39             }
40             return true;
41         }else if(leftChild != null && rightChild != null){// 兩個子節點都存在
42             Node successor = getSuccessor(node);// 這種狀況,必定存在後繼節點
43             int temp = successor.val;
44             boolean delete = delete(temp);
45             if(delete){
46                 node.val = temp;
47             }
48             successor = null;
49             return true;
50         }
51         return false;
52     }
53     
54     /**
55      * 找到node節點的後繼節點
56      * 一、先判斷該節點有沒有右子樹,若是有,則從右節點的左子樹中尋找後繼節點,沒有則進行下一步
57      * 二、查找該節點的父節點,若該父節點的右節點等於該節點,則繼續尋找父節點,
58      *   直至父節點爲Null或找到不等於該節點的右節點。
59      * 理由,後繼節點必定比該節點大,若存在右子樹,則後繼節點必定存在右子樹中,這是第一步的理由
60      *      若不存在右子樹,則也可能存在該節點的某個祖父節點(即該節點的父節點,或更上層父節點)的右子樹中,
61      *      對其迭代查找,如有,則返回該節點,沒有則返回null
62      * @param node
63      * @return
64      */
65     private Node getSuccessor(Node node){
66         if(node.rightChild != null){
67             Node rightChild = node.rightChild;
68             while(rightChild.leftChild != null){
69                 rightChild = rightChild.leftChild;
70             }
71             return rightChild;
72         }
73         Node parent = node.parent;
74         while(parent != null && (node == parent.rightChild)){
75             node = parent;
76             parent = parent.parent;
77         }
78         return parent;
79     }

具體邏輯,看上面分析,這裏不做文字敘述了,

除了這種實現,在算法導論書中,提供了另一種實現。

 1    public boolean remove(int val){
 2         Node node = getNode(val);
 3         if(node == null){
 4             return false;
 5         }
 6         if(node.leftChild == null){// 一、左節點不存在,右節點可能存在,包含兩種狀況  ,兩個節點都不存在和只存在右節點
 7             transplant(node, node.rightChild);
 8         }else if(node.rightChild == null){//二、左孩子存在,右節點不存在
 9             transplant(node, node.leftChild);
10         }else{// 三、兩個節點都存在
11             Node successor = getSuccessor(node);// 獲得node後繼節點 
12             if(successor.parent != node){// 後繼節點存在node的右子樹中。
13                 transplant(successor, successor.rightChild);// 用後繼節點的右子節點替換該後繼節點
14                 successor.rightChild = node.rightChild;// 將node節點的右子樹賦給後繼節點的右節點,即相似後繼與node節點調換位置
15                 successor.rightChild.parent = successor;// 接着上一步  給接過來的右節點的父引用複製
16             }
17             transplant(node, successor);
18             successor.leftChild = node.leftChild;
19             successor.leftChild.parent = successor;
20         }
21         return true;
22     }
23     /**
24      * 將child節點替換node節點
25      * @param root    根節點
26      * @param node    要刪除的節點
27      * @param child   node節點的子節點
28      */
29     private void transplant(Node node,Node child){
30         /**
31          * 一、先判斷 node是否存在父節點
32          *    一、不存在,則child替換爲根節點
33          *    二、存在,則繼續下一步
34          * 二、判斷node節點是父節點的那個孩子(即判斷出 node是右節點仍是左節點),
35          *    得出結果後,將child節點替換node節點 ,即若node節點是左節點 則child替換後 也爲左節點,不然爲右節點
36          * 三、將node節點的父節點置爲child節點的父節點
37          */
38         
39         if(node.parent == null){
40             this.root = child;
41         }else if(node.parent.leftChild == node){
42             node.parent.leftChild = child;
43         }else if(node.parent.rightChild == node){
44             node.parent.rightChild = child;
45         }
46         if(child != null){
47             child.parent = node.parent;
48         }
49     }

 

5、查找

  查找也比較簡單,其實在增長的時候,已經實現了。實際狀況中,這部分能夠抽出來單獨方法。代碼以下

 1   public Node getNode(int val){
 2         Node temp = root;
 3         int t;
 4         do{
 5             t = temp.val-val;
 6             if(t > 0){
 7                 temp = temp.leftChild;
 8             }else if(t < 0){
 9                 temp = temp.rightChild;
10             }else{
11                 return temp;
12             }
13         }while(temp != null);
14         return null;
15     }

 

6、二叉搜索樹遍歷

  在瞭解二叉搜索樹的性質後,很清楚的知道,它的中序遍歷是從小到大依次排列的,這裏提供中序遍歷代碼

 1    public void print(){
 2         print(root);
 3     }
 4     private void print(Node root){
 5         if(root != null){
 6             print(root.leftChild);
 7             System.out.println(root.val);// 位置在中間,則中序,若在前面,則爲先序,不然爲後續
 8             print(root.rightChild);
 9         }
10     }

 

-------------------------------------------------------------------------------------------------------華麗分割線----------------------------------------------------------------------------------------------

 以上都是我的看法,如有錯誤或不足之處,還望指正!!!

相關文章
相關標籤/搜索