2-3-4树

二叉树在之前章节已经做了介绍, 二叉树每个节点只有一个数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树。本篇博客我们将介绍的2-3-4树,它是一种多叉树,它的每个节点最多有三个数据项和四个子节点。
 
简介
2-3-4树每个节点最多有三个数据项和四个字节点,名字中 2,3,4 的数字含义是指一个节点可能含有的子节点的个数,对于非叶节点有三种可能的情况:

  1. 有一个数据项的节点总是有两个子节点
  2. 有二个数据项的节点总是有三个子节点
  3. 有三个数据项的节点总是有四个子节点

简而言之,非叶节点的子节点数总是比它含有的数据项多1,如果子节点个数为L,数据项个数为D,那么:L = D + 1。

叶节点(上图最下面的一排)是没有子节点的,然而它可能含有一个、两个或三个数据项,空节点是不会存在的。
树结构中很重要的一点就是节点之间关键字值大小的关系。在二叉树中,所有关键字值比某个节点值小的节点都在这个节点左子节点为根的子树上;所有关键字值比某个节点值大的节点都在这个节点右子节点为根的子树上。2-3-4 树规则也是一样,并且还加上以下几点:
为了方便描述,用从0到2的数字给数据项编号,用0到3的数字给子节点编号,如下图:

  1. 根是child0的子树的所有子节点的关键字值小于key0
  2. 根是child1的子树的所有子节点的关键字值大于key0并且小于key1
  3. 根是child2的子树的所有子节点的关键字值大于key1并且小于key2
  4. 根是child3的子树的所有子节点的关键字值大于key2

简化关系如下图,由于2-3-4树中一般不允许出现重复关键值,所以不用考虑比较关键值相同的情况。


搜索
查找特定关键字值的数据项和在二叉树中的搜索类似。从根节点开始搜索,除非查找的关键字值就是根,否则选择关键字值所在的合适范围,转向那个方向,直到找到为止。

比如对于下面这幅图,我们需要查找关键字值为 64 的数据项。

首先从根节点开始,根节点只有一个数据项50,没有找到,而且因为64比50大,那么转到根节点的子节点child1。60|70|80 也没有找到,而且60<64<70,所以我们还是找该节点的child1,62|64|66,我们发现其第二个数据项正好是64,于是找到了。

插入
新的数据项一般要插在叶节点里,在树的最底层。如果你插入到有子节点的节点里,那么子节点的编号就要发生变化来维持树的结构,因为在2-3-4树中节点的子节点要比数据项多1。
插入操作有时比较简单,有时却很复杂。
①当插入没有满数据项的节点时是很简单的,找到合适的位置,只需要把新数据项插入就可以了,插入可能会涉及到在一个节点中移动一个或其他两个数据项,这样在新的数据项插入后关键字值仍保持正确的顺序。如下图:

②如果往下寻找插入位置的途中,节点已经满了,那么插入就变得复杂了。发生这种情况,节点必须分裂,分裂能保证2-3-4树的平衡。

ps:这里讨论的是自顶向下的2-3-4树,因为是在向下找到插入点的路途中节点发生了分裂。把要分裂的数据项设为A,B,C,下面是节点分裂的情况(假设分裂的节点不是根节点):

节点分裂

  1. 创建一个新的空节点,它是要分裂节点的兄弟,在要分裂节点的右边
  2. 数据项C移到新节点中
  3. 数据项B移到要分裂节点的父节点中
  4. 数据项A保留在原来的位置
  5. 最右边的两个子节点从要分裂处断开,连到新节点上

上图描述了节点分裂的例子,另一种描述节点分裂的说法是4-节点变成了两个 2- 节点。节点分裂是把数据向上和向右移动,从而保持了数的平衡。一般插入只需要分裂一个节点,除非插入路径上存在不止一个满节点时,这种情况就需要多重分裂。

根的分裂
如果一开始查找插入节点时就碰到满的根节点,那么插入过程更复杂:

  1. 创建新的根节点,它是要分裂节点的父节点
  2. 创建第二个新的节点,它是要分裂节点的兄弟节点
  3. 数据项C移到新的兄弟节点中
  4. 数据项B移到新的根节点中
  5. 数据项A保留在原来的位置
  6. 要分裂节点最右边的两个子节点断开连接,连到新的兄弟节点中

上图便是根分裂的情况,分裂完成之后,整个树的高度加1。另外一种描述根分裂的方法是说4-节点变成三个2-节点。
注意:插入时,碰到没有满的节点时,要继续向下寻找其子节点进行插入。如果直接插入该节点,那么还要进行子节点的增加,因为在2-3-4树中节点的子节点个数要比数据项多1;如果插入的节点满了,那么就要进行节点分裂。下图是一系列插入过程,有4个节点分裂了,两个是根,两个是叶节点:


完整源码

/**
 * @ClassName: Node
 * @Description: 
 *  Node类表示节点中的数据存储格式。包含两个数组类型:childArray和itemArray。childArray有四个数据单元存储子节点。
 *  itemArray有三个数据单元,用来存储DataItem对象(的引用),代表具体内容,而且插入和移除数据时要保持该数组有序      (关键字从小到大)。
    Node类提供了三个重要方法:
    findItem:依据关键字在当前节点的数据项数组itemArray中查找。
    insertItem:把数据项插入到itemArray中,并保持有序
    removeItem:根据关键字在itemArray中移除相应的数据项,并保持有序。
 * @author andy
 * @date 2017年11月1日 下午4:51:35
 */
public class Node {
    private static final int ORDER = 4;
    private int numItems; // 节点中实际存储的数据项数目,其值一定不大于3
    private Node parent;
    private Node childArray[] = new Node[ORDER]; // 子节点数组
    private DataItem itemArray[] = new DataItem[ORDER - 1]; // 存储数据项数组

    // 把参数中的节点作为子节点,与当前节点进行连接
    public void connectChild(int childNum, Node child) {
        childArray[childNum] = child;
        if (child != null)
            child.parent = this; // 当前节点作为父节点
    }

    // 断开参数确定的节点与当前节点的连接,这个节点一定是当前节点的子节点。
    public Node disconnectChild(int childNum) {
        Node tempNode = childArray[childNum];
        childArray[childNum] = null; // 断开连接
        return tempNode; // 返回要这个子节点
    }

    // 获取相应的子节点
    public Node getChild(int childNum){
        return childArray[childNum];
    }

    // 获取父节点
    public Node getParent(){
        return parent;
    }

    // 是否是叶结点
    public boolean isLeaf(){
        return (childArray[0] == null) ? true : false;
    }
  
    // 获取实际存储的数据项数目
    public int getNumItems(){
        return numItems;
    }

    // 获取具体的数据项
    public DataItem getItem(int index){
        return itemArray[index];
    }

    // 该节点是否已满
    public boolean isFull(){
        return (numItems == ORDER - 1) ? true : false;
    }

    // 查找
    public int findItem(long key) {
        for (int j = 0; j < ORDER - 1; j++) { // 遍历数组
            if (itemArray[j] == null)  // 数组未满,未找到
                break;
            else if (itemArray[j].dData == key)
                return j;
        }
        return -1;
    }
   
    // 节点未满的插入
    public int insertItem(DataItem newItem){
        numItems++;
        long newKey = newItem.dData; // 获得关键字

        for (int j = ORDER - 2; j >= 0; j--){ // 因为节点未满,所以从倒数第二项向前查找
            if (itemArray[j] == null) // 没存数据
                continue;
            else {
                long itsKey = itemArray[j].dData; // 获得关键字
                if (newKey < itsKey) // 插入位置在其前面,但未必相邻
                    itemArray[j + 1] = itemArray[j]; // 当前数据项后移
                else {
                    itemArray[j + 1] = newItem; // 在其后位置插入
                    return j + 1; // 返回插入的位置下标
                }
            }
        }
        // 若上述代码没有执行返回操作,那么这是空节点(只有初始时根是这个情况)
        itemArray[0] = newItem; // insert new item
        return 0;
    }

    // 移除数据项,从后向前移除
    public DataItem removeItem(){
        // 假设节点非空
        DataItem temp = itemArray[numItems - 1]; // 要移除的数据项
        itemArray[numItems - 1] = null; // 移除
        numItems--; // 数据项数目减一
        return temp; // 返回要移除的数据项
    }

    public void displayNode() { // format "/24/56/74/"
        for (int j = 0; j < numItems; j++)
            itemArray[j].displayItem(); // "/56"
        System.out.println("/"); // final "/"
    }
}

// DataItem类表示节点中存储的数据项的数据类型。
public class DataItem {
    public long dData; // 存储的数据类型,可以为其他复杂的对象或自定义对象
    public DataItem(long dd) {
        dData = dd;
    }
    public void displayItem() {
        System.out.print("/" + dData);
    }
}


/**
 * 
 * @ClassName: Tree234
 * @Description: 
 *  Tree234类来表示一颗完整的2-3-4树。它只有一个数据项:root,类型为Node。我们操作一棵树,只需要知道它的根就行了。
    关键方法如下:
    find:根据关键字查找树中是否存在。从根开始,依次调用getNextChild方法来向下查找,在每个节点上都调用Node类中的            findItem方法在当前节点中查找。当在底层的叶结点查找完毕,整个查找过程就结束了。若仍未找到,则查找失败返回-1。
    insert:与find方法类似,不断向下查找,直到叶结点,插入数据项。这个过程中遇到满节点会先执行分裂操作,调用split方          法,再来插入数据项。
    split:按照之前介绍的分裂方法进行分裂。
 * @author andy
 * @date 2017年11月1日 下午4:53:00
 */
public class Tree234 {
    private Node root = new Node(); // 创建树的根

    // 获取查找的下一个节点
    public Node getNextChild(Node theNode, long theValue) {
        int j;
        // 假设这个节点不是叶结点
        int numItems = theNode.getNumItems(); // 获得当前节点的数据项数目
        for (j = 0; j < numItems; j++) {
            if (theValue < theNode.getItem(j).dData)
                return theNode.getChild(j); // 返回相应的节点
        }
        return theNode.getChild(j); // 此时j=numItems
    }

    public int find(long key) {
        Node curNode = root;
        int childNumber;
        while (true) {
            if ((childNumber = curNode.findItem(key)) != -1) // 每次循环这句一定执行
                return childNumber; // found it
            else if (curNode.isLeaf()) // 叶结点上也没找到
                return -1; // can't find it
            else // 不是叶结点,则继续向下查找
                curNode = getNextChild(curNode, key);
        }
    }

    // 插入数据项
    public void insert(long dValue) {
        Node curNode = root; // 当前节点标志
        DataItem tempItem = new DataItem(dValue); // 插入数据项封装
        while (true) {
            if (curNode.isFull()) {
                split(curNode); // 分裂
                curNode = curNode.getParent(); // 回到分裂出的父节点上,继续向下查找
                curNode = getNextChild(curNode, dValue);
            } // end if(node is full)
            // 后面的操作中节点都未满,否则先执行上面的代码
            else if (curNode.isLeaf()) // 是叶结点,非满
                break; // 跳出,直接插入
            else
                curNode = getNextChild(curNode, dValue);// 向下查找
        }
        curNode.insertItem(tempItem); // 此时节点一定不满,直接插入数据项
    }

    public void split(Node thisNode){ // 分裂
        // 操作中节点一定是满节点,否则不会执行该操作
        DataItem itemB, itemC;
        Node parent, child2, child3;
        int itemIndex;
        System.out.println("");
        itemC = thisNode.removeItem(); // 移除最右边的两个数据项,并保存为B和C
        itemB = thisNode.removeItem();
        child2 = thisNode.disconnectChild(2); // //断开最右边两个子节点的链接
        child3 = thisNode.disconnectChild(3);

        Node newRight = new Node(); // 新建一个节点,作为当前节点的兄弟节点

        if (thisNode == root) { // 是根
            root = new Node(); // 新建一个根
            parent = root; // 把新根设为父节点
            root.connectChild(0, thisNode); // 连接父节点和子节点
        } else{
            parent = thisNode.getParent(); // 获取父节点
        }
        itemIndex = parent.insertItem(itemB); // 把B插入父节点中,返回插入位置
        int n = parent.getNumItems(); // 获得总数据项数目

        for (int j = n - 1; j > itemIndex; j--) { // 从后向前移除, 注释:此处分裂后,添加到父节点的key在index=0的位置上
            Node temp = parent.disconnectChild(j); // 断开连接
            parent.connectChild(j + 1, temp); // 连接到新的位置
        }
        parent.connectChild(itemIndex + 1, newRight); // 连接到新位置
        // 处理兄弟节点
        newRight.insertItem(itemC); // 将C放入兄弟节点中
        newRight.connectChild(0, child2); // 把子节点中最右边的两个连接到兄弟节点上
        newRight.connectChild(1, child3);
    } 

    // gets appropriate child of node during search for value
    public void displayTree() {
        recDisplayTree(root, 0, 0);
    }
    
    private void recDisplayTree(Node thisNode, int level, int childNumber) {
        System.out.print("level=" + level + " child=" + childNumber + " ");
        thisNode.displayNode(); // display this node

        // call ourselves for each child of this node
        int numItems = thisNode.getNumItems();
        for (int j = 0; j < numItems + 1; j++) {
            Node nextNode = thisNode.getChild(j);
            if (nextNode != null)
                recDisplayTree(nextNode, level + 1, j);
            else
                return;
        }
    }
}

public class Tree234App {
    public static void main(String[] args) throws IOException {
        long value = 0;
        Tree234 theTree = new Tree234();
        theTree.insert(50);
        theTree.insert(40);
        theTree.insert(60);
        theTree.insert(30);
        theTree.insert(70);        
        while (true) {
            System.out.print("Enter first letter of ");
            System.out.print("show, insert, or find: ");
            char choice = getChar();
            switch (choice) {
            case 's':
                theTree.displayTree();
                break;
            case 'i':
                System.out.print("Enter value to insert: ");
                value = getInt();
                theTree.insert(value);
                break;
            case 'f':
                System.out.print("Enter value to find: ");
                value = getInt();
                int found = theTree.find(value);
                if (found != -1)
                    System.out.println("Found "+ found +","+ value);
                else
                    System.out.println("Could not find " + value);
                break;
            default:
                System.out.print("Invalid entry\n");
            }
        }
    }

    public static String getString() throws IOException {
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(isr);
        String s = br.readLine();
        return s;
    }

    public static char getChar() throws IOException {
        String s = getString();
        return s.charAt(0);
    }

    public static int getInt() throws IOException {
        String s = getString();
        return Integer.parseInt(s);
    }
}

参考:https://www.cnblogs.com/ysocean/p/8032648.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值