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节点
- 创建一个新的空间点。它是要分裂节点的兄弟,再要分裂的节点的右边
- 数据项C移到新的节点中
- 数据项B移到要分裂节点的父节点中
- 数据项A保留在原来的位置上
- 最右边的两个子节点从分裂节点处断开,连接到新的节点上面
==注意:==节点分裂是把数据项向上和向右移动,正是这样的重新排列才可以保持树的平衡,
插入只需要分裂一个节点,除非插入的路径上存在不止一个满的节点,这种情况需要多重分裂。
根的分裂
如果一开始查找插入点时就碰到满根的话,插入过程会更复杂一点
根分裂的过程创建了新的根,比旧的高一层,因此整个树的高度就增加了1,另一种描述根分裂的方法就是讲一个4节点变成了三个2节点
如图:
顺着分裂的节点,继续向下查找插入点,如上图,关键值为41的数据项插入找到合适的节点里面
分裂根的步骤
- 创建新的根,它是要分裂节点的父节点
- 创建第二个新的节点,它是要分裂节点的兄弟节点
- 数据项C移动到新的兄弟节点上面
- 数据项B移动到新的根节点上面
- 数据项A保留在原来的位置
- 要分裂的节点最右边的两个子节点断开连接,连接到新的兄弟节点中
在下行路途中分裂
注意,因为所有满节点都是在下行路途中分裂的,分裂不可能向回波及到树上面的节点,任何要分裂的节点的父节点肯定不是满的,因此该节点不需要分裂的就可以插入数据项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树转红黑树
- 把2-3-4-树的每个2-节点转换为红黑树的黑节点
2. 把每3-节点转换为一个子节点和一个父节点,子节点有两个自己的子节点,如下图中的W和X或X和Y,父节点有另一个子节点,Y或W,哪个节点变成子节点或父节点都无所谓,子节点变成红色,父节点变成黑色
3. 把每4-节点转化一个父节点和两个子节点,如下图,第一个子节点有自己的子节点W和X,第二个子节点有自己的子节点Y和Z,前面一样,子节点涂成红河,父节点涂成黑色
一个完整的2-3-4树转换为红黑树
同样节点的分裂和颜色变换也是等价的如下图
2-3-4树的总结
- 多叉树比二叉树有更多的关键字和子节点
- 2-3-4树是多叉树,每个节点最多有三个关键字和四个子节点
- 多叉树中,节点中数据项按关键字升序排列
- 2-3-4树中,所有的插入都在叶子节点上,所有的叶节点都在同一层
- 在2-3-4中有三种可能的节点。2-节点有一个关键字和两个子节点,3-节点有2个关键字和3个子节点,4-节点有三个关键字和四个子节点
- 2-3-4树中没有1-节点
- 在2-3-4树中查找时,检查每一个关键字,没有找到时,如果要查找节点的关键字比第一个数据项小,则进行当前节点的第一个子节点,如果查找的在第一个和第二个关键值中间,则查找当前节点的第二个子节点,如果查找的是在第二个与第三个关键值中间,则查找当前节点的第三个子节点,如果查找值大于当前节点的第三个关键值,则查找当前节点的第四个子节点
- 在2-3-4树中插入需要在查找插入点的过程中,顺着路径向下分裂路径上每个满的节点
- 分裂根要创建两个新节点,分裂出另一个节点,创建一个新的节点
- 2-3-4树和红黑树存在一对一的对应关系
- 把2-3-4树转化为红黑树,需要把2-节点变为黑色节点,将3-节点变为一个黑色的父节点和一个红色的子节点,将4-节点编程一个黑色的父节点和两个红色的子节点
- 当3-节点化作一个父节点和一个子节点的时候,每一个节点都可以做父节点
- 2-3-4树中的分裂节点和在红黑树中的颜色变换是一样的
- 2-3-4树很浪费空间,应为很多节点还不满一半