2-3 查找樹及其Java實現


 

 

2-3 查找樹

定義(來源:wiki)

2–3樹是一種樹型數據結構,內部節點(存在子節點的節點)要么有2個孩子和1個數據元素,要么有3個孩子和2個數據元素,葉子節點沒有孩子,並且有1個或2個數據元素。

 

2個結點
2個結點
3個結點

 

  • 定義

    如果一個內部節點擁有一個數據元素、兩個子節點,則此節點為2節點

    如果一個內部節點擁有兩個數據元素、三個子節點,則此節點為3節點

    當且僅當以下敘述中有一條成立時,T為2–3樹:

    • T為空。即T不包含任何節點。
    • T為擁有數據元素a的2節點。若T的左孩子為L、右孩子為R,則
      • LR是等高的非空2–3樹;
      • a大於L中的所有數據元素;
      • a小於等於R中的所有數據元素。
    • T為擁有數據元素ab的3節點,其中a < b。若T的左孩子為L、中孩子為M、右孩子為R,則
      • LM、和R是等高的非空2–3樹;
      • a大於L中的所有數據元素,並且小於等於M中的所有數據元素;
      • b大於M中的所有數據元素,並且小於等於R中的所有數據元素。

查找

首先我們說一下查找

2-3查找樹的查找和二叉樹很類似,無非就是進行比較然后選擇下一個查找的方向。 (這幾張圖不知道來源,知道的呲我一聲)

 

2-3樹查找
2-3樹查找

 

插入

2-3查找樹的插入

我們可以思考一下,為什么要兩個結點。在前面可以知道,二叉查找樹變成鏈表的原因就是因為新插入的結點沒有選擇的”權利”,當我們插入一個元素的時候,實際上它的位置已經確定了, 我們並不能對它進行操作。那么2-3查找樹是怎么做到賦予“權利”的呢?秘密便是這個多出來結點,他可以緩存新插入的結點。(具體我們將在插入的時候講)

前面我們知道,2-3查找樹分為2結點3結點,so,插入就分為了2結點插入和3結點插入。

**2-結點插入:**向2-結點插入一個新的結點和向而插入插入一個結點很類似,但是我們並不是將結點“吊”在結點的末尾,因為這樣就沒辦法保持樹的平衡。我們可以將2-結點替換成3-結點即可,將其中的鍵插入這個3-結點即可。(相當於緩存了這個結點)

 


 

 

3-結點插入: 3結點插入比較麻煩,emm可以說是特別麻煩,它分為3種情況。

  1. 向一棵只含有3-結點的樹插入新鍵。

    假如2-3樹只有一個3-結點,那么當我們插入一個新的結點的時候,我們先假設結點變成了4-結點,然后使得中間的結點為根結點,左邊的結點為其左結點,右邊的結點為其右結點,然后構成一棵2-3樹,樹的高度加1

     


     

     

  2. 向父結點為2-結點的3-結點中插入新鍵。

    和上面的情況類似,我們將新的節點插入3-結點使之成為4-結點,然后將結點中的中間結點”升“到其父節點(2-結點)中的合適的位置,使其父節點成為一個3-節點,然后將左右節點分別掛在這個3-結點的恰當位置,樹的高度不發生改變

 


 

 

  1. 向父節點為3-結點的3-結點中插入新鍵。

    這種情況有點類似遞歸:當我們的結點為3-結點的時候,我們插入新的結點會將中間的元素”升“父節點,然后父節點為4-結點,右將中間的結點”升“到其父結點的父結點,……如此進行遞歸操作,直到遇到的結點不再是3-結點。

 


 

 

JAVA代碼實現2-3樹

接下來就是最難的操作來了,實現這個算法,2-3查找樹的算法比較麻煩,所以我們不得不將問題分割,分割求解能將問題變得簡單。參考博客

接下來就是最難的操作來了,實現這個算法,2-3查找樹的算法比較麻煩,所以我們不得不將問題分割,分割求解能將問題變得簡單。參考博客

首先我們定義數據結構,作用在注釋已經寫的很清楚了。

public class Tree23<Key extends Comparable<Key>,Value> {
        /** * 保存key和value的鍵值對 * @param <Key> * @param <Value> */
    private class Data<Key extends Comparable<Key>,Value>{
        private Key key;
        private Value value;

        public Data(Key key, Value value) {
            this.key = key;
            this.value = value;
        }
        public void displayData(){
            System.out.println("/" + key+"---"+value);
        }
    }

    /** * 保存樹結點的類 * @param <Key> * @param <Value> */
    private class Node23<Key extends Comparable<Key>,Value>{

        public void displayNode() {
            for(int i = 0; i < itemNum; i++){
                itemDatas[i].displayData();
            }
            System.out.println("/");
        }

        private static final int N = 3;
        // 該結點的父節點
        private Node23 parent;
        // 子節點,子節點有3個,分別是左子節點,中間子節點和右子節點
        private Node23[] chirldNodes = new Node23[N];
        // 代表結點保存的數據(為一個或者兩個)
        private Data[] itemDatas = new Data[N - 1];
        // 結點保存的數據個數
        private int itemNum = 0;

        /** * 判斷是否是葉子結點 * @return */
        private boolean isLeaf(){
            // 假如不是葉子結點。必有左子樹(可以想一想為什么?)
            return chirldNodes[0] == null;
        }

        /** * 判斷結點儲存數據是否滿了 * (也就是是否存了兩個鍵值對) * @return */
        private boolean isFull(){
            return itemNum == N-1;
        }

        /** * 返回該節點的父節點 * @return */
        private Node23 getParent(){
            return this.parent;
        }

        /** * 將子節點連接 * @param index 連接的位置(左子樹,中子樹,還是右子樹) * @param child */
        private void connectChild(int index,Node23 child){
            chirldNodes[index] = child;
            if (child != null){
                child.parent = this;
            }
        }

        /** * 解除該節點和某個結點之間的連接 * @param index 解除鏈接的位置 * @return */
        private Node23 disconnectChild(int index){
            Node23 temp = chirldNodes[index];
            chirldNodes[index] = null;
            return temp;
        }

        /** * 獲取結點左或右的鍵值對 * @param index 0為左,1為右 * @return */
        private Data getData(int index){
            return itemDatas[index];
        }

        /** * 獲得某個位置的子樹 * @param index 0為左指數,1為中子樹,2為右子樹 * @return */
        private Node23 getChild(int index){
            return chirldNodes[index];
        }

        /** * @return 返回結點中鍵值對的數量,空則返回-1 */
        public int getItemNum(){
            return itemNum;
         }

        /** * 尋找key在結點的位置 * @param key * @return 結點沒有key則放回-1 */
        private int findItem(Key key){
            for (int i = 0; i < itemNum; i++) {
                if (itemDatas[i] == null){
                    break;
                }else if (itemDatas[i].key.compareTo(key) == 0){
                    return i;
                }
            }
            return -1;
        }

        /** * 向結點插入鍵值對:前提是結點未滿 * @param data * @return 返回插入的位置 0或則1 */
        private int insertData(Data data){
            itemNum ++;
            for (int i = N -2; i >= 0 ; i--) {
                if (itemDatas[i] == null){
                    continue;
                }else{
                    if (data.key.compareTo(itemDatas[i].key)<0){
                        itemDatas[i+1] = itemDatas[i];
                    }else{
                        itemDatas[i+1] = data;
                        return i+1;
                    }
                }
            }
            itemDatas[0] = data;
            return 0;
        }

        /** * 移除最后一個鍵值對(也就是有右邊的鍵值對則移右邊的,沒有則移左邊的) * @return 返回被移除的鍵值對 */
        private Data removeItem(){
            Data temp = itemDatas[itemNum - 1];
            itemDatas[itemNum - 1] = null;
            itemNum --;
            return temp;
        }
    }
    /** * 根節點 */
    private Node23 root = new Node23();
    ……接下來就是一堆方法了
}

主要是兩個方法:find查找方法和Insert插入方法:看注釋

/** *查找含有key的鍵值對 * @param key * @return 返回鍵值對中的value */
public Value find(Key key) {
    Node23 curNode = root;
    int childNum;
    while (true) {
        if ((childNum = curNode.findItem(key)) != -1) {
            return (Value) curNode.itemDatas[childNum].value;
        }
        // 假如到了葉子節點還沒有找到,則樹中不包含key
        else if (curNode.isLeaf()) {
            return null;
        } else {
            curNode = getNextChild(curNode,key);
        }
    }
}

/** * 在key的條件下獲得結點的子節點(可能為左子結點,中間子節點,右子節點) * @param node * @param key * @return 返回子節點,若結點包含key,則返回傳參結點 */
private Node23 getNextChild(Node23 node,Key key){
    for (int i = 0; i < node.getItemNum(); i++) {
        if (node.getData(i).key.compareTo(key)>0){
            return node.getChild(i);
        }
        else if (node.getData(i).key.compareTo(key) == 0){
            return node;
        }
    }
    return node.getChild(node.getItemNum());
}

/** * 最重要的插入函數 * @param key * @param value */
public void insert(Key key,Value value){
    Data data = new Data(key,value);
    Node23 curNode = root;
    // 一直找到葉節點
    while(true){
        if (curNode.isLeaf()){
            break;
        }else{
            curNode = getNextChild(curNode,key);
            for (int i = 0; i < curNode.getItemNum(); i++) {
                // 假如key在node中則進行更新
                if (curNode.getData(i).key.compareTo(key) == 0){
                    curNode.getData(i).value =value;
                    return;
                }
            }
        }
    }

    // 若插入key的結點已經滿了,即3-結點插入
    if (curNode.isFull()){
        split(curNode,data);
    }
    // 2-結點插入
    else {
        // 直接插入即可
        curNode.insertData(data);
    }
}

/** * 這個函數是裂變函數,主要是裂變結點。 * 這個函數有點復雜,我們要把握住原理就好了 * @param node 被裂變的結點 * @param data 要被保存的鍵值對 */
private void split(Node23 node, Data data) {
    Node23 parent = node.getParent();
    // newNode用來保存最大的鍵值對
    Node23 newNode = new Node23();
    // newNode2用來保存中間key的鍵值對
    Node23 newNode2 = new Node23();
    Data mid;

    if (data.key.compareTo(node.getData(0).key)<0){
        newNode.insertData(node.removeItem());
        mid = node.removeItem();
        node.insertData(data);
    }else if (data.key.compareTo(node.getData(1).key)<0){
        newNode.insertData(node.removeItem());
        mid = data;
    }else{
        mid = node.removeItem();
        newNode.insertData(data);
    }
    if (node == root){
        root = newNode2;
    }
    /** * 將newNode2和node以及newNode連接起來 * 其中node連接到newNode2的左子樹,newNode * 連接到newNode2的右子樹 */
    newNode2.insertData(mid);
    newNode2.connectChild(0,node);
    newNode2.connectChild(1,newNode);
    /** * 將結點的父節點和newNode2結點連接起來 */
    connectNode(parent,newNode2);
}

/** * 鏈接node和parent * @param parent * @param node node中只含有一個鍵值對結點 */
private void connectNode(Node23 parent, Node23 node) {
    Data data = node.getData(0);
    if (node == root){
        return;
    }
    // 假如父節點為3-結點
    if (parent.isFull()){
        // 爺爺結點(爺爺救葫蘆娃)
        Node23 gParent = parent.getParent();
        Node23 newNode = new Node23();
        Node23 temp1,temp2;
        Data itemData;

        if (data.key.compareTo(parent.getData(0).key)<0){
            temp1 = parent.disconnectChild(1);
            temp2 = parent.disconnectChild(2);
            newNode.connectChild(0,temp1);
            newNode.connectChild(1,temp2);
            newNode.insertData(parent.removeItem());

            itemData = parent.removeItem();
            parent.insertData(itemData);
            parent.connectChild(0,node);
            parent.connectChild(1,newNode);
        }else if(data.key.compareTo(parent.getData(1).key)<0){
            temp1 = parent.disconnectChild(0);
            temp2 = parent.disconnectChild(2);
            Node23 tempNode = new Node23();

            newNode.insertData(parent.removeItem());
            newNode.connectChild(0,newNode.disconnectChild(1));
            newNode.connectChild(1,temp2);

            tempNode.insertData(parent.removeItem());
            tempNode.connectChild(0,temp1);
            tempNode.connectChild(1,node.disconnectChild(0));

            parent.insertData(node.removeItem());
            parent.connectChild(0,tempNode);
            parent.connectChild(1,newNode);
        } else{
            itemData = parent.removeItem();

            newNode.insertData(parent.removeItem());
            newNode.connectChild(0,parent.disconnectChild(0));
            newNode.connectChild(1,parent.disconnectChild(1));
            parent.disconnectChild(2);
            parent.insertData(itemData);
            parent.connectChild(0,newNode);
            parent.connectChild(1,node);
        }
        // 進行遞歸
        connectNode(gParent,parent);
    }
    // 假如父節點為2結點
    else{
        if (data.key.compareTo(parent.getData(0).key)<0){
            Node23 tempNode = parent.disconnectChild(1);
            parent.connectChild(0,node.disconnectChild(0));
            parent.connectChild(1,node.disconnectChild(1));
            parent.connectChild(2,tempNode);
        }else{
            parent.connectChild(1,node.disconnectChild(0));
            parent.connectChild(2,node.disconnectChild(1));
        }
        parent.insertData(node.getData(0));
    }
}

2-3查找樹的原理很簡單,甚至說代碼實現起來難度都不是很大,但是卻很繁瑣,因為它有很多種情況,而在紅黑樹中,用巧妙的方法使用了2個結點解決了3個結點的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM