帶你了解Java數(shù)據(jù)結(jié)構(gòu)和算法之2-3-4樹(shù)
1、2-3-4 樹(shù)介紹
2-3-4樹(shù)每個(gè)節(jié)點(diǎn)最多有四個(gè)字節(jié)點(diǎn)和三個(gè)數(shù)據(jù)項(xiàng),名字中 2,3,4 的數(shù)字含義是指一個(gè)節(jié)點(diǎn)可能含有的子節(jié)點(diǎn)的個(gè)數(shù)。對(duì)于非葉節(jié)點(diǎn)有三種可能的情況:
①、有一個(gè)數(shù)據(jù)項(xiàng)的節(jié)點(diǎn)總是有兩個(gè)子節(jié)點(diǎn);
②、有二個(gè)數(shù)據(jù)項(xiàng)的節(jié)點(diǎn)總是有三個(gè)子節(jié)點(diǎn);
③、有三個(gè)數(shù)據(jù)項(xiàng)的節(jié)點(diǎn)總是有四個(gè)子節(jié)點(diǎn);
簡(jiǎn)而言之,非葉節(jié)點(diǎn)的子節(jié)點(diǎn)數(shù)總是比它含有的數(shù)據(jù)項(xiàng)多1。如果子節(jié)點(diǎn)個(gè)數(shù)為L(zhǎng),數(shù)據(jù)項(xiàng)個(gè)數(shù)為D,那么:L = D + 1

葉節(jié)點(diǎn)(上圖最下面的一排)是沒(méi)有子節(jié)點(diǎn)的,然而它可能含有一個(gè)、兩個(gè)或三個(gè)數(shù)據(jù)項(xiàng)??展?jié)點(diǎn)是不會(huì)存在的。
樹(shù)結(jié)構(gòu)中很重要的一點(diǎn)就是節(jié)點(diǎn)之間關(guān)鍵字值大小的關(guān)系。在二叉樹(shù)中,所有關(guān)鍵字值比某個(gè)節(jié)點(diǎn)值小的節(jié)點(diǎn)都在這個(gè)節(jié)點(diǎn)左子節(jié)點(diǎn)為根的子樹(shù)上;所有關(guān)鍵字值比某個(gè)節(jié)點(diǎn)值大的節(jié)點(diǎn)都在這個(gè)節(jié)點(diǎn)右子節(jié)點(diǎn)為根的子樹(shù)上。2-3-4 樹(shù)規(guī)則也是一樣,并且還加上以下幾點(diǎn):
為了方便描述,用從0到2的數(shù)字給數(shù)據(jù)項(xiàng)編號(hào),用0到3的數(shù)字給子節(jié)點(diǎn)編號(hào),如下圖:

①、根是child0的子樹(shù)的所有子節(jié)點(diǎn)的關(guān)鍵字值小于key0;
②、根是child1的子樹(shù)的所有子節(jié)點(diǎn)的關(guān)鍵字值大于key0并且小于key1;
③、根是child2的子樹(shù)的所有子節(jié)點(diǎn)的關(guān)鍵字值大于key1并且小于key2;
④、根是child3的子樹(shù)的所有子節(jié)點(diǎn)的關(guān)鍵字值大于key2。
簡(jiǎn)化關(guān)系如下圖,由于2-3-4樹(shù)中一般不允許出現(xiàn)重復(fù)關(guān)鍵值,所以不用考慮比較關(guān)鍵值相同的情況。

2、搜索2-3-4樹(shù)
查找特定關(guān)鍵字值的數(shù)據(jù)項(xiàng)和在二叉樹(shù)中的搜索類似。從根節(jié)點(diǎn)開(kāi)始搜索,除非查找的關(guān)鍵字值就是根,否則選擇關(guān)鍵字值所在的合適范圍,轉(zhuǎn)向那個(gè)方向,直到找到為止。
比如對(duì)于下面這幅圖,我們需要查找關(guān)鍵字值為 64 的數(shù)據(jù)項(xiàng)。

首先從根節(jié)點(diǎn)開(kāi)始,根節(jié)點(diǎn)只有一個(gè)數(shù)據(jù)項(xiàng)50,沒(méi)有找到,而且因?yàn)?4比50大,那么轉(zhuǎn)到根節(jié)點(diǎn)的子節(jié)點(diǎn)child1。60|70|80 也沒(méi)有找到,而且60<64<70,所以我們還是找該節(jié)點(diǎn)的child1,62|64|66,我們發(fā)現(xiàn)其第二個(gè)數(shù)據(jù)項(xiàng)正好是64,于是找到了。
3、插入
新的數(shù)據(jù)項(xiàng)一般要插在葉節(jié)點(diǎn)里,在樹(shù)的最底層。如果你插入到有子節(jié)點(diǎn)的節(jié)點(diǎn)里,那么子節(jié)點(diǎn)的編號(hào)就要發(fā)生變化來(lái)維持樹(shù)的結(jié)構(gòu),因?yàn)樵?-3-4樹(shù)中節(jié)點(diǎn)的子節(jié)點(diǎn)要比數(shù)據(jù)項(xiàng)多1。
插入操作有時(shí)比較簡(jiǎn)單,有時(shí)卻很復(fù)雜。
①、當(dāng)插入沒(méi)有滿數(shù)據(jù)項(xiàng)的節(jié)點(diǎn)時(shí)是很簡(jiǎn)單的,找到合適的位置,只需要把新數(shù)據(jù)項(xiàng)插入就可以了,插入可能會(huì)涉及到在一個(gè)節(jié)點(diǎn)中移動(dòng)一個(gè)或其他兩個(gè)數(shù)據(jù)項(xiàng),這樣在新的數(shù)據(jù)項(xiàng)插入后關(guān)鍵字值仍保持正確的順序。如下圖:

②、如果往下尋找插入位置的途中,節(jié)點(diǎn)已經(jīng)滿了,那么插入就變得復(fù)雜了。發(fā)生這種情況時(shí),節(jié)點(diǎn)必須分裂,分裂能保證2-3-4樹(shù)的平衡。
ps:這里討論的是自頂向下的2-3-4樹(shù),因?yàn)槭窃谙蛳抡业讲迦朦c(diǎn)的路途中節(jié)點(diǎn)發(fā)生了分裂。把要分裂的數(shù)據(jù)項(xiàng)設(shè)為A,B,C,下面是節(jié)點(diǎn)分裂的情況(假設(shè)分裂的節(jié)點(diǎn)不是根節(jié)點(diǎn)):
1、節(jié)點(diǎn)分裂
一、創(chuàng)建一個(gè)新的空節(jié)點(diǎn),它是要分裂節(jié)點(diǎn)的兄弟,在要分裂節(jié)點(diǎn)的右邊;
二、數(shù)據(jù)項(xiàng)C移到新節(jié)點(diǎn)中;
三、數(shù)據(jù)項(xiàng)B移到要分裂節(jié)點(diǎn)的父節(jié)點(diǎn)中;
四、數(shù)據(jù)項(xiàng)A保留在原來(lái)的位置;
五、最右邊的兩個(gè)子節(jié)點(diǎn)從要分裂處斷開(kāi),連到新節(jié)點(diǎn)上。

上圖描述了節(jié)點(diǎn)分裂的例子,另一種描述節(jié)點(diǎn)分裂的說(shuō)法是4-節(jié)點(diǎn)變成了兩個(gè) 2- 節(jié)點(diǎn)。節(jié)點(diǎn)分裂是把數(shù)據(jù)向上和向右移動(dòng),從而保持了數(shù)的平衡。一般插入只需要分裂一個(gè)節(jié)點(diǎn),除非插入路徑上存在不止一個(gè)滿節(jié)點(diǎn)時(shí),這種情況就需要多重分裂。
2、根的分裂
如果一開(kāi)始查找插入節(jié)點(diǎn)時(shí)就碰到滿的根節(jié)點(diǎn),那么插入過(guò)程更復(fù)雜:
①、創(chuàng)建新的根節(jié)點(diǎn),它是要分裂節(jié)點(diǎn)的父節(jié)點(diǎn)。
②、創(chuàng)建第二個(gè)新的節(jié)點(diǎn),它是要分裂節(jié)點(diǎn)的兄弟節(jié)點(diǎn);
③、數(shù)據(jù)項(xiàng)C移到新的兄弟節(jié)點(diǎn)中;
④、數(shù)據(jù)項(xiàng)B移到新的根節(jié)點(diǎn)中;
⑤、數(shù)據(jù)項(xiàng)A保留在原來(lái)的位置;
⑥、要分裂節(jié)點(diǎn)最右邊的兩個(gè)子節(jié)點(diǎn)斷開(kāi)連接,連到新的兄弟節(jié)點(diǎn)中。

上圖便是根分裂的情況,分裂完成之后,整個(gè)樹(shù)的高度加1。另外一種描述根分裂的方法是說(shuō)4-節(jié)點(diǎn)變成三個(gè)2-節(jié)點(diǎn)。
注意:插入時(shí),碰到?jīng)]有滿的節(jié)點(diǎn)時(shí),要繼續(xù)向下尋找其子節(jié)點(diǎn)進(jìn)行插入。如果直接插入該節(jié)點(diǎn),那么還要進(jìn)行子節(jié)點(diǎn)的增加,因?yàn)樵?-3-4樹(shù)中節(jié)點(diǎn)的子節(jié)點(diǎn)個(gè)數(shù)要比數(shù)據(jù)項(xiàng)多1;如果插入的節(jié)點(diǎn)滿了,那么就要進(jìn)行節(jié)點(diǎn)分裂。下圖是一系列插入過(guò)程,有4個(gè)節(jié)點(diǎn)分裂了,兩個(gè)是根,兩個(gè)是葉節(jié)點(diǎn):

4、完整源碼實(shí)現(xiàn)
分為節(jié)點(diǎn)類Node,表示每個(gè)節(jié)點(diǎn)的數(shù)據(jù)項(xiàng)類DataItem,以及最后的2-3-4樹(shù)類Tree234.class
package com.ys.tree.twothreefour;
public class Tree234 {
private Node root = new Node() ;
/*public Tree234(){
root = new Node();
}*/
//查找關(guān)鍵字值
public int find(long key){
Node curNode = root;
int childNumber ;
while(true){
if((childNumber = curNode.findItem(key))!=-1){
return childNumber;
}else if(curNode.isLeaf()){//節(jié)點(diǎn)是葉節(jié)點(diǎn)
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);
}
//插入數(shù)據(jù)項(xiàng)
public void insert(long dValue){
Node curNode = root;
DataItem tempItem = new DataItem(dValue);
while(true){
if(curNode.isFull()){//如果節(jié)點(diǎn)滿數(shù)據(jù)項(xiàng)了,則分裂節(jié)點(diǎn)
split(curNode);
curNode = curNode.getParent();
curNode = getNextChild(curNode, dValue);
}else if(curNode.isLeaf()){//當(dāng)前節(jié)點(diǎn)是葉節(jié)點(diǎn)
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){//如果當(dāng)前節(jié)點(diǎn)是根節(jié)點(diǎn),執(zhí)行根分裂
root = new Node();
parent = root;
root.connectChild(0, thisNode);
}else{
parent = thisNode.getParent();
}
//處理父節(jié)點(diǎn)
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);
//處理新建的右節(jié)點(diǎn)
newRight.insertItem(itemC);
newRight.connectChild(0, child2);
newRight.connectChild(1, child3);
}
//打印樹(shù)節(jié)點(diǎn)
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;
}
}
}
//數(shù)據(jù)項(xiàng)
class DataItem{
public long dData;
public DataItem(long dData){
this.dData = dData;
}
public void displayItem(){
System.out.println("/"+dData);
}
}
//節(jié)點(diǎn)
class Node{
private static final int ORDER = 4;
private int numItems;//表示該節(jié)點(diǎn)有多少個(gè)數(shù)據(jù)項(xiàng)
private Node parent;//父節(jié)點(diǎn)
private Node childArray[] = new Node[ORDER];//存儲(chǔ)子節(jié)點(diǎn)的數(shù)組,最多有4個(gè)子節(jié)點(diǎn)
private DataItem itemArray[] = new DataItem[ORDER-1];//存放數(shù)據(jù)項(xiàng)的數(shù)組,一個(gè)節(jié)點(diǎn)最多有三個(gè)數(shù)據(jù)項(xiàng)
//連接子節(jié)點(diǎn)
public void connectChild(int childNum,Node child){
childArray[childNum] = child;
if(child != null){
child.parent = this;
}
}
//斷開(kāi)與子節(jié)點(diǎn)的連接,并返回該子節(jié)點(diǎn)
public Node disconnectChild(int childNum){
Node tempNode = childArray[childNum];
childArray[childNum] = null;
return tempNode;
}
//得到節(jié)點(diǎn)的某個(gè)子節(jié)點(diǎn)
public Node getChild(int childNum){
return childArray[childNum];
}
//得到父節(jié)點(diǎn)
public Node getParent(){
return parent;
}
//判斷是否是葉節(jié)點(diǎn)
public boolean isLeaf(){
return (childArray[0] == null)?true:false;
}
//得到節(jié)點(diǎn)數(shù)據(jù)項(xiàng)的個(gè)數(shù)
public int getNumItems(){
return numItems;
}
//得到節(jié)點(diǎn)的某個(gè)數(shù)據(jù)項(xiàng)
public DataItem getItem(int index){
return itemArray[index];
}
//判斷節(jié)點(diǎn)的數(shù)據(jù)項(xiàng)是否滿了(最多3個(gè))
public boolean isFull(){
return (numItems == ORDER-1) ? true:false;
}
//找到數(shù)據(jù)項(xiàng)在節(jié)點(diǎn)中的位置
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;
}
//將數(shù)據(jù)項(xiàng)插入到節(jié)點(diǎn)
public int insertItem(DataItem newItem){
numItems++;
long newKey = newItem.dData;
for(int j = ORDER-2 ; j >= 0 ; j--){
if(itemArray[j] == null){//如果為空,繼續(xù)向前循環(huán)
continue;
}else{
long itsKey = itemArray[j].dData;//保存節(jié)點(diǎn)某個(gè)位置的數(shù)據(jù)項(xiàng)
if(newKey < itsKey){//如果比新插入的數(shù)據(jù)項(xiàng)大
itemArray[j+1] = itemArray[j];//將大數(shù)據(jù)項(xiàng)向后移動(dòng)一位
}else{
itemArray[j+1] = newItem;//如果比新插入的數(shù)據(jù)項(xiàng)小,則直接插入
return j+1;
}
}
}
//如果都為空,或者都比待插入的數(shù)據(jù)項(xiàng)大,則將待插入的數(shù)據(jù)項(xiàng)放在節(jié)點(diǎn)第一個(gè)位置
itemArray[0] = newItem;
return 0;
}
//移除節(jié)點(diǎn)的數(shù)據(jù)項(xiàng)
public DataItem removeItem(){
DataItem temp = itemArray[numItems-1];
itemArray[numItems-1] = null;
numItems--;
return temp;
}
//打印節(jié)點(diǎn)的所有數(shù)據(jù)項(xiàng)
public void displayNode(){
for(int j = 0 ; j < numItems ; j++){
itemArray[j].displayItem();
}
System.out.println("/");
}
}
}5、2-3-4樹(shù)和紅黑樹(shù)
2-3-4樹(shù)是多叉樹(shù),而紅黑樹(shù)是二叉樹(shù),看上去可能完全不同,但是,在某種意義上它們又是完全相同的,一個(gè)可以通過(guò)應(yīng)用一些簡(jiǎn)單的規(guī)則變成另一個(gè),而且使他們保持平衡的操作也是一樣,數(shù)學(xué)上稱他們?yōu)橥瑯?gòu)。
①、對(duì)應(yīng)規(guī)則
應(yīng)用如下三條規(guī)則可以將2-3-4樹(shù)轉(zhuǎn)化為紅黑樹(shù):
一、把2-3-4樹(shù)中的每個(gè)2-節(jié)點(diǎn)轉(zhuǎn)化為紅-黑樹(shù)的黑色節(jié)點(diǎn)。
二、把每個(gè)3-節(jié)點(diǎn)轉(zhuǎn)化為一個(gè)子節(jié)點(diǎn)和一個(gè)父節(jié)點(diǎn),子節(jié)點(diǎn)有兩個(gè)自己的子節(jié)點(diǎn):W和X或X和Y。父節(jié)點(diǎn)有另一個(gè)子節(jié)點(diǎn):Y或W。哪個(gè)節(jié)點(diǎn)變成子節(jié)點(diǎn)或父節(jié)點(diǎn)都無(wú)所謂。子節(jié)點(diǎn)涂成紅色,父節(jié)點(diǎn)涂成黑色。
三、把每個(gè)4-節(jié)點(diǎn)轉(zhuǎn)化為一個(gè)父節(jié)點(diǎn)和兩個(gè)子節(jié)點(diǎn)。第一個(gè)子節(jié)點(diǎn)有它自己的子節(jié)點(diǎn)W和X;第二個(gè)子節(jié)點(diǎn)擁有子節(jié)點(diǎn)Y和Z。和前面一樣,子節(jié)點(diǎn)涂成紅色,父節(jié)點(diǎn)涂成黑色。

下圖是一顆2-3-4樹(shù)轉(zhuǎn)化成對(duì)應(yīng)的紅-黑樹(shù)。虛線環(huán)繞的子樹(shù)是由3-節(jié)點(diǎn)和4-節(jié)點(diǎn)變成的。轉(zhuǎn)化后符合紅-黑樹(shù)的規(guī)則,根節(jié)點(diǎn)為紅色,兩個(gè)紅色節(jié)點(diǎn)不會(huì)相連,每條從根到葉節(jié)點(diǎn)的路徑上的黑節(jié)點(diǎn)個(gè)數(shù)是一樣的?! ?/p>

②、操作等價(jià)
不僅紅-黑樹(shù)的結(jié)構(gòu)與2-3-4樹(shù)對(duì)應(yīng),而且兩種樹(shù)操作也一樣。2-3-4樹(shù)用節(jié)點(diǎn)分裂保持平衡,紅-黑樹(shù)用顏色變換和旋轉(zhuǎn)保持平衡。

上圖是4-節(jié)點(diǎn)分裂。虛線環(huán)繞的部分等價(jià)于4-節(jié)點(diǎn)。顏色變換之后,40,60節(jié)點(diǎn)都為黑色的,50節(jié)點(diǎn)是紅色的。因此,節(jié)點(diǎn) 50 和它的父節(jié)點(diǎn)70 對(duì)于3-節(jié)點(diǎn),如上圖虛線所示。
6、2-3-4 樹(shù)的效率
分析2-3-4樹(shù)我們可以和紅黑樹(shù)作比較分析。紅-黑樹(shù)的層數(shù)(平衡二叉樹(shù))大約是log2(N+1),而2-3-4樹(shù)每個(gè)節(jié)點(diǎn)可以最多有4個(gè)數(shù)據(jù)項(xiàng),如果節(jié)點(diǎn)都是滿的,那么高度和log4N。因此在所有節(jié)點(diǎn)都滿的情況下,2-3-4樹(shù)的高度大致是紅-黑樹(shù)的一半。不過(guò)他們不可能都是滿的,所以2-3-4樹(shù)的高度大致在log2(N+1)和log2(N+1)/2。減少2-3-4樹(shù)的高度可以使它的查找時(shí)間比紅-黑樹(shù)的短一些。
但是另一方面,每個(gè)節(jié)點(diǎn)要查看的數(shù)據(jù)項(xiàng)就多了,這會(huì)增加查找時(shí)間。因?yàn)楣?jié)點(diǎn)中用線性搜索來(lái)查看數(shù)據(jù)項(xiàng),使得查找時(shí)間的倍數(shù)和M成正比,即每個(gè)節(jié)點(diǎn)數(shù)據(jù)項(xiàng)的平均數(shù)量??偟牟檎視r(shí)間和M*log4N成正比。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java實(shí)戰(zhàn)房屋租賃網(wǎng)的實(shí)現(xiàn)流程
讀萬(wàn)卷書(shū)不如行萬(wàn)里路,只學(xué)書(shū)上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)一個(gè)房屋租賃網(wǎng)站,大家可以在過(guò)程中查缺補(bǔ)漏,提升水平2021-11-11
SpringBoot詳細(xì)分析自動(dòng)裝配原理并實(shí)現(xiàn)starter
相對(duì)于傳統(tǒng)意義上的Spring項(xiàng)目,SpringBoot具有開(kāi)箱即用,簡(jiǎn)化配置,內(nèi)置Tomcat等等等等一系列的特點(diǎn)。在這些特點(diǎn)中,最重要的兩條就是約定優(yōu)于配置和自動(dòng)裝配2022-07-07
Linux系統(tǒng)Docker拉取JDK21鏡像超細(xì)詳解
這篇文章主要介紹了如何在Linux系統(tǒng)上使用Docker拉取JDK21鏡像,并基于該鏡像搭建開(kāi)發(fā)和測(cè)試環(huán)境,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-12-12
SpringBoot如何使用Undertow做服務(wù)器
這篇文章主要介紹了SpringBoot如何使用Undertow做服務(wù)器,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
詳解IDEA多module項(xiàng)目maven依賴的一些說(shuō)明
這篇文章主要介紹了詳解IDEA多module項(xiàng)目maven依賴的一些說(shuō)明,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10
springboot+mybatis+redis 二級(jí)緩存問(wèn)題實(shí)例詳解
Mybatis默認(rèn)沒(méi)有開(kāi)啟二級(jí)緩存,需要在全局配置(mybatis-config.xml)中開(kāi)啟二級(jí)緩存。本文講述的是使用Redis作為緩存,與springboot、mybatis進(jìn)行集成的方法。需要的朋友參考下吧2017-12-12

