1. 程式人生 > >7 二分搜尋樹的原理與Java原始碼實現

7 二分搜尋樹的原理與Java原始碼實現

1 折半查詢法

瞭解二叉查詢樹之前,先來看看折半查詢法,也叫二分查詢法
在一個有序的整數陣列中(假如是從小到大排序的),如果查詢某個元素,返回元素的索引。
如下:

int[] arr = new int[]{1,3,4,6,8,9};
在 arr 陣列中查詢6這個元素,查到返回對應的索引,沒有找到就返回-1

思想很簡單:
1 先找到陣列中間元素target與6比較
2 如果target比6大,就在陣列的左邊查詢
3 如果target比6小,就在陣列的右邊查詢

java實現程式碼如下:

    private static int binarySearch(int[] data, int target) {
        int l = 0;
        int r = data.length - 1;

        while (l <= r) {
            //int mid = (l + r) / 2;
            //這句程式碼理論上是沒有問題的,但是是有bug的
            //如果因為 l + r 會超過整數的最大值,就會溢位
            //所以換成下面的寫法,最小邊界,加上差的一半,就是中間索引

            //最小邊界,加上差的一半,就是中間值
            int mid = l + (r - l) / 2;


            if (data[mid] > target) { //如果中間的值比target大,r向右移動。
                r = mid - 1;
            } else if (data[mid] < target) { //如果中間的值比target小,l向左移動
                l = mid + 1;
            } else {
                return mid; //如果中間的值與target相等,就返回下標
            }
        }

        //沒有找到就返回-1
        return -1;
    }

測試程式碼如下:

 public static void main(String[] args) {
        int[] data = new int[]{1,3,4,6,8,9};
        System.out.println(binarySearch(data, 6));
    }

輸出

3

折半查詢的關鍵是陣列必須有序,一次過濾掉一半的資料,時間複雜度為O(logN)。
上面是以2為底的,N為陣列的元素個數.

折半查詢和下面的要講的二分搜尋樹是有一樣的思想

2 二分搜尋樹定義

二分搜尋樹定義雙叫二分查詢樹,其定義如下
1 若它的左子樹不為空,則左子樹上所有的節點的值均小於根結點的值
2 若它的右子樹不為空,則右子樹上所有的節點的值均大於根結點的值
3 它的左右子樹也分別為二分搜尋樹

由二叉搜尋樹的定義可知,它前提是二叉樹,並且採用了遞迴的定義方式
。再得,它的節點滿足一定的關係,左子樹的節點一定比父節點的小,
右子樹的節點一定比父節點的大。

構造一棵二叉搜尋樹的目的,其實目的不是為了排序,是為了提高查詢,刪除,插入關鍵字的速度。

下面我們用圖和程式碼來解釋二叉樹的查詢,插入,和刪除。比如下圖就是一個二叉搜尋樹

2.0 二叉搜尋樹的定義和節點的定義

二叉搜尋樹中存放的都是key。先看下二叉樹的定義

    //key必須繼承Comparable,可以比較大小的
    public class QBST<K extends Comparable<K>, V> {
        ...
    }

二叉樹中節點的定義

   //QNode是作為QBST的內部類的。後面會有完整的原始碼
   class QNode {
        //key,也相當於上圖中的數字,只不過不一定是數字
        //只要能比較大小就行了。這裡的key,是繼承Comparable的
        K key;     
        
        //節點中的value
        V value;
        
        //左子樹
        QNode left;
        
        //右子樹
        QNode right;

        //根據key,value構造一個節點
        QNode(K key, V value) {
            this.key = key;
            this.value = value;
            this.left = null;
            this.right = null;
        }

        //根據一個節點,構造另一個新節點
        QNode(QNode node){
            this.key = node.key;
            this.value = node.value;
            this.left = node.left;
            this.right = node.right;
        }
    }

類的定義和類中節點的定義都有了。
二分搜尋樹的定義如下:

/**
 * 二分搜尋樹,也叫二分查詢樹
 */
public class QBST<K extends Comparable<K>, V> {
    class QNode {
        K key;
        V value;
        QNode left;
        QNode right;

        QNode(K key, V value) {
            this.key = key;
            this.value = value;
            this.left = null;
            this.right = null;
        }

        QNode(QNode node){
            this.key = node.key;
            this.value = node.value;
            this.left = node.left;
            this.right = node.right;
        }
    }

    //樹的根
    private QNode root;
    //樹中節點的個數
    private int count;

    //構造一棵空的二分搜尋樹
    public QBST() {
        root = null;
        count = 0;
    }

    //返回二分搜尋樹中的個數
    public int size() {
        return count;
    }
    
    //樹是否為空
    public boolean isEmpty() {
        return count == 0;
    }
   
 }

2.1 二叉搜尋樹的插入

1 如果這棵樹為空,新建一個節點,作為根
2 如果要插入的key比根節點大,就插入到右子樹中
3 如果要插入的key比根節點小,就插入到左子樹中
4 如果要插入的key和根節點相等,就更新當前節點的value
程式碼如下:

 public void insert(K key, V value) {
        root = insert(root, key, value);
    }

    // 向以node為根的二叉搜尋樹中,插入節點(key,value)
    // 返回插入新節點後的二叉搜尋樹的根
    private QNode insert(QNode node, K key, V value) {
        //查檢條件
        checkNotNull(key,"key is null");

        //如果node為空,直接new一個節點返回
        if (node == null) {
            count++;
            return new QNode(key, value);
        }

        //如果key比根節點大,插入到node的右子樹中
        if (key.compareTo(node.key) == 1) {
            node.right = insert(node.right, key, value);
            
        //如果key比根節點小,插入到node的左子樹中    
        } else if (key.compareTo(node.key) == -1) {
            node.left = insert(node.left, key, value);
            
        //如果key和根節點相等,更新根節點的value    
        } else {
            node.value = value;
        }

        //返回根
        return node;
    }

2.2 二叉搜尋樹的查詢

和上面向一棵二叉搜尋樹插入一個節點一樣。
向一棵二叉搜尋樹中查詢一個節點也是類似
1 如果根節點為空,不用查找了,返回null
2 如果key比根節點的key要大,在右子樹中查詢
3 如果key比根節點的key要小,在左子樹中查詢
4 如果key和根節點的key相等,返回根節點

程式碼實現如下:

   //搜尋key結果的value
   public V search(K key){
        return search(root,key);
    }

    // 向以node為根的二叉搜尋樹中,以key為鍵,返回V
    private V search(QNode node,K key){
        checkNotNull(key,"key is null");
        
        //如果當前節點為null,返回null
        if(node == null){
            return null;
        }
        
        //如果key比根節點的key大,在右子樹中查詢
        if(key.compareTo(node.key) == 1){
            return search(node.right,key);
            
        //如果key比根節點的key小,在左子樹中查詢    
        }else if(key.compareTo(node.key) == -1){
            return search(node.left,key);
            
        //如果key與根節點的key值相等,就返回節點的value值    
        }else {
            return node.value;
        }
    }

2.3 二叉搜尋樹的遍歷

二叉樹的遍歷有前序遍歷,中序遍歷,後序遍歷,層序遍歷(也叫做廣度優先遍歷)
如下圖的二叉搜尋樹。

根據根節點的訪問順序,可以把遍歷分為前序遍歷,中序遍歷,後序遍歷
前序遍歷:先訪問根節點,再前序遍歷左右子樹
中序遍歷:先中序遍歷左子樹,再訪問根節點,後中序遍歷右子樹
後序遍歷:先後序遍歷左子樹,再後序遍歷右子樹,再訪問根節點

程式碼實現分別如下:

    // 前序遍歷 O(n)
    public void preOrder(){
        //後序遍歷以root為根的二叉搜尋樹
        preOrder(root);
    }

    private void preOrder(QNode node){
        if(node != null){
            //先遍歷根節點
            System.out.println(node.key);//這裡的訪問只是列印
            //前序遍歷左子樹
            preOrder(node.left);
            //後序遍歷右子樹
            preOrder(node.right);
        }
    }

    // 中序遍歷 O(n)
    public void middleOrder(){
        middleOrder(root);
    }

    private void middleOrder(QNode node){
        if(node != null){
            middleOrder(node.left);
            System.out.println(node.key);
            middleOrder(node.right);
        }
    }

    // 後序遍歷 O(n)
    public void postOrder(){
        postOrder(root);
    }

    private void postOrder(QNode node){
        if(node != null){
            postOrder(node.left);
            postOrder(node.right);
            System.out.println(node.key);
        }
    }

其中層序遍歷就是一層一層的從左到右遍歷
上圖中層序遍歷的結果是 13 6 15 3 7 10 18
程式碼實現需要藉助佇列,程式碼實現如下:

   // 層序遍歷,也叫做廣度優先遍歷
    public void levelOrder(){
        if(root == null){
            return;
        }

        LinkedList<QNode> queue = new LinkedList<>();
        queue.addLast(root);

        while (!queue.isEmpty()){
            QNode node = queue.removeLast();
            
            //這裡我們只打印
            System.out.println(node.key);
            
            queue.addLast(node.left);
            queue.addLast(node.right);
        }
    }

2.4 二叉搜尋樹的刪除

二叉搜尋樹最麻煩的就是刪除節點,刪除任意二叉樹中的節點之前,我們來先刪除特殊的節點。

  1. 刪除二叉搜尋樹中最小的節點
  2. 刪除二叉搜尋樹中最大的節點
  3. 查詢二叉搜尋樹中最小的節點
  4. 查詢二叉搜尋樹中最大的節點

我們先來實現這些操作。

如下圖

根據二叉搜尋樹的定義,可以得出以下結論

  1. 在一個二叉搜尋樹中,最小的節點一定是最左邊的節點,也就是圖中的節點 3
  2. 在一個二叉搜尋樹中,最大的節點一定是最右邊的節點,也就是圖中的節點 18

總之:
最小節點去左子樹中找,直到節點的左孩子為空,則當前節點就是最小節點
最大節點去右子樹中找,直到節點的右孩子為空,則當前節點就是最大節點

1 先來實現查詢二叉搜尋樹中最小的節點
如下程式碼

    //查詢一棵樹中最小的節點,返回 K 
    public K minimum(){
        checkNotNull(root,"the tree is empty");
        
        //在以根為root的二叉搜尋樹中返回最小節點的鍵值
        QNode minNode = minimum(root);
        
        //返回最小節點的鍵值
        return minNode.key;
    }

    // 在以node為根的二叉搜尋樹中,返回最小鍵值的節點
    private QNode minimum(QNode node){
        //如果node.left == null,說明當前node節點就是最小的節點
        //返回當前節點node
        if(node.left == null){
            return node;
        }
        
        //如果當前節點不是最小的節點
        //繼承往左子樹中查詢
        return minimum(node.left);
    }

同理,查詢最大節點也是一樣
2 實現查詢二叉搜尋樹中最大的節點
程式碼如下:


    public K maximum(){
        checkNotNull(root,"the tree is empty");
        QNode maxNode = maximum(root);
        return maxNode.key;
    }

    // 在以node為根的二叉搜尋樹中,返回最大鍵值的節點
    private QNode maximum(QNode node){
        if(node.right == null){
            return node;
        }

        return maximum(node.right);
    }

上面實現了查詢最小節點和最大節點,下面我們再來實現刪除最小節點和刪除最大節點

3 實現刪除二叉搜尋樹中最小的節點
一直往左孩子中刪除,當某一個節點node沒有左孩子時,說明當前節點就是最小節點
這時候分兩種情況

  1. 當前節點有右孩子
    如果是這種情況,直接把右孩子返回,作為當前節點
  2. 當前節點沒有右孩子
    如果是這種情況,直接返回null。此時返回右孩子也行,因為右孩子也是null

程式碼實現如下

    // 刪除二叉搜尋樹中最小的節點
    public void removeMin(){
        if(root != null){
            root = removeMin(root);
        }
    }

    // 刪除掉以node為根的二分搜尋樹中的最小的節點
    // 返回刪除節點後新的二分搜尋樹的根
    private QNode removeMin(QNode node){
        //如果當前當前沒有左孩子,則當前節點就是最小節點
        if(node.left == null){
            //儲存當前節點的右孩子,這句程式碼把上面兩種情況都包含了
            QNode rightNode = node.right;
            node = null;    //釋放當前節點
            count--;        //記得數量要減1
            return rightNode;//返回右孩子,有可能為空或者不為空
        }
        
        //遞迴呼叫刪除以當前節點的左孩子為根的二叉搜尋中最小的節點
        node.left = removeMin(node.left);
        
        //別忘了返回當前節點
        return node;
    }

同理,刪除二叉搜尋樹中最大的節點的程式碼如下:

   // 刪除二叉搜尋樹中最大的節點
    public void removeMax(){
        if(root != null){
            root = removeMax(root);
        }
    }

    // 刪除掉以node為根的二分搜尋樹中的最大的節點
    // 返回刪除節點後新的二分搜尋樹的根
    private QNode removeMax(QNode node){
        if(node.right == null){
            QNode leftNode = node.left;
            count--;
            node = null;

            return leftNode;
        }

        node.right = removeMax(node.right);
        return node;
    }

下面來分析一下刪除任意一個節點。
刪除任意一個節點node,那麼可以分為以下幾種情況

  1. node 沒有孩子
  2. node 只有一個孩子
  3. node 有兩個孩子

如下圖一棵二叉搜尋樹,我們來分析

第一種情況:node沒有孩子
這種情況最簡單,直接刪除就行了,剩下的還是一棵二叉搜尋樹
比如圖中的 節點5,節點13,節點27,節點50,刪除任意一個節點之後
剩下的還是滿足一棵二叉搜尋樹

第二種情況:node只有一個孩子
這種情況又分兩種

  1. node節點有一個左孩子
  2. node節點有一個右孩子

上面兩種情況其實不影響,比如圖中的節點10,節點45,分別有一個左孩子和一個右孩子。
也好辦,節點10刪除後,它的左孩子節點5,放在節點10的位置
同理知,節點45刪除後,它的右孩子節點50,放在節點45的位置
這樣一來,剩下的節點還是一棵二叉搜尋樹

第三種情況:node有兩個孩子
還是上圖為準,以節點17為例,節點17有左右兩個孩子,分別是10,19
要刪除節點17,怎麼辦呢?
或者說節點17刪除 後,哪個節點應該放在節點17的位置上呢?

我們節點17滿足兩個性質 :

  1. 17大於它的左孩子10
  2. 17小於它的右孩子19

那麼我們找到一個這樣的節點,只要滿足上面這兩條性質,不就是可以了嗎。
so easey

我們就來先找一個大於10而且小於19的節點

  1. 大於 10 的節點,只要在 17 的右子樹
    也就是以 19 為根節點的樹中找不就行了嗎
    因為17的右子樹中所有的節點都比 17 大
  2. 小於 19 的節點,只要在以 19 為根的樹中找左孩子不就得了嗎
    經過上面的分析,這樣的節點就是 13 啊,將17刪除 ,把13放到17的位置 ,如圖

其實,把10放到17的位置也是可以的。如下圖

10和13兩個節點都滿足條件,所以我們可以得出結論

刪除一個有兩個孩子節點,可以找這個節點左子樹中的最大節點,或者右子樹中的最小節點來放到當前位置

虛擬碼:
刪除左右都 有孩子的節點 d
找到 s = min(d.right)
s 可以叫作 d 的後繼
s.right = deledeMin(d->right)
s.left = d.left;
刪除 d, s 是新的子樹的根

翻譯成程式碼如下:

  public void remove(K key) {
        root = remove(root, key);
    }

    // 刪除掉以node為根的二分搜尋樹中鍵值為key的節點
    // 返回刪除節點後新的二分搜尋樹的根
    // O(logN)
    private QNode remove(QNode node, K key) {
        //如果樹為null,返回null
        if (node == null) {
            return null;
        }

        //想要刪除某個節點,必須先要找到這個節點
        //所以下面的程式碼包含了查詢

        if (key.compareTo(node.key) == -1) {//如果key小於根節點的key

            //到node的左子樹查詢並刪除鍵值為key的節點
            node.left = remove(node.left, key);

            //返回刪除節點後新的二分搜尋樹的根
            return node;

        } else if (key.compareTo(node.key) == 1) {//如果key大於根節點的key

            //到node的右子樹查詢並刪除鍵值為key的節點
            node.right = remove(node.right, key);

            //返回刪除節點後新的二分搜尋樹的根
            return node;
        } else { //key == node.key,也就是找到了這個節點

            //當前節點的左孩子為null
            if (node.left == null) {
                //儲存右孩子節點
                QNode rightNode = node.right;
                //個數減1
                count--;

                //刪除
                node = null;

                //右節點作為新的根
                return rightNode;
            }

            //當前節點的右孩子為null
            if (node.right == null) {
                //儲存左孩子的節點
                QNode leftNode = node.left;
                //個數減1
                count--;

                //刪除
                node = null;

                //左節點作為新的根
                return leftNode;
            }

            //上面的情況也包括了左右兩個孩子都是null
            //這樣的情況就走第一種,node.left==null的條件中。也滿足


            //下面是 node.left != null && node.right != null的情況

            //找到右子樹中最小節點
            QNode min = minimum(node.right);

            //用最小節點新建一個節點,因為等會要刪除最小的節點,所以這裡我們要新建一個最小節點
            QNode s = new QNode(min);

            //s的右孩子,就是刪除node右子樹中最小節點返回的根
            s.right = removeMin(node.right);

            //s的左孩子,就是刪除節點的左孩子
            s.left = node.left;

            //返回新的根
            return s;
        }
    }

同過上面的分析,我們瞭解了二叉搜尋樹的性質,以及插入,查詢,查詢最大節點,查詢最小節點,刪除最大節點,刪除最小節點,以及最後分析出來刪除一個任意節點。

下面我們粘出完整程式碼 。如下


/**
 * 二分搜尋樹,也叫二分查詢樹
 */
public class QBST<K extends Comparable<K>, V> {
    class QNode {
        K key;
        V value;
        QNode left;
        QNode right;

        QNode(K key, V value) {
            this.key = key;
            this.value = value;
            this.left = null;
            this.right = null;
        }

        QNode(QNode node) {
            this.key = node.key;
            this.value = node.value;
            this.left = node.left;
            this.right = node.right;
        }
    }

    private QNode root;
    private int count;


    public QBST() {
        root = null;
        count = 0;
    }

    public int size() {
        return count;
    }

    public boolean isEmpty() {
        return count == 0;
    }

    public void insert(K key, V value) {
        root = insert(root, key, value);
    }

    // 向以node為根的二叉搜尋樹中,插入節點(key,value)
    // 返回插入新節點後的二叉搜尋樹的根
    private QNode insert(QNode node, K key, V value) {
        checkNotNull(key, "key is null");

        if (node == null) {
            count++;
            return new QNode(key, value);
        }

        if (key.compareTo(node.key) == 1) {
            node.right = insert(node.right, key, value);
        } else if (key.compareTo(node.key) == -1) {
            node.left = insert(node.left, key, value);
        } else {
            node.value = value;
        }

        return node;
    }

    public boolean contain(K key) {
        return contain(root, key);
    }

    // 向以node為根的二叉搜尋樹中,查詢是否包含key的節點
    private boolean contain(QNode node, K key) {
        checkNotNull(key, "key is null");

        if (node == null) {
            return false;
        }

        if (key.compareTo(node.key) == 1) {
            return contain(node.right, key);
        } else if (key.compareTo(node.key) == -1) {
            return contain(node.left.key);
        } else {
            return true;
        }
    }

    public V search(K key) {
        return search(root, key);
    }

    // 向以node為根的二叉搜尋樹中,
    private V search(QNode node, K key) {
        checkNotNull(key, "key is null");

        if (node == null) {
            return null;
        }

        if (key.compareTo(node.key) == 1) {
            return search(node.right, key);
        } else if (key.compareTo(node.key) == -1) {
            return search(node.left, key);
        } else {
            return node.value;
        }
    }

    // 前序遍歷 O(n)
    public void preOrder() {
        preOrder(root);
    }

    private void preOrder(QNode node) {
        if (node != null) {
            System.out.println(node.key);
            preOrder(node.left);
            preOrder(node.right);
        }
    }

    // 中序遍歷 O(n)
    public void middleOrder() {
        middleOrder(root);
    }

    private void middleOrder(QNode node) {
        if (node != null) {
            middleOrder(node.left);
            System.out.println(node.key);
            middleOrder(node.right);
        }
    }

    // 後序遍歷 O(n)
    public void postOrder() {
        postOrder(root);
    }

    private void postOrder(QNode node) {
        if (node != null) {
            postOrder(node.left);
            postOrder(node.right);
            System.out.println(node.key);
        }
    }

    // 層序遍歷,也叫做廣度優先遍歷
    public void levelOrder() {
        if (root == null) {
            return;
        }

        LinkedList<QNode> queue = new LinkedList<>();
        queue.addLast(root);

        while (!queue.isEmpty()) {
            QNode node = queue.removeLast();
            System.out.println(node.key);
            queue.addLast(node.left);
            queue.addLast(node.right);
        }
    }

    public void destroy() {
        destroy(root);
    }

    // 銷燬操作就是後序遍歷的一次應用
    private void destroy(QNode node) {
        if (node != null) {
            destroy(node.left);
            destroy(node.right);

            node = null;
            count--;
        }
    }

    public K minimum() {
        checkNotNull(root, "the tree is empty");
        QNode minNode = minimum(root);
        return minNode.key;
    }

    // 在以node為根的二叉搜尋樹中,返回最小鍵值的節點
    private QNode minimum(QNode node) {
        if (node.left == null) {
            return node;
        }

        return minimum(node.left);
    }

    public K maximum() {
        checkNotNull(root, "the tree is empty");
        QNode maxNode = maximum(root);
        return maxNode.key;
    }

    // 在以node為根的二叉搜尋樹中,返回最大鍵值的節點
    private QNode maximum(QNode node) {
        if (node.right == null) {
            return node;
        }

        return maximum(node.right);
    }

    // 刪除二叉搜尋樹中最小的節點
    public void removeMin() {
        if (root != null) {
            root = removeMin(root);
        }
    }

    // 刪除掉以node為根的二分搜尋樹中的最小的節點
    // 返回刪除節點後新的二分搜尋樹的根
    private QNode removeMin(QNode node) {
        if (node.left == null) {
            QNode rightNode = node.right;
            node = null;
            count--;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }

    // 刪除二叉搜尋樹中最大的節點
    public void removeMax() {
        if (root != null) {
            root = removeMax(root);
        }
    }

    // 刪除掉以node為根的二分搜尋樹中的最大的節點
    // 返回刪除節點後新的二分搜尋樹的根
    private QNode removeMax(QNode node) {
        if (node.right == null) {
            QNode leftNode = node.left;
            count--;
            node = null;

            return leftNode;
        }

        node.right = removeMax(node.right);
        return node;
    }

    public void remove(K key) {
        root = remove(root, key);
    }

    // 刪除掉以node為根的二分搜尋樹中鍵值為key的節點
    // 返回刪除節點後新的二分搜尋樹的根
    // O(logN)
    private QNode remove(QNode node, K key) {
        //如果樹為null,返回null
        if (node == null) {
            return null;
        }

        //想要刪除某個節點,必須先要找到這個節點
        //所以下面的程式碼包含了查詢

        if (key.compareTo(node.key) == -1) {//如果key小於根節點的key

            //到node的左子樹查詢並刪除鍵值為key的節點
            node.left = remove(node.left, key);

            //返回刪除節點後新的二分搜尋樹的根
            return node;

        } else if (key.compareTo(node.key) == 1) {//如果key大於根節點的key

            //到node的右子樹查詢並刪除鍵值為key的節點
            node.right = remove(node.right, key);

            //返回刪除節點後新的二分搜尋樹的根
            return node;
        } else { //key == node.key,也就是找到了這個節點

            //當前節點的左孩子為null
            if (node.left == null) {
                //儲存右孩子節點
                QNode rightNode = node.right;
                //個數減1
                count--;

                //刪除
                node = null;

                //右節點作為新的根
                return rightNode;
            }

            //當前節點的右孩子為null
            if (node.right == null) {
                //儲存左孩子的節點
                QNode leftNode = node.left;
                //個數減1
                count--;

                //刪除
                node = null;

                //左節點作為新的根
                return leftNode;
            }

            //上面的情況也包括了左右兩個孩子都是null
            //這樣的情況就走第一種,node.left==null的條件中。也滿足


            //下面是 node.left != null && node.right != null的情況

            //找到右子樹中最小節點
            QNode min = minimum(node.right);

            //用最小節點新建一個節點,因為等會要刪除最小的節點,所以這裡我們要新建一個最小節點
            QNode s = new QNode(min);

            //s的右孩子,就是刪除node右子樹中最小節點返回的根
            s.right = removeMin(node.right);

            //s的左孩子,就是刪除節點的左孩子
            s.left = node.left;

            //返回新的根
            return s;
        }
    }


    private <E> void checkNotNull(E e, String message) {
        if (e == null) {
            throw new IllegalArgumentException(message);
        }
    }

}