2-3-4 Tree

来源:互联网 发布:无法优化游戏的问题 编辑:程序博客网 时间:2024/05/16 01:16

2-3-4 Tree

简介

2-3-4 Tree又叫2-4 Tree,属于一种平衡查找树,其高度满足:<=$\log_2 x N$,关于性能问题,以后会专门出个小专题来讨论。

– 以下出自[维基百科]

  • 2-节点,就是说,他包含1个元素和2个子节点
  • 3-节点,就是说,他包含2个元素和3个子节点
  • 4-节点,就是说,他包含3个元素和4个子节点

从上面我们可以看出一个非叶子节点的子节点的个数总是比它的元素个数少一个。
2-4Tree是一棵查找树,因此它的元素的排列也是具有一定的顺序。具体可自行查阅。本文主要给出2-4Tree相关操作的主要算法。
为了行文方便,下面统一将2-3-4 Tree成为2-4 Tree。

操作算法分析

本文所用代码取自倪升武的博客,本人只是对其代码进行了一些注解和说明。在此对倪升武 表示感谢!

2-4Tree操作节点的定义代码

这里我们将节点内部的值包装进DataItem类,首先是DataItem类:

// 数据项class DataItem{    public long dData;    public DataItem(long data)    {        dData = data;    }    public void displayItem()    {        System.out.print("/" + dData);    }}

然后是2-4Tree的节点定义类:

// 节点class Node2{    private static final int ORDER = 4;    private int numItems; // 表示该节点存有多少个数据项    private Node2 parent; //父节点    private Node2 childArray[] = new Node2[ORDER]; // 存储子节点的数组,最多4个子节点    private DataItem itemArray[] = new DataItem[ORDER - 1];// 该节点中存放数据项的数组,每个节点最多存放3个数据项    // 将child节点连接到节点的childNum的位置上    public void connectChild(int childNum, Node2 child)    {        childArray[childNum] = child;        if (child != null)        {            child.parent = this;        }    }    // 断开与子节点的连接,并返回该子节点    public Node2 disconnectChild(int childNum)    {        Node2 tempNode = childArray[childNum];        childArray[childNum] = null;        return tempNode;    }    //得到childNum位置上的子节点    public Node2 getChild(int childNum)    {        return childArray[childNum];    }    public Node2 getParent()    {        return parent;    }    //判断是否是叶子节点,依据:因为对于非叶子节点,总是满足子节点个数=父节点元素个数+1;在满足这种条件的情况下,子节点的排列总是从childArray[0]开始。    public boolean isLeaf()    {        return (childArray[0] == null);    }    //得到节点内部元素的个数    public int getNumItems()    {        return numItems;    }    //得到指定位置的元素    public DataItem getItem(int index)    {        return itemArray[index];    }    //"满"的判断    public boolean isFull()    {        return (numItems == ORDER - 1);    }    //在节点内部寻找值为key的元素    public int findItem(long key)    {        for (int j = 0; j < ORDER - 1; j++)//循环次数为4次        {            if (itemArray[j] == null)//因为2-3-4树节点内部元素排列总是连续的,即2个元素之间不存在null            {                break;//满足条件,跳出循环            } else if (itemArray[j].dData == key)            {                return j;//找到即返回            }        }        return -1;//未在上述代码返回,说明并未找到,此时返回-1    }    //节点插入元素(DataItem)    public int insertItem(DataItem newItem)    {        //这里我们不考虑节点内元素已满的情况,因为在对树调用这个方法之前,我们会对节点是否满进行判断。        numItems++;        long newKey = newItem.dData;        for (int j = ORDER - 2; j >= 0; j--)//因为没有满,因此元素个数最多为3,因此循环次数定位3,能否优化?(**跟博主沟通过,博主也同意此处可以将循环次数减少至2,因为方法开头我们已经假定元素未满,而且null位置总是位于最后,因此可以不必再判定最后一个元素**)        {             if (itemArray[j] == null)            { // item is null                continue; // get left one cell            } else            { // not null                long itsKey = itemArray[j].dData; // get its key                if (newKey < itsKey)//如果新元素值小于itsKey,则将itsKey右移                {                     itemArray[j + 1] = itemArray[j]; // shift it right                } else                {                    itemArray[j + 1] = newItem; // insert new item                    return j + 1; // 返回添加的新元素的位置                }            }        }        //下面主要是考虑该节点为空值节点,即itemArray数组中全部为null的情况。        //显然此时应该插入到itemkey[0]的位置        itemArray[0] = newItem;        return 0;    }    //移除节点内的值(从itemKey的右边开始移除一个值,并返回移除的值)    public DataItem removeItem()    {        // assumes node not empty        DataItem tempItem = itemArray[numItems - 1]; // save item        itemArray[numItems - 1] = null; // disconnect it        numItems--;        return tempItem;    }    //显示元素数组    public void displayNode()    {        for (int i = 0; i < numItems; i++)        {            itemArray[i].displayItem();        }        System.out.println("/");    }}

分裂

如果一个节点的元素个数已经达到3个,那么我们称该节点已“满”,在插入新节点的过程中,如果遇到这种节点,我们需要对其进行分裂操作,分裂过程也是保证2-4Tree平衡的一种方式,正是这种分裂过程才保证了树的平衡。(建议先看下面较完整的代码)

public void split(Node2 currentNode)//我感觉这是核心方法,    {        //假定节点已经满了,因为满节点才能分裂        //分裂时,将中间值插到父节点中,右值分离出来独立成一个节点连接到父节点上        DataItem itemB, itemC; // 存储要分裂节点的后两个DataItem(即中值和右值)        Node2 parent, child2, child3; // 存储要分裂节点的父节点和后两个child        int itemIndex;        itemC = currentNode.removeItem();//移除本节点的右值,并用itemC保存这个值(右值)        itemB = currentNode.removeItem(); // 继续移除本节点的中值,并用itemB保存这个值(中值)        child2 = currentNode.disconnectChild(2);//因为中值和右值的移除,因此需要移除与这2个值有关的2个子节点        child3 = currentNode.disconnectChild(3); // 同上                                                    // node        Node2 newRight = new Node2(); // make a new node        if (currentNode == root)//如果root节点“满”,那么将新节点作为root节点        {            root = new Node2(); // make a new root            parent = root; // root is our parent            root.connectChild(0, currentNode);// connect currentNode to parent        } else//如果该满节点不是root节点,那么得到其父节点,便于后面将分裂出的值存到该父节点        {            parent = currentNode.getParent();        }        // deal with parent        itemIndex = parent.insertItem(itemB); // 中值插入,并返回插入的位置        int n = parent.getNumItems(); // 得到父节点中值的个数,便于后面对添加的子节点的操作        for (int j = n - 1; j > itemIndex; j--)//因为父节点中有值的插入,因此需要调整与值有关的子节点的连接位置        {//调整方法,插入值后方的子节点均后移一位,这样最后就会空出来新插入值与其后方值之间子节点的“线”,然后在此处插入我们分离出来的右值            Node2 temp = parent.disconnectChild(j);//            parent.connectChild(j + 1, temp);        }        parent.connectChild(itemIndex + 1, newRight);        // deal with newRight        newRight.insertItem(itemC);//在新节点中插入之前我们移除的右值        newRight.connectChild(0, child2);//连接与移除的右值有关的子节点        newRight.connectChild(1, child3);    }

完整代码注解

public class Tree234{    private Node2 root = new Node2();    public int find(long key)    {        Node2 currentNode = root;        int childNumber;        while (true)        {            if ((childNumber = currentNode.findItem(key)) != -1)//在节点中查找,如果返回的位置值不为-1,说明找到了,然后返回位置值            {                return childNumber;            } else if (currentNode.isLeaf())//如果是叶子节点,说明无后继节点了,因此不需要进行下面的移后变换了            {                return -1;            } else            {                currentNode = getNextChild(currentNode, key);//如果节点中找不到值key,那么定位到他的子节点中寻找。            }        }    }    // insert a DataItem    public void insert(long data)    {        Node2 currentNode = root;        DataItem tempItem = new DataItem(data);        while (true)        {            if (currentNode.isFull())//如果节点是满节点            {                split(currentNode); // if node is full, split it                currentNode = currentNode.getParent(); // 父节点被我们改变了,因此需要回退重新判定插入位置                currentNode = getNextChild(currentNode, data); // 判定插入位置            } else if (currentNode.isLeaf())//如果是叶子节点,那么该节点就是我们需要将值插入的节点            { // if node if leaf                break; // go insert            } else//既不是叶子节点,也不是满节点            {                currentNode = getNextChild(currentNode, data);//通过值来判定currentNode节点是否需要移位            }        }        currentNode.insertItem(tempItem);//以上代码都是进行寻找插入节点的位置,这里找到之后,进行插入操作。    }    // display tree    public void displayTree()    {        recDisplayTree(root, 0, 0);    }    public Node2 getNextChild(Node2 currentNode, long key)    {        int j;        // assumes node is not empty, not full and not leaf        //假定节点非空且未满且不是叶子节点(因此后面使用该方法时,会对使用该方法的条件加以限制)        int numItems = currentNode.getNumItems();//得到元素的个数        for (j = 0; j < numItems; j++)//进行循环比较        {            if (key < currentNode.getItem(j).dData)//如果key小,则返回j位置处的子节点            {                return currentNode.getChild(j);            }        }        return currentNode.getChild(j);//循环完毕,则移到尾位置的子节点    }    public void split(Node2 currentNode)//我感觉这是核心方法,    {        //假定节点已经满了,因为满节点才能分裂        //分裂时,将中间值插到父节点中,右值分离出来独立成一个节点连接到父节点上        DataItem itemB, itemC; // 存储要分裂节点的后两个DataItem(即中值和右值)        Node2 parent, child2, child3; // 存储要分裂节点的父节点和后两个child        int itemIndex;        itemC = currentNode.removeItem();//移除本节点的右值,并用itemC保存这个值(右值)        itemB = currentNode.removeItem(); // 继续移除本节点的中值,并用itemB保存这个值(中值)        child2 = currentNode.disconnectChild(2);//因为中值和右值的移除,因此需要移除与这2个值有关的2个子节点        child3 = currentNode.disconnectChild(3); // 同上                                                    // node        Node2 newRight = new Node2(); // make a new node        if (currentNode == root)//如果root节点“满”,那么将新节点作为root节点        {            root = new Node2(); // make a new root            parent = root; // root is our parent            root.connectChild(0, currentNode);// connect currentNode to parent        } else//如果该满节点不是root节点,那么得到其父节点,便于后面将分裂出的值存到该父节点        {            parent = currentNode.getParent();        }        // deal with parent        itemIndex = parent.insertItem(itemB); // 中值插入,并返回插入的位置        int n = parent.getNumItems(); // 得到父节点中值的个数,便于后面对添加的子节点的操作        for (int j = n - 1; j > itemIndex; j--)//因为父节点中有值的插入,因此需要调整与值有关的子节点的连接位置        {//调整方法,插入值后方的子节点均后移一位,这样最后就会空出来新插入值与其后方值之间子节点的“线”,然后在此处插入我们分离出来的右值            Node2 temp = parent.disconnectChild(j);//            parent.connectChild(j + 1, temp);        }        parent.connectChild(itemIndex + 1, newRight);        // deal with newRight        newRight.insertItem(itemC);//在新节点中插入之前我们移除的右值        newRight.connectChild(0, child2);//连接与移除的右值有关的子节点        newRight.connectChild(1, child3);    }    public void recDisplayTree(Node2 thisNode, int level, int childNumber)    {        System.out.print("level = " + level + " child = " + childNumber + " ");        thisNode.displayNode();        // call ourselves for each child of this node        int numItems = thisNode.getNumItems();        for (int j = 0; j < numItems + 1; j++)        {            Node2 nextNode = thisNode.getChild(j);            if (nextNode != null)            {                recDisplayTree(nextNode, level + 1, j);            } else                continue;        }    }}

如果有不当之处,敬请各位朋友指正!

0 0
原创粉丝点击