Java数据结构和算法(十二)——2-3-4树

  通过前面的介绍,我们知道在二叉树中,每个节点只有一个数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树。本篇博客我们将介绍的——2-3-4树,它是一种多叉树,它的每个节点最多有四个子节点和三个数据项。

1、2-3-4 树介绍

  2-3-4树每个节点最多有四个字节点和三个数据项,名字中 2,3,4 的数字含义是指一个节点可能含有的子节点的个数。对于非叶节点有三种可能的情况:

  ①、有一个数据项的节点总是有两个子节点;

  ②、有二个数据项的节点总是有三个子节点;

  ③、有三个数据项的节点总是有四个子节点;

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

  

  叶节点(上图最下面的一排)是没有子节点的,然而它可能含有一个、两个或三个数据项。空节点是不会存在的。

  树结构中很重要的一点就是节点之间关键字值大小的关系。在二叉树中,所有关键字值比某个节点值小的节点都在这个节点左子节点为根的子树上;所有关键字值比某个节点值大的节点都在这个节点右子节点为根的子树上。2-3-4 树规则也是一样,并且还加上以下几点:

  为了方便描述,用从0到2的数字给数据项编号,用0到3的数字给子节点编号,如下图:

  

  ①、根是child0的子树的所有子节点的关键字值小于key0;

  ②、根是child1的子树的所有子节点的关键字值大于key0并且小于key1;

  ③、根是child2的子树的所有子节点的关键字值大于key1并且小于key2;

  ④、根是child3的子树的所有子节点的关键字值大于key2。

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

  

2、搜索2-3-4树

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

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

  

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

3、插入

  新的数据项一般要插在叶节点里,在树的最底层。如果你插入到有子节点的节点里,那么子节点的编号就要发生变化来维持树的结构,因为在2-3-4树中节点的子节点要比数据项多1。

  插入操作有时比较简单,有时却很复杂。

  ①、当插入没有满数据项的节点时是很简单的,找到合适的位置,只需要把新数据项插入就可以了,插入可能会涉及到在一个节点中移动一个或其他两个数据项,这样在新的数据项插入后关键字值仍保持正确的顺序。如下图:

  

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

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

  1、节点分裂

  一、创建一个新的空节点,它是要分裂节点的兄弟,在要分裂节点的右边;

  二、数据项C移到新节点中;

  三、数据项B移到要分裂节点的父节点中;

  四、数据项A保留在原来的位置;

  五、最右边的两个子节点从要分裂处断开,连到新节点上。

  

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

  2、根的分裂

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

  ①、创建新的根节点,它是要分裂节点的父节点。

  ②、创建第二个新的节点,它是要分裂节点的兄弟节点;

  ③、数据项C移到新的兄弟节点中;

  ④、数据项B移到新的根节点中;

  ⑤、数据项A保留在原来的位置;

  ⑥、要分裂节点最右边的两个子节点断开连接,连到新的兄弟节点中。

  

  上图便是根分裂的情况,分裂完成之后,整个树的高度加1。另外一种描述根分裂的方法是说4-节点变成三个2-节点。

  注意:插入时,碰到没有满的节点时,要继续向下寻找其子节点进行插入。如果直接插入该节点,那么还要进行子节点的增加,因为在2-3-4树中节点的子节点个数要比数据项多1;如果插入的节点满了,那么就要进行节点分裂。下图是一系列插入过程,有4个节点分裂了,两个是根,两个是叶节点:

  

  

4、完整源码实现

  分为节点类Node,表示每个节点的数据项类DataItem,以及最后的2-3-4树类Tree234.class

package com.ys.tree.twothreefour;

public class Tree234 {
	private Node root = new Node() ;
	/*public Tree234(){
		root = new Node();
	}*/
	//查找关键字值
	public int find(long key){
		Node curNode = root;
		int childNumber ;
		while(true){
			if((childNumber = curNode.findItem(key))!=-1){
				return childNumber;
			}else if(curNode.isLeaf()){//节点是叶节点
				return -1;
			}else{
				curNode = getNextChild(curNode,key);
			}
		}
	}

	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);
	}

	//插入数据项
	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);
			}else if(curNode.isLeaf()){//当前节点是叶节点
				break;
			}else{
				curNode = getNextChild(curNode, dValue);
			}
		}//end while
		curNode.insertItem(tempItem);
	}

	public void split(Node thisNode){
		DataItem itemB,itemC;
		Node parent,child2,child3;
		int itemIndex;
		itemC = thisNode.removeItem();
		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);
		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, newRight);

		//处理新建的右节点
		newRight.insertItem(itemC);
		newRight.connectChild(0, child2);
		newRight.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 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;
			}
		}
	}

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

	//节点
	class Node{
		private static final int ORDER = 4;
		private int numItems;//表示该节点有多少个数据项
		private Node parent;//父节点
		private Node childArray[] = new Node[ORDER];//存储子节点的数组,最多有4个子节点
		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];
		}
		//判断节点的数据项是否满了(最多3个)
		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;
			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("/");
		}
	}

}

5、2-3-4树和红黑树  

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

  ①、对应规则

  应用如下三条规则可以将2-3-4树转化为红黑树:

  一、把2-3-4树中的每个2-节点转化为红-黑树的黑色节点。

  二、把每个3-节点转化为一个子节点和一个父节点,子节点有两个自己的子节点:W和X或X和Y。父节点有另一个子节点:Y或W。哪个节点变成子节点或父节点都无所谓。子节点涂成红色,父节点涂成黑色。

  三、把每个4-节点转化为一个父节点和两个子节点。第一个子节点有它自己的子节点W和X;第二个子节点拥有子节点Y和Z。和前面一样,子节点涂成红色,父节点涂成黑色。

  

  下图是一颗2-3-4树转化成对应的红-黑树。虚线环绕的子树是由3-节点和4-节点变成的。转化后符合红-黑树的规则,根节点为红色,两个红色节点不会相连,每条从根到叶节点的路径上的黑节点个数是一样的。

  

  ②、操作等价

  不仅红-黑树的结构与2-3-4树对应,而且两种树操作也一样。2-3-4树用节点分裂保持平衡,红-黑树用颜色变换和旋转保持平衡。

  

  上图是4-节点分裂。虚线环绕的部分等价于4-节点。颜色变换之后,40,60节点都为黑色的,50节点是红色的。因此,节点 50 和它的父节点70 对于3-节点,如上图虚线所示。

6、2-3-4 树的效率

  分析2-3-4树我们可以和红黑树作比较分析。红-黑树的层数(平衡二叉树)大约是log2(N+1),而2-3-4树每个节点可以最多有4个数据项,如果节点都是满的,那么高度和log4N。因此在所有节点都满的情况下,2-3-4树的高度大致是红-黑树的一半。不过他们不可能都是满的,所以2-3-4树的高度大致在log2(N+1)和log2(N+1)/2。减少2-3-4树的高度可以使它的查找时间比红-黑树的短一些。

  但是另一方面,每个节点要查看的数据项就多了,这会增加查找时间。因为节点中用线性搜索来查看数据项,使得查找时间的倍数和M成正比,即每个节点数据项的平均数量。总的查找时间和M*log4N成正比。

原文地址:https://www.cnblogs.com/ysocean/p/8032648.html

时间: 2024-08-29 21:29:59

Java数据结构和算法(十二)——2-3-4树的相关文章

Java数据结构和算法(二)树的基本操作

Java数据结构和算法(二)树的基本操作 一.树的遍历 二叉树遍历分为:前序遍历.中序遍历.后序遍历.即父结点的访问顺序 1.1 前序遍历 基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即根-左-右.图中前序遍历结果是:1,2,4,5,7,8,3,6. // 递归实现前序遍历 public void preOrder() { System.out.printf("%s ", value); if (left != null) { left.preOrder1(); }

Java数据结构和算法(二)——数组

上篇博客我们简单介绍了数据结构和算法的概念,对此模糊很正常,后面会慢慢通过具体的实例来介绍.本篇博客我们介绍数据结构的鼻祖——数组,可以说数组几乎能表示一切的数据结构,在每一门编程语言中,数组都是重要的数据结构,当然每种语言对数组的实现和处理也不相同,但是本质是都是用来存放数据的的结构,这里我们以Java语言为例,来详细介绍Java语言中数组的用法. 1.Java数组介绍 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型. ①.数组的声明 第一种方式: 数据类型 []

Java数据结构和算法(八)--红黑树与2-3树

红黑树规则: 1.根节点与叶节点都是黑色节点 2.每个红色节点的两个子节点都是黑色节点,反之,不做要求,换句话说就是不能有连续两个红色节点 3.从根节点到所有叶子节点上的黑色节点数量是相同的 一般对红黑树的讲述都是先给出这样的定义,这样想对不太容易理解的,而在算法4一书中,直接跳过这些规则,而讲述了红黑树与2-3树的等价性 如果我们先了解2-3树,理解了红黑树与2-3树之间的关系,回过头就会发现红黑树不难 2-3树: 2-3树满足二分搜索树的基本性质,但是不是二叉树 2-3树节点可以存放一个元素

Java数据结构和算法(二)——数组

数组的用处是什么呢?--当你需要将30个数进行大小排列的时候,用数组这样的数据结构存储是个很好的选择,当你是一个班的班主任的时候,每次要记录那些学生的缺勤次数的时候,数组也是很有用.数组可以进行插入,删除,查找等. 1)创建和内存分配 Java中有两种数据类型,基本类型和对象类型,也有人称为引用类型,Java中把数组当成对象,创建数组时使用new操作符. int array[] = new int[10]; 既然是对象,那么array便是数组的一个引用,根据Java编程思想(一) -- 一切都是

Java数据结构与算法之集合

线性表.链表.哈希表是常用的数据结构,在进行Java开发时,SDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中. 一.Collection接口 Collection是最基本的集合接口,一个Collection代表一组Object.一些Collection允许相同元素而另一些不行.一些能排序而另一些不行.Java  SDK不提供直接继承自Collection的类,Java  SDK提供的类都是继承自Collection的"子接口"如List和Set

java数据结构和算法06(红黑树)

这一篇我们来看看红黑树,首先说一下我啃红黑树的一点想法,刚开始的时候比较蒙,what?这到底是什么鬼啊?还有这种操作?有好久的时间我都缓不过来,直到我玩了两把王者之后回头一看,好像有点儿意思,所以有的时候碰到一个问题困扰了很久可以先让自己的头脑放松一下,哈哈! 不瞎扯咳,开始今天的正题: 前提:看红黑树之前一定要先会搜索二叉树 1.红黑树的概念 红黑树到底是个什么鬼呢?我最开始也在想这个问题,你说前面的搜索二叉树多牛,各种操作效率也不错,用起来很爽啊,为什么突然又冒出来了红黑树啊? 确实,搜索二

Java数据结构和算法之栈与队列

二.栈与队列 1.栈的定义 栈(Stack)是限制仅在表的一端进行插入和删除运算的线性表. (1)通常称插入.删除的这一端为栈顶(Top),另一端称为栈底(Bottom). (2)当表中没有元素时称为空栈. (3)栈为后进先出(Last In First Out)的线性表,简称为LIFO表. 栈的修改是按后进先出的原则进行. 每次删除(退栈)的总是当前栈中"最新"的元素,即最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除. 图1 [示例]元素是以a1,a2,-,a

Java数据结构和算法之数组与简单排序

一.数组于简单排序 数组 数组(array)是相同类型变量的集合,可以使用共同的名字引用它.数组可被定义为任何类型,可以是一维或多维.数组中的一个特别要素是通过下标来访问它.数组提供了一种将有联系的信息分组的便利方法. 一维数组 一维数组(one‐dimensional array )实质上是相同类型变量列表.要创建一个数组,你必须首先定义数组变量所需的类型.通用的一维数组的声明格式是: type var‐name[ ]; 获得一个数组需要2步: 第一步,你必须定义变量所需的类型. 第二步,你必

Java数据结构和算法之链表

三.链表 链结点 在链表中,每个数据项都被包含在'点"中,一个点是某个类的对象,这个类可认叫做LINK.因为一个链表中有许多类似的链结点,所以有必要用一个不同于链表的类来表达链结点.每个LINK对象中都包含一个对下一个点引用的字段(通常叫做next)但是本身的对象中有一个字段指向对第一个链结点的引用. 单链表 用一组地址任意的存储单元存放线性表中的数据元素. 以元素(数据元素的映象)  + 指针(指示后继元素存储位置)  = 结点(表示数据元素 或 数据元素的映象) 以"结点的序列&q

java数据结构与算法之平衡二叉树(AVL树)的设计与实现

[版权申明]未经博主同意,不允许转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/53892797 出自[zejian的博客] 关联文章: java数据结构与算法之顺序表与链表设计与实现分析 java数据结构与算法之双链表设计与实现 java数据结构与算法之改良顺序表与双链表类似ArrayList和LinkedList(带Iterator迭代器与fast-fail机制) java数据结构与算法之栈(Stack)设