文章目录
2-3-4树简介
2-3-4树的结构
2-3-4树是一种多叉树,它的每个节点最多有三个数据项和四个子节点。
非叶节点数据项和其子节点数量的关系:
- 有一个数据项的节点总是有两个子节点
- 有两个数据项的节点总是有三个子节点
- 有三个数据项的节点总是有四个子节点
简而言之,非叶节点的子节点数总是比它含有的数据项多1。
叶节点(上图最下面的一排)是没有子节点的,然而叶节点可能含有一个、两个或三个数据项。空节点是不会存在的。
2-3-4树数据项之间的大小关系
树结构中很重要的一点就是节点之间关键字值大小的关系。(为了方便描述2-3-4树节点关键字之间的大小关系,用从0到2的数字给数据项编号,用0到3的数字给子节点编号,如下图)
- 根是child0的子树的所有子节点的关键字值小于key0;
- 根是child1的子树的所有子节点的关键字值大于key0并且小于key1;
- 根是child2的子树的所有子节点的关键字值大于key1并且小于key2;
- 根是child3的子树的所有子节点的关键字值大于key2。
由于2-3-4树中一般不允许出现重复关键值,所以不用考虑比较关键值相同的情况。
2-3-4树的操作
搜索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树的平衡。
节点分裂
在寻找插入数据位置的过程中,如果遇到了满数据项的节点,该满数据项节点就要分裂。
- 创建一个新的空节点,它是要分裂节点的兄弟,在要分裂节点的右边;
- 数据项C移到新节点中;
- 数据项B移到要分裂节点的父节点中;
- 数据项A保留在原来的位置;
- 最右边的两个子节点从要分裂处断开,连到新节点上。
一般插入只需要分裂一个节点,除非插入路径上存在不止一个满节点时,这种情况就需要多重分裂。
根的分裂
如果一开始查找插入节点时就碰到满的根节点,那么插入过程更复杂。
- 创建新的根节点,它是要分裂节点的父节点。
- 创建第二个新的节点,它是要分裂节点的兄弟节点;
- 数据项C移到新的兄弟节点中;
- 数据项B移到新的根节点中;
- 数据项A保留在原来的位置;
- 要分裂节点最右边的两个子节点断开连接,连到新的兄弟节点中。
分裂完成之后,整个树的高度加1。
Java实现2-3-4树
封装数据项的类
package tree234;
/**
* 封装数据项的类
*/
public class DataItem {
public int data;//节点中的具体的数据项的值,为了简化代码,这里用public定义数据类型
//在这里还可以同时封装其他属性
//构造方法
public DataItem(int data){
this.data = data;
}
//打印数据项的方法
public void displayItem(){
System.out.print("/" + data);
}
}
封装节点的类
package tree234;
/**
* 234数的节点封装起来的类
*/
public class Node {
//Node节点里的基本属性值
//把最大的节点数,做成一个常数
private static final int ORDER = 4;//最多4个子节点
private int numItems;//保存节点中含有的数据项的个数
private Node parent;//指向当前节点的父节点
private Node[] childArray = new Node[ORDER];//指向当前节点的子节点的指针集
private DataItem[] itemArray = new DataItem[ORDER-1];//封装节点中的数据
//返回节点中的数据项的数组
public DataItem[] getItemArray(){
return itemArray;
}
//连接子节点。
//将child节点作为当前节点的第childNum个子节点
public void connectChild(int childNum, Node child){
if (childNum>=0 && childNum<ORDER){
childArray[childNum] = child;
if (child != null){
child.parent = this;
}
}else {
System.out.println("2-3-4树节点不允许索引为"+childNum+"的子节点");
}
}
//断开子节点,并返回子节点
public Node disconnectChild(int childNum){
if (childNum>=0 && childNum<ORDER) {
Node childNode = childArray[childNum];
childArray[childNum] = null;//断开
return childNode;
}else {
System.out.println("2-3-4树节点不会存在索引为"+childNum+"的子节点");
return null;
}
}
//拿到当前节点的某个指定的子节点
public Node getChild(int childNum){
if (childNum>=0 && childNum<ORDER) {
return childArray[childNum];
}else {
System.out.println("2-3-4树节点不会存在索引为"+childNum+"的子节点");
return null;
}
}
//获取当前节点的父节点
public Node getParent(){
return parent;
}
//判断当前节点是不是叶节点
public boolean isLeaf(){
//是叶节点返回true,否则返回false
return childArray[0] == null;//如果子节点指针列表的第一个元素是空,那就说明没有子节点
}
//拿到当前节点包含的数据项个数
public int getNumItems(){
return numItems;
}
//判断当前节点是不是满节点
public boolean isFull(){
return numItems == ORDER-1;
}
//找到指定的数据项在节点中的位置
public int findItem(int key){
for(int i=0;i<numItems;i++){
if (key == itemArray[i].data){
return i;
}
}
return -1;//表示节点中没有要找的数据
}
//实现数据项插入到此节点中对应的位置,并返回插入的位置
public int insertItem(DataItem newDataItem){
if (numItems<ORDER-1){
//节点不满,可以插入新数据
numItems++;
int newData = newDataItem.data;
for (int i=ORDER-2;i>=0;i--){
if (itemArray[i] == null){
continue;
}else {
int itemArrayData = itemArray[i].data;
if (newData < itemArrayData){
//新数据项比当前的数据项小
itemArray[i+1] = itemArray[i];//把数据项往后面移动
}else {
//新数据项比当前的数据项大,那么新数据项应该插入到当前数据项的后面
itemArray[i+1] = newDataItem;//把新的数据项插入到当前数据项的后面
return i+1;
}
}
}
//如果循环完了,还没有结束,就证明数组是空的,直接插入到第0个位置
itemArray[0] = newDataItem;
return 0;
}else {
//节点已满
System.out.println("节点已满");
return 3;//表示节点已满
}
}
//实现移除当前节点中的最后一个数据项
public DataItem removeLastDataItem(){
DataItem delData = itemArray[numItems-1];
itemArray[numItems-1] =null;
numItems--;
return delData;
}
//把当前节点中的所有数据项都打印出来
public void displayNode(){
for (int i = 0; i<numItems;i++){
itemArray[i].displayItem();
}
System.out.print("/"+"\n");
}
}
封装2-3-4树的类
package tree234;
/**
* 实现234树的类
*/
public class Tree234 {
private Node root;
//构造防范。初始的一个空的234树,有一个空节点
public Tree234(){
root = new Node();
}
//实现搜索234树
//找到了指定的数据,就返回数据在节点数据项数组中的索引,没找到返回-1
public int find(int key){
Node current = root;
int childNumber;//存放返回值的变量
while (true){
if ((childNumber=current.findItem(key)) != -1){
//找到了
return childNumber;
}else if (current.isLeaf()){
//树中没有要找的数据项
return -1;
}else {
//current指针向正确的子节点移动
current = getNextNode(current,key);
}
}
}
//查找过程中,确定下一个要查找的节点
public Node getNextNode(Node theNode, int theValue){
int numItems = theNode.getNumItems();//获取当前节点含有的数据项个数
int i;
for (i=0; i<numItems;i++){
if (theValue<theNode.getItemArray()[i].data){
return theNode.getChild(i);
}
}
//循环完毕后,都没有返回,证明这个值比节点中的所有数据项都大,返回最后一个子节点
return theNode.getChild(i);
}
//实现新的数据项插入的234树
public void insert(int insValue){
//封装数据
DataItem insDataItem = new DataItem(insValue);
//找到数据值应该被插入的位置
Node current = root;
while (true){
if (current.isFull()){
//分裂
split(current);//独立出一个方法来专门实现复杂的分裂操作
current = current.getParent();//分裂结束后,回到父节点重新操作
current = getNextNode(current,insValue);//重新寻找下一个节点
}else if(current.isLeaf()){
//此时节点不是满节点,且是叶子节点
//找到了要插入的节点
break;
}else {
//不是满节点。也不是叶子节点,继续循环
current = getNextNode(current,insValue);
}
}
current.insertItem(insDataItem);//往目标节点中插入数据项
}
//实现节点分裂
public void split(Node theNode){
Node parent;//当前节点的父节点
DataItem itemB;
DataItem itemC;
Node child2;//第三个子节点,索引值2
Node child3;//第四个子节点,索引值3
//把上面的四个单位分离出来
itemC = theNode.removeLastDataItem();
itemB = theNode.removeLastDataItem();
child3 = theNode.disconnectChild(3);
child2 = theNode.disconnectChild(2);
//在右边新建一个新的节点
Node rightNewNode = new Node();
if (theNode == root){
//根节点的分裂
root = new Node();
parent = root;
root.connectChild(0,theNode);
}else {
//不是根节点分裂
parent = theNode.getParent();
}
//到这里,不管是根节点分裂还是非根节点分裂,都拿到了要分裂的节点的父节点
//将分裂节点的B数据项插入父节点中
int itemBinParentIndex = parent.insertItem(itemB);
//考虑父节点原本两个数据项的情况,要改变原本子节点对应的索引值
int n = parent.getNumItems();//B数据项插入到父节点后,父节点的新的数据项个数
Node tmp;
for (int i =n;i>itemBinParentIndex;i--){
tmp = parent.disconnectChild(i);
parent.connectChild(i+1,tmp);
}
//处理右边新的节点
//itemC先放到新节点
rightNewNode.insertItem(itemC);
//新节点连接到父节点
parent.connectChild(itemBinParentIndex+1,rightNewNode);
//右边两个子节点连接到新节点
rightNewNode.connectChild(0,child2);
rightNewNode.connectChild(1,child3);
}
//用递归打印输出234树所有节点的值
public void recursionDisplayTree(Node thisNode, int level, int childNum){
//节点,节点所在深度,当前节点是父节点的第几个子节点
System.out.println("level="+level+"child:"+childNum);
thisNode.displayNode();
int numItems = thisNode.getNumItems();
for (int i=0; i<numItems+1;i++){
Node childNode = thisNode.getChild(i);
//递归
if (childNode == null){
//边界
return;
}else {
recursionDisplayTree(childNode,level+1,i);
}
}
}
//打印输出所有的节点的值
public void displayTree(){
recursionDisplayTree(root, 0, 0);
}
}
测试2-3-4树的类
package tree234;
public class Tree234Test {
public static void main(String[] args) {
Tree234 tree234 = new Tree234();
tree234.insert(20);
tree234.insert(22);
tree234.insert(15);
tree234.insert(98);
tree234.insert(45);
tree234.insert(6);
tree234.displayTree();
System.out.println(tree234.find(45));
}
}