数据结构与算法——2-3-4树详解java

本文详细介绍了2-3-4树的结构和特性,包括其与二叉树的区别、组织原则、搜索、插入操作以及节点分裂过程。2-3-4树是一种多叉树,每个节点最多有三个数据项和四个子节点,其数据项按升序排列。插入操作可能导致节点分裂以保持平衡,而根节点分裂时会创建新的根节点。此外,文章还探讨了2-3-4树与红黑树之间的转换关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2-3-4树的介绍

在二叉树中,每个节点有一个数据项,最多有两个子节点,当每个节点有多个数据项和更多的节点的话这就是多叉树,这里我说的2-3-4树就是一个多叉树,它的每个节点最多有三个数据项和四个节点,它和红黑树一样是讲究平衡的,它的效率比红黑树稍微差一些。但是编程比红黑树容易,其实最重要的是通过红黑树更容易了解B树,B树是另外的一种树,B树的节点可以有几十个或者几百个。
下面是一个小的2-3-4树
在这里插入图片描述
图中的三个节点有子节点,底层有两个叶子节点,2-3-4树中所有的叶子节点都会在一层,
2-3-4树听着名字就知道和2,3,4相关,下面我就详细的说一下为什么叫2-3-4树

  • 有一个数据项的总是有两个子节点
  • 有两个数据项的总有三个子节点
  • 有三个数据项的总是有四个子节点
    也就是说非叶节点的子节点树总比它含有的数据项多一,或者,用符号表示这个规则,假设子节点的连接数为L,数据项的个数是D ,那么L=D+1 这就是2-3-4树名字的含义
    这个关系决定的2-3-4树的结构,比较来说,叶节点没有子节点,然后他可能含有两个或者三个数据项,空节点是不会存在的
    因为2-3-4树最多可以有四个子节点,也可以称它为4叉树。

二叉树和2-3-4树的不同

我们知道二叉树可以只有一个子节点,指向它的左节点或者右节点,它的另一个链接可以为null值,然后在2-3-4树中不允许只有一个连接,有一个数据项的节点总是保持着两个链接,除非是它的叶节点,在那种情况下没有链接。
下面的图可以很好地看清楚2-3-4树的数据项与链接的节点
在这里插入图片描述

2-3-4树的组织

下面我讲解一下2-3-4树的组织以及排序
首先节点中的数据按照关键字值升序排列,习惯上从左到右的升序
我先来假设数据项是A,B,C因为最多只有三个数据项
节点的我按照1.2.3.4排列,最多有四个节点

  • 1节点里面的数据项都是比父节点的数据项中的A小
  • 2节点里面的数据项在父节点中大于A小于B的值
  • 3节点里面的数据项在父节点中是大有B小于C的值
  • 4节点里面的数据项都是大于C的值
    如下图
    在这里插入图片描述
    但是叶节点都是在同一层,也就是最底层的,上面层的节点一般都是不满的,也就是说,他们可能只含有一个或者两个数据项而不是三个
    注意:树是平衡的,即使插入一组升序或者降序的数据在2-3-4树都能保持平衡,2-3-4树的自我平衡能力取决于新节点的插入方式

搜索2-3-4树

查找特定的值的数据项和二叉树搜索很类似,从根开始,除非查找的关键值就是根,否则选择关键子值所在的范围,转向那个方向直到查找到为止。下图就是假设查找关键字为64的数据项
在这里插入图片描述1. 首先判断是不是根,不是根就判断是不是比根大或者比根小
2. 比根大的时候走右边的子节点,然后判断是不是比60小或者在60-70之间,70-80之间 ,比80大
3. 当判断是在60-70之间就转到第二个子节点,就会找到关键字值为64的项,如果没有找到就会返回没找到

插入

新的数据项总是插入在叶子节点里面,在树的最底层,如果插入到右子节点的节点里面,子节点的编号就要发生变化,这样才能保持树的结构,保证子节点树比数据项里面的多1.
2-3-4树的插入有时很简单,有时相当复杂。无论哪一种情况都是从查找适当的叶节点开始的。
插入时没有碰到满节点时,插入很简单,找到合适的叶子节点后,只要把新数据项插入进去就可以了,如下图
在这里插入图片描述
插入可能会涉及到在一个节中移动一个或者两个其他的数据项,这样在新数据项插入后关键值仍然是保持正常的顺序

节点分裂

如果往下寻找插入位置的路途中,发现节点已经满了,插入就会变得很复杂,发生这种情况,节点必须分裂,正是因为这种分裂节点才会保持树的平衡,这里讨论的是2-3-4树的一种自顶向下的分裂,因为是在向下找到插入点的路途中发生分裂,把要分裂节点中的数据项设为A,B,C下面就是分裂的情况,这种分裂可以描述的是一个4节点变成两个2节点
在这里插入图片描述

  1. 创建一个新的空间点。它是要分裂节点的兄弟,再要分裂的节点的右边
  2. 数据项C移到新的节点中
  3. 数据项B移到要分裂节点的父节点中
  4. 数据项A保留在原来的位置上
  5. 最右边的两个子节点从分裂节点处断开,连接到新的节点上面
    ==注意:==节点分裂是把数据项向上和向右移动,正是这样的重新排列才可以保持树的平衡,
    插入只需要分裂一个节点,除非插入的路径上存在不止一个满的节点,这种情况需要多重分裂。

根的分裂

如果一开始查找插入点时就碰到满根的话,插入过程会更复杂一点
根分裂的过程创建了新的根,比旧的高一层,因此整个树的高度就增加了1,另一种描述根分裂的方法就是讲一个4节点变成了三个2节点
如图:
在这里插入图片描述
顺着分裂的节点,继续向下查找插入点,如上图,关键值为41的数据项插入找到合适的节点里面

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

在下行路途中分裂

注意,因为所有满节点都是在下行路途中分裂的,分裂不可能向回波及到树上面的节点,任何要分裂的节点的父节点肯定不是满的,因此该节点不需要分裂的就可以插入数据项B,当然,如果父节点的子节点分裂时它已经有了两个子节点了,她就变满了,但是这意味着下次碰见它就要进行分裂,下面描述的是空树的一系列的插入过程,只有四个节点分裂了,两个是根,两个是叶节点。
如图

在这里插入图片描述在这里插入图片描述



    //    数据项
    public class DataItem {
        public long dData;

        public DataItem(long dData) {
            this.dData = dData;
        }

        public void displayItem() {
            System.out.println("/" + dData);
        }
    }


    // 节点
    public class Node {

        private static final int ORDER = 4;

        private int numItems;//表示该节点有多少数据项

        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 int getNumItems() {
            return numItems;
        }

        //       判断是否是叶节点
        public boolean isLeaf() {
            return (childArray[0] == null) ? true : false;
        }

        //        得到节点的某个数据项
        public DataItem getItem(int key) {
            return itemArray[key];
        }

        //        判断数据项是否满了 最多就只有三个
        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) {
//            首先节点里面的数据项+1
            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;
            return 0;
        }

        //        移除节点的数据项
        public DataItem removeItem() {
            DataItem temp = itemArray[numItems - 1];
            itemArray[numItems - 1] = null;//将数据项移除
            numItems--;//数据项的个数减一
            return temp;
        }

        //        打印节点的所有数据项
        public void disPlayNode() {
            for (int j = 0; j < numItems; j++) {
                itemArray[j].displayItem();
            }
            System.out.println("/");
        }
    }


    private Node root = new Node();

    //    查找关键值
    public int finf(long key) {
        Node curNode = root;
        int childNumber;
        while (true) {
            if ((childNumber = curNode.findItem(key)) != -1) {//找到数据项在节点中的位置
                return childNumber;//找到了就直接返回
            } else if (curNode.isLeaf()) {//判断是否是叶子节点
                return -1;//查找到叶子节点还没有 则返回-1
            } else {//没有找到关键值则继续寻找
                curNode = getNextChild(curNode, key);
            }
        }
    }

    public Node getNextChild(Node theNode, Long key) {
        int j;
        int number = theNode.getNumItems();//得到节点数据项的个数
        for (j = 0; j < number; j++) {
            if (key < theNode.getItem(j).dData) {//一次比较节点中的数据项 查找的数据值小于当前数据项的关键值
                return theNode.getChild(j);//得到某个节点的子节点 查找的关键值小于当前数据项则返回 没有的话则继续查找

            }
        }
        return theNode.getChild(j);//得到某个节点的子节点
    }


    //    插入数据项
    public void insert(long dValue) {
        Node curNode = root;
        DataItem dataItem = new DataItem(dValue);
        while (true) {
            if (curNode.isFull()) {//判断是否是满子节点,是的话 则分裂节点
                split(curNode);
                curNode = curNode.getParent();
                curNode = getNextChild(curNode, dValue);
            } else if (curNode.isLeaf()) {//当前节点是叶子节点
                break;
            } else {
                curNode = getNextChild(curNode, dValue);
            }
        }
        curNode.insertItem(dataItem);
    }

    //    分裂节点
    public void split(Node thisNode) {
        DataItem itemB, itemC;//上面说的是 B为新的父节点 C为新的兄弟节点
        Node parent, child2, child3;
        int itemIndex;
        itemC = thisNode.removeItem();//最右边的数据项
        itemB = thisNode.removeItem();
        child2 = thisNode.disConnectChild(2);// 断开与子节点的连接,并返回该子节点
        child3 = thisNode.disConnectChild(3);
        Node newRigh = 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--) {
            Node temp = parent.disConnectChild(j);//断开与子节点的连接,并返回该子节点
            parent.connectChild(j + 1, temp);
        }
        parent.connectChild(itemIndex + 1, newRigh);
// 处理右节点
        newRigh.insertItem(itemC);//将c出入新的父节点
        newRigh.connectChild(0, child2);
        newRigh.connectChild(1, child3);
    }

    // 打印树节点
    public void displayTree() {
        recDisplayTree(root, 0, 0);
    }

    private void recDisplayTree(Node thisNode, int level, int childNumber) {
        System.out.println("levle=" + level + " child=" + childNumber + " ");
        thisNode.disPlayNode();
        int numbers = thisNode.getNumItems();
        for (int j = 0; j < numbers + 1; j++) {
            Node node = thisNode.getChild(j);
            if (node != null) {
                recDisplayTree(node, level - 1, j);
            } else {
                return;
            }
        }

    }
}

2-3-4树与红黑树

2-3-4树与红黑树看上去可能完全不同,但是,在某种意义上他们又是完全相同的,一个可以通过应用一些简单的规则转换成另外一个,而且使它们保持平衡的操作也是一样的,数学上称它们为同构,

2-3-4树转红黑树
  1. 把2-3-4-树的每个2-节点转换为红黑树的黑节点

在这里插入图片描述
2. 把每3-节点转换为一个子节点和一个父节点,子节点有两个自己的子节点,如下图中的W和X或X和Y,父节点有另一个子节点,Y或W,哪个节点变成子节点或父节点都无所谓,子节点变成红色,父节点变成黑色
在这里插入图片描述
3. 把每4-节点转化一个父节点和两个子节点,如下图,第一个子节点有自己的子节点W和X,第二个子节点有自己的子节点Y和Z,前面一样,子节点涂成红河,父节点涂成黑色
在这里插入图片描述
一个完整的2-3-4树转换为红黑树
在这里插入图片描述
在这里插入图片描述
同样节点的分裂和颜色变换也是等价的如下图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述在这里插入图片描述

2-3-4树的总结

  1. 多叉树比二叉树有更多的关键字和子节点
  2. 2-3-4树是多叉树,每个节点最多有三个关键字和四个子节点
  3. 多叉树中,节点中数据项按关键字升序排列
  4. 2-3-4树中,所有的插入都在叶子节点上,所有的叶节点都在同一层
  5. 在2-3-4中有三种可能的节点。2-节点有一个关键字和两个子节点,3-节点有2个关键字和3个子节点,4-节点有三个关键字和四个子节点
  6. 2-3-4树中没有1-节点
  7. 在2-3-4树中查找时,检查每一个关键字,没有找到时,如果要查找节点的关键字比第一个数据项小,则进行当前节点的第一个子节点,如果查找的在第一个和第二个关键值中间,则查找当前节点的第二个子节点,如果查找的是在第二个与第三个关键值中间,则查找当前节点的第三个子节点,如果查找值大于当前节点的第三个关键值,则查找当前节点的第四个子节点
  8. 在2-3-4树中插入需要在查找插入点的过程中,顺着路径向下分裂路径上每个满的节点
  9. 分裂根要创建两个新节点,分裂出另一个节点,创建一个新的节点
  10. 2-3-4树和红黑树存在一对一的对应关系
  11. 把2-3-4树转化为红黑树,需要把2-节点变为黑色节点,将3-节点变为一个黑色的父节点和一个红色的子节点,将4-节点编程一个黑色的父节点和两个红色的子节点
  12. 当3-节点化作一个父节点和一个子节点的时候,每一个节点都可以做父节点
  13. 2-3-4树中的分裂节点和在红黑树中的颜色变换是一样的
  14. 2-3-4树很浪费空间,应为很多节点还不满一半
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值