java数据算法节点删除_数据结构与算法->树->2-3-4树的查找,添加,删除(Java)

代码: 兵马未动,粮草先行

作者: 传说中的汽水枪

如有错误,请留言指正,欢迎一起探讨.

转载请注明出处.

目录

一. 2-3-4树的定义

二. 2-3-4树数据结构定义

三. 2-3-4树的可以得到几个推论

四. 2-3-4树节点keys/subNodes相关方法定义与解释

五. 2-3-4树节点性质属性相关方法定义

六. 2-3-4树查找逻辑解释和代码实现

七. 2-3-4树插入逻辑解释和代码实现

八. 2-3-4树删除逻辑解释和代码实现

九. 2-3-4树辅助功能和测试

摘要

网上有一大堆介绍了2-3-4树这种数据结构,但是没有给出代码实现,本系列文章是实现2-3-4树的查找/插入/删除操作!

一. 2-3-4树的定义

2-3-4树是一种阶为4的B树。它是一种自平衡的数据结构,可以保证在O(lgn)的时间内完成查找、插入和删除操作。它主要满足以下性质:

(1)每个节点每个节点有1、2或3个key,分别称为2(孩子)节点,3(孩子)节点,4(孩子)节点。

(2)所有叶子节点到根节点的长度一致(也就是说叶子节点都在同一层)。

(3)每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key。

(摘自:http://blog.csdn.net/xiaokang123456kao/article/details/54379868)

此系列文章,不考虑key相同的情况.

二. 2-3-4树数据结构定义

定义了3个类, 分别是: TwoThreeFourTree, Node, DataItem

Node和DataItem是TwoThreeFourTree的类中类

图一: 3个分别指的相关数据

1d262be97436cd6f3691168138ac9844.png

Java代码一

/**

*

* @Description : 2-3-4树

* @author : Rush.D.Xzj

* @CreateDate : 2017-07-25 18:06:09

*

*/

public classTwoThreeFourTree {

/**根节点*/

privateNode root;

/**TwoThreeFourTree.LOG*/

private static final Log LOG = LogFactory.getLog(TwoThreeFourTree.class);

/**

*

* @Description : 2-3-4树的节点

* @author : Rush.D.Xzj

* @CreateDate : 2017-07-05 18:08:30

*

*/

public classNode {

public static final int MAX_CHILD_COUNT = 4;

/**最大的字节点个数*/

private intmaxChildCount;

/**父节点*/

privateNode parent;

/**count = 0, 2, 3, 4*/

private ListsubNodes;

/**count = 1, 2, 3, 此数据的数目不可能为空!*/

/**在非叶子节点的情况下, keys.size() + 1 = subNodes.size()*/

private Listkeys;

publicNode(DataItem dataItem) {

this.subNodes = new ArrayList();

this.keys = new ArrayList();

this.parent = null;

this.keys.add(dataItem);

this.maxChildCount =MAX_CHILD_COUNT;

}

public Node(intkey) {

this.subNodes = new ArrayList();

this.keys = new ArrayList();

this.parent = null;

this.keys.add(newDataItem(key));

this.maxChildCount =MAX_CHILD_COUNT;

}

}

/**

*

* @Description : 数据项封装

* @author : Rush.D.Xzj

* @CreateDate : 2017-07-05 18:25:11

*

*/

public classDataItem {

public intkey;

public DataItem(intkey) {

this.key =key;

}

}

}

三. 2-3-4树的可以得到几个推论

根据代码一的相关注释我们可以得出以下的推论

推论1: keys.size() >= 1

推论2: 在非叶子节点的情况下, keys.size() + 1 == subNodes.size()

推论3: 因为是搜索树,所以某个key(keyA)下面的左边所有树的节点中的所有key都小于keyA,同理:keyA下面的右边所有树的节点中的所有key都大于keyA

图二: 示例树

67de43bbcdf77b9cc055008735644816.png

以图二来中的树来举例推论3

(55,60,71)节点不可能是(49,60,71)

因为49小于(23,50)节点中的50了.

四. 2-3-4树节点keys/subNodes相关方法定义与解释

在Node类中定义如下的方法:

public Node getSubNode(intindex) {

return this.subNodes.get(index);

}

public intsubNodeSize() {

return this.subNodes.size();

}

public void addSubNode(intindex, Node node) {

if (node != null) {

this.subNodes.add(index, node);

node.parent = this;

}

}

public void removeSubNode(intindex) {

this.subNodes.remove(index);

}

public voidremoveSubNode(Node node) {

this.subNodes.remove(node);

}

public intindexOfNode(Node node) {

return this.subNodes.indexOf(node);

}

public voidclearSubNodes() {

this.subNodes.clear();

}

publicNode getFirstSubNode() {

if (this.subNodes.size() == 0) {

return null;

}

return this.subNodes.get(0);

}

publicNode getLastSubNode() {

if (this.subNodes.size() == 0) {

return null;

}

return this.subNodes.get(this.subNodes.size() - 1);

}

public voidremoveFirstSubNode() {

if (this.subNodes.size() != 0) {

this.subNodes.remove(0);

}

}

public voidremoveLastSubNode() {

if (this.subNodes.size() != 0) {

this.subNodes.remove(this.subNodes.size() - 1);

}

}

public voidaddToFirstSubNode(Node subNode) {

if (subNode != null) {

this.subNodes.add(0, subNode);

subNode.parent = this;

}

}

public voidaddToLastSubNode(Node subNode) {

if (subNode != null) {

this.subNodes.add(subNode);

subNode.parent = this;

}

}

public DataItem getKey(intindex) {

return this.keys.get(index);

}

public intkeySize() {

return this.keys.size();

}

public void addKey(intindex, DataItem dataItem) {

if (dataItem != null) {

this.keys.add(index, dataItem);

}

}

public void removeKey(intindex) {

this.keys.remove(index);

}

public voidremoveKey(DataItem dataItem) {

DataItem removeItem = null;

for (int i = 0; i < this.keySize(); i++) {

DataItem tmp = this.keys.get(i);

if (tmp.key ==dataItem.key) {

removeItem =tmp;

break;

}

}

this.keys.remove(removeItem);

}

public intindexOfKey(DataItem dataItem) {

return this.keys.indexOf(dataItem);

}

public voidclearKeys() {

this.keys.clear();

}

publicDataItem getFirstKey() {

if (this.keys.size() == 0) {

return null;

}

return this.keys.get(0);

}

publicDataItem getLastKey() {

if (this.keys.size() == 0) {

return null;

}

return this.keys.get(this.keys.size() - 1);

}

public voidremoveFirstKey() {

if (this.keys.size() != 0) {

this.keys.remove(0);

}

}

public voidremoveLastKey() {

if (this.keys.size() != 0) {

this.keys.remove(this.keys.size() - 1);

}

}

public voidaddToFirstKey(DataItem dataItem) {

if (dataItem != null) {

this.keys.add(0, dataItem);

}

}

public voidaddToLastKey(DataItem dataItem) {

if (dataItem != null) {

this.keys.add(dataItem);

}

}

认真看完代码发现上述N个方法就是简单的对Java List(keys, subNodes)的相关函数二次包装(public void removeKey(DatItem dataItem) 这个函数除外).

有的读者可能会有疑惑为什么要定义这样的函数?

此问题可以留在最后再给出答案.

五. 2-3-4树节点性质属性相关方法定义

Node类中的相关方法

/**

*

* @Description : 是否是满节点,keys数目必须是this.maxChildCount-1才是满节点

* @return : boolean

*

*/

public booleanisFull() {

return this.keySize() == this.maxChildCount - 1;

}

/**

*

* @Description : 是否是叶子节点,子节点数目必须是0

* @return : boolean

*

*/

public booleanisLeaf() {

return this.subNodeSize() == 0;

}

/**

*

* @Description : 2节点(包含子节点或不包含)

* @return : boolean

*

*/

public booleanisTwoKey() {

return this.keySize() == 1;

}

/**

*

* @Description : 3节点(包含子节点或不包含)

* @return : boolean

*

*/

public booleanisThreeKey() {

return this.keySize() == 2;

}

/**

*

* @Description : 4节点(包含子节点或不包含)

* @return : boolean

*

*/

public booleanisFourKey() {

return this.keySize() == 3;

}

/**

*

* @Description : 2节点

* @return : boolean

*

*/

public booleanisTwoNode() {

return this.subNodeSize() == 2;

}

/**

*

* @Description : 3节点

* @return : boolean

*

*/

public booleanisThreeNode() {

return this.subNodeSize() == 3;

}

/**

*

* @Description : 4节点

* @return : boolean

*

*/

public booleanisFourNode() {

return this.subNodeSize() == 4;

}

因为推论2,实际上上面的相关方法实际上还有另外一种判断,例如:

public booleanisFourNode() {

return this.keySize() == 3;

}

六. 2-3-4树查找逻辑解释和代码实现

查找逻辑算法:

1. 从根节点设置为当前节点

2. 如果当前节点找到了,就返回

3. 如果当前没有找到, 获取离的最近的一个

4. 重复步骤2,3

以图二的树来说明步骤3

假设找17:

1.先在节点(23,50)找

2.没有找到, 得到一个最近的一个index为0(因为17<23) ,这时候直接跳转到节点(9,16,19)

3.在节点(9,16,19)找,没有找到,得到一个最近的一个index为2(因为17<19),这个时候直接跳转到节点(17)

4.在节点(17)找到17 查找结束

再来一次假设:

假设找76:

1.先在节点(23,50)找

2.没有找到, 得到一个最近的一个index为2(因为76>50, 所以是该节点最后一个子节点找) ,这时候直接跳转到节点(76)

3.在节点(76)找到76 查找结束

上述举的2个例子有一个关键是找到那个index,直接上代码

在Node类中添加如下代码:

/**是否找到*/

public static final String VALUE_FIND = "VALUE_FIND";

/**相关的keyIndex*/

public static final String VALUE_INDEX = "VALUE_INDEX";

/**

*

* @Description : 查找位置和相关的index

* @param dataItem : 需要查找的数据

* @return : java.util.Map

*

*/

publicMap keyIndex(DataItem dataItem) {

Map result = newHashMap();

// 默认的设置未找到

result.put(VALUE_FIND, false);

int index = 0;

for (; index < this.keySize(); index ++) {

DataItem tmpItem = this.getKey(index);

if (dataItem.key ==tmpItem.key) {

// 如果等于就跳出循环,并且设置为true

result.put(VALUE_FIND, true);

break;

} else if (dataItem.key

// 如果小于了就直接跳出循环

break;

}

}

// 也有可能需要查找的数据比此节点所有的数据都大

// 因此循环结束后都没有执行break

// 此时的index就是keySize()

result.put(VALUE_INDEX, index);

returnresult;

}

注意注释中这样的一段话:

此时的index就是keySize()

真好可以对应:假设找70的例子,是印证了推论2.

在TwoThreeFourTree中添加查找函数:

public DataItem find(intkey) {

if (this.root == null) {

return null;

} else{

DataItem dataItem = newDataItem(key);

DataItem findItem = this.find(this.root, dataItem);

returnfindItem;

}

}

privateDataItem find(Node node, DataItem dataItem) {

if (node == null) {

return null;

}

// 请注意这个函数的注释

Map keyMap =node.keyIndex(dataItem);

boolean find =(Boolean)(keyMap.get(Node.VALUE_FIND));

int keyIndex =(Integer)(keyMap.get(Node.VALUE_INDEX));

if(find) {

returnnode.getKey(keyIndex);

}

// 如果上述没有找到,那么如果代码运行到这里,

// keyIndex = keySize

// 那么subNodes.get(keyIndex),

// 就是subNodes的最后一个元素(因为2-3-4树,subNodes的数目不是0就是keySize + 1)

// 如果keyIndex

// 如果没有字节点,就表示没有找到

if (node.subNodeSize() == 0) {

return null;

}

Node subNode =node.getSubNode(keyIndex);

// 重复上述这个过程

return this.find(subNode, dataItem);

}

七. 2-3-4树插入逻辑解释和代码实现

本博文用的插入算法是: 带有预分裂的插入算法.

算法的基本思路是:

1. 如果找到已经存在此值,那么就直接退出

2. 如果没有找到,从根节点开始遍历

3.如果是满节点,那么先分裂此节点(7.1)

4.如果是叶子节点,那么就在合适的位置插入(7.4)

5.否则,继续寻找合适的位置(7.3)

7.1 满节点的分裂

分裂示意图:

01f94c198e24756912405601902a9da6.png

具体代码实现如下(Node类中):

public voidconnectNode(Node childNode) {

if (childNode != null) {

this.addToLastSubNode(childNode);

}

}

public void connectNodes(Listnodes) {

for(Node node : nodes) {

this.connectNode(node);

}

}

/**

*

* @Description : 必须是满节点才能分裂

* 节点分裂的时候,会分裂成3个新的节点,此节点相当于没有用了

* @return : com.rush.structure.TwoThreeFourTree.Node

*

*/

publicNode split() {

DataItem leftItem = this.getKey(0);

DataItem midItem = this.getKey(1);

DataItem rightItem = this.getKey(2);

Node resultNode = newNode(midItem);

Node resultFirstChildNode = newNode(leftItem);

Node resultSecondChildNode = newNode(rightItem);

// 原来的subNode,一人一半

for (int i = 0; i < this.subNodeSize(); i++) {

Node subNode = this.getSubNode(i);

if (i < 2) {

resultFirstChildNode.connectNode(subNode);

} else{

resultSecondChildNode.connectNode(subNode);

}

}

resultNode.connectNode(resultFirstChildNode);

resultNode.connectNode(resultSecondChildNode);

returnresultNode;

}

7.2 分裂后的节点处理

当约到7.1的情况后,分裂实际上是多了一层.所以要处理这个多了这一层分两种情况.

根据2-3-4树的相关性质和预分裂的处理,可以得到:

节点A: 分裂的节点的父节点, 为空或者一定不是满节点

节点B: 分裂的节点(示例图中的:9,16,19节点),一定是满节点

节点C: 节点B分裂后得到的节点(示例图中的16节点),一定是2节点,且此节点的2个子节点也一定是2节点

如果节点B是根节点那么就不处理,表示此2-3-4树经过插入操作后,增加了一层,此时的根节点就是节点C.

如果节点B不是根节点,那么节点A需要吸收节点C,使新添加的层消失掉,这样才满足2-3-4树性质.

以下代码就是节点吸收函数(Node类中).

public voidabsorbNode(Node oldNode, Node newNode) {

int index = this.indexOfNode(oldNode);

this.removeSubNode(oldNode);

int keyIndex =index;

int subNodeStartIndex =index;

this.addKey(keyIndex, newNode.getFirstKey());

for (int i = 0; i < newNode.subNodeSize() ; i++) {

Node tmpNode =newNode.getSubNode(i);

// 把newNode下的节点添加到this的节点下

this.addSubNode(i +subNodeStartIndex, tmpNode);

// 更新父节点

tmpNode.parent = this;

}

}

7.3 继续寻找合适的位置

代码如下(Node类中):

publicNode getSuitableNode(DataItem dataItem) {

int index = 0;

for (; index < this.keySize(); index ++) {

DataItem tmpDataItem = this.getKey(index);

if (dataItem.key

break;

}

}

return this.getSubNode(index);

}

78a8ea94173262fcbd6cccf3ad59528c.png

例如

如果dataItem是3, 那么就返回子节点1

如果dataItem是11, 那么就返回子节点2

如果dataItem是17, 那么就返回子节点3

如果dataItem是20, 那么就返回子节点4

7.4 是叶子节点,那么就在合适的位置插入(Node类中)

public voidinsert(DataItem dataItem) {

int index = 0;

for (; index < this.keySize(); index++) {

DataItem tmpItem = this.getKey(index);

if (tmpItem.key >dataItem.key) {

break;

}

}

// 此处逻辑跟find类似

// list.add(list.size(), key) 等价于 list.add(key)

this.addKey(index, dataItem);

}

最后是2-3-4的插入实现(TwoThreeFourTree类中):

public boolean insert(intkey) {

if (this.find(key) != null) {

return false;

}

this.root = this.insert(this.root, key);

return true;

}

private Node insert(Node node, intkey) {

DataItem dataItem = newDataItem(key);

if (node == null) {

Node newNode = newNode(dataItem);

returnnewNode;

}

Node resultNode =node;

Node curNode =node;

while (true) {

if(curNode.isFull()) {

Node curNodeParent =curNode.parent;

Node splitNode =curNode.split();

// 当前节点是根节点

if (curNodeParent == null) {

resultNode =splitNode;

} else{

curNodeParent.absorbNode(curNode, splitNode);

}

curNode =splitNode;

} else if(curNode.isLeaf()) {

// 跳出循环

break;

} else{

curNode =curNode.getSuitableNode(dataItem);

}

}

curNode.insert(dataItem);

returnresultNode;

}

八. 2-3-4树删除逻辑解释和代码实现

最简单的删除元素是直接删除非2节点的叶子节点上的元素,所以在删除元素的时候,我们需要把待删除的元素给想办法替换到叶子节点上.

称之为预合并的删除方法.

为了实现删除功能,我们需要定义实现如下的功能

8.1 树的高度

跟其他的树的高度概念一样

直接给出实现(TwoThreeFourTree)

public intheight() {

return this.height(this.root);

}

private intheight(Node node) {

if (node == null) {

return 0;

}

if (node.subNodeSize() == 0) {

return 1;

}

return 1 + this.height(node.getFirstSubNode());

}

8.2 某个元素在2-3-4树中的路径

我定义了一个路径的概念,先给出结果(基于示例图二),然后再解释一下这个路径

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:95) - key:20,path:[0, 0, 3]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:99) - key:42,path:[0, 1, 1]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:105) - key:45,path:[0, 1, 1]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:111) - key:23,path:[0, 0, 0]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:117) - key:50,path:[0, 1, 1]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:123) - key:16,path:[0, 0, 1]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:128) - key:19,path:[0, 0, 2]

2017-09-18 13:32:07 INFO testPath(TwoThreeFourTreeTest.java:133) - key:31,path:[0, 1, 0]

8.2.1 路径的长度等于高度

8.2.2 路径第一个元素都是0

8.2.3 路径下标为i(i > 0)的值表示i - 1的那个节点的第i个元素.(下标值是从0开始的!!!!!)

举例来说, 对于42 这个key值的路径是  [0,1,1]

路径数组下标为0,值为0表示是根节点(23,50)

路径数组下标为1,值为1表示是(31)节点, 也就是(23,50)节点的下标值为1的子节点

路径数组下标为2,值为1表示是(33,42,45)节点,也就是(31)节点的下标值为1的子节点

上面的这个逻辑好绕,慢慢的看

8.2.4 如果key值不在叶子节点中,那么后面的都是0

举例来说, 对于42 这个key值的路径是  [0,1,0]

路径数组下标为0,值为0表示是根节点(23,50)

路径数组下标为1,值为1表示是(31)节点, 也就是(23,50)节点的下标值为1的子节点

路径数组下标为2,值为0表示是(25,27)节点,也就是(31)节点的下标值为1的子节点

以下是路径函数(TwoThreeFourTree):

public List path(intkey) {

DataItem dataItem = newDataItem(key);

List keyIndexes = new ArrayList();

int height = this.height();

int nodeIndex = 0;

Node curNode = this.root;

// 第一层 肯定是根节点并且是第0个元素

keyIndexes.add(nodeIndex);

boolean find = false;

// 当找到的时候,它的子节点对应的keyIndex

int findNextIndex = 0;

// 可以把结束条件变成 i < height + 1

// 但是为了输出在最后叶子节点找到的信息,所以变成了 i < height

for (int i = 0; i < height; i++) {

// 已经提前找到就是默认此key值的左边的那个子元素

// 不是最后一层

if (find && (i != height - 1)) {

keyIndexes.add(findNextIndex);

// 这个时候就需要置为0

findNextIndex = 0;

continue;

}

int keyIndex = 0;

// 遍历当前节点的所有key

for (; keyIndex < curNode.keySize(); keyIndex++) {

DataItem tmpItem =curNode.getKey(keyIndex);

if (dataItem.key

break;

} else if (dataItem.key ==tmpItem.key) {

find = true;

findNextIndex =keyIndex;

break;

} else{

}

}

// 不是最后一层

if (i != height - 1) {

keyIndexes.add(keyIndex);

// 获取下一个字节点

curNode =curNode.getSubNode(keyIndex);

}

}

returnkeyIndexes;

}

所以说预合并的删除就是需要把路径中的2节点全部转换成3,4节点.

8.3 节点的转换

如何把路径中的2节点全部转换成3,4节点?

转换的方式主要有两种,

8.3.1 从节点的兄弟节点(此兄弟节点是2节点)给合并成4节点(还有一个是从父节点偷的),

8.3.2 从兄弟节点(此兄弟节点是3节点或者4节点)偷节点变成3节点.这一块还要涉及到跟父节点的变化,具体可看效果图

8.3.1 的效果图(对(31)节点的转换,合并(76)节点)

6cb12f51a5c1b0d82882419d5ebe9e54.png

8.3.2 的效果图1(对(5)节点的转换,从右边偷,偷(10,15)节点)

e5f7f96ec2993dbaa127c863ac80613e.png

8.3.2 的效果图2(对(17)节点的转换,从左边边偷,偷(10,15)节点)

ad067b9d03c6d0ecd32203cbe79ce9a5.png

转换函数(Node):

public voidtransfer() {

if (this.parent == null || !this.isTwoKey()) {

return;

}

int parentSubNodeSize = this.parent.subNodeSize();

int subNodeIndex = this.parent.indexOfNode(this);

Node leftNode = null;

Node rightNode = null;

// 转换方式

// 1. 此节点和另一个兄弟节点都是2节点,把右边的节点给合并过来

// 2. 此节点和另一个兄弟节点都是2节点,把左边的节点给合并过来

// 3. 从右边兄弟节点偷一个值

// 4. 从左边兄弟节点偷一个值

int transferType = 1;

int parentKeyIndex = 0;

// 表示此节点在父节点的最左边

if (subNodeIndex == 0) {

parentKeyIndex = 0;

leftNode = this;

rightNode = this.parent.getSubNode(1);

if(rightNode.isTwoKey()) {

transferType = 1;

} else{

transferType = 3;

}

} else if (subNodeIndex == parentSubNodeSize - 1) {

parentKeyIndex = this.parent.keySize() - 1;

leftNode = this.parent.getSubNode(subNodeIndex - 1);

rightNode = this;

if(leftNode.isTwoKey()) {

transferType = 2;

} else{

transferType = 4;

}

} else { // 肯定有左右2个兄弟节点

Node thisLeftBrotherNode = this.parent.getSubNode(subNodeIndex - 1);

Node thisRightBrotherNode = this.parent.getSubNode(subNodeIndex + 1);

// 优先找兄弟节点合并,如果都不满足从左兄弟节点偷

if(thisLeftBrotherNode.isTwoKey()) {

leftNode =thisLeftBrotherNode;

rightNode = this;

transferType = 1;

parentKeyIndex = subNodeIndex - 1;

} else if(thisRightBrotherNode.isTwoKey()) {

leftNode = this;

rightNode =thisRightBrotherNode;

transferType = 2;

parentKeyIndex =subNodeIndex;

} else{

leftNode =thisLeftBrotherNode;

rightNode = this;

transferType = 4;

parentKeyIndex = subNodeIndex - 1;

}

}

if (transferType == 1 || transferType == 2) {

// 父节点是2节点, 降层, 这种情况下只会出现在: 根节点是2节点的时候

if (this.parent.isTwoKey()) {

LOG.info("transfer :" + transferType + ", parent is two node");

this.parent.addToFirstKey(leftNode.getFirstKey());

this.parent.addToLastKey(rightNode.getFirstKey());

this.parent.clearSubNodes();

// 如果此节点有子节点,那么他的兄弟节点肯定有子节点,2-3-4的性质决定的

if (leftNode.subNodeSize() > 0) {

this.parent.connectNodes(leftNode.subNodes);

this.parent.connectNodes(rightNode.subNodes);

}

} else{

LOG.info("transfer :" + transferType + ", parent is not two node");

// 不降层, 把父节点的一个key给拉下来,例如把父节点从4节点变3节点或者父节点从3节点变2节点

// 把此节点变成一个4节点(自己,兄弟,父亲的一个key),所以是4节点

// 1. 获取父节点需要被拉下来的那个key

DataItem parentDataItem = this.parent.getKey(parentKeyIndex);

// 删除这个父节点的key

this.parent.removeKey(parentKeyIndex);

// 2. 删除父节点被拉下来的key对应的subNodes, 需要删除2次,

// 也就是leftNode, rightNode

// 这里需要连续调用2次,list的一个属性

this.parent.removeSubNode(parentKeyIndex);

this.parent.removeSubNode(parentKeyIndex);

// 或者用如下的这段代码去删除这2个subNode

// this.parent.removeSubNode(leftNode);

// this.parent.removeSubNode(rightNode);

// 3. 产生一个新的节点

Node newNode = newNode(parentDataItem);

// newNode key 左边插入一个

newNode.addToFirstKey(leftNode.getLastKey());

// newNode key 右边插入一个

newNode.addToLastKey(rightNode.getFirstKey());

// 把原来的左右节点的 subNodes 给添加到新节点上去

newNode.connectNodes(leftNode.subNodes);

newNode.connectNodes(rightNode.subNodes);

// 4. 把新产生的节点跟父节点关联起来

this.parent.addSubNode(parentKeyIndex, newNode);

}

} else if (transferType == 3) {

LOG.info("transfer 3");

// 从右边兄弟节点偷一个值, 从右边的兄弟节点偷

// 1. 先把右节点需要被偷的元素给取出来, 并删除

DataItem rightDataItem =rightNode.getFirstKey();

rightNode.removeFirstKey();

// 2. 取parent的key值,并删除

DataItem parentDataItem = this.parent.getKey(parentKeyIndex);

this.parent.removeKey(parentKeyIndex);

// 3. parent的key重新赋值

this.parent.addKey(parentKeyIndex, rightDataItem);

// 4. 左边的那个2节点变成3节点

leftNode.addToLastKey(parentDataItem);

// 如果右边节点有字节点(左边必有子节点),把第一个子节点给删除掉,并添加到左节点的最后一个子节点

if (rightNode.subNodeSize() > 0) {

Node rightSubNode =rightNode.getFirstSubNode();

rightNode.removeFirstSubNode();

leftNode.addToLastSubNode(rightSubNode);

}

} else{

LOG.info("transfer 4");

// 从左边兄弟节点偷一个值, 从左边的兄弟节点偷

// 1. 先把右节点需要被偷的元素给取出来, 并删除

DataItem leftDataItem =leftNode.getLastKey();

leftNode.removeLastKey();

// 2. 取parent的key值,并删除

DataItem parentDataItem = this.parent.getKey(parentKeyIndex);

this.parent.removeKey(parentKeyIndex);

// 3. parent的key重新赋值

this.parent.addKey(parentKeyIndex, leftDataItem);

// 4. 右边的那个2节点变成3节点

rightNode.addToFirstKey(parentDataItem);

// 如果左边节点有子节点,把最后一个子节点给删除掉,并添加到左节点的第一个子节点

if (leftNode.subNodeSize() > 0) {

Node leftSubNode =leftNode.getLastSubNode();

leftNode.removeLastSubNode();

rightNode.addToFirstSubNode(leftSubNode);

}

}

}

转换函数(TwoThreeFourTree):

public void transfer(intkey) {

List path = this.path(key);

Node curNode = this.root;

for (int i = 0; i < path.size(); i++) {

boolean haveSub = (i != (path.size() - 1));

Node nextCurNode = null;

if(haveSub) {

int nextCodeIndex = path.get(i + 1);

nextCurNode =curNode.getSubNode(nextCodeIndex);

}

if (curNode.parent != null) {

curNode.transfer();

}

if(haveSub) {

curNode =nextCurNode;

}

}

}

最后删除函数如下(TwoThreeFourTree):

public boolean delete(intkey) {

if (this.find(key) == null) {

return false;

}

int originHeight = this.height();

// 唯一的一个

if (originHeight == 1) {

if (this.root.keySize() == 1) {

this.root = null;

} else{

this.root.removeKey(newDataItem(key));

}

return true;

}

List path = this.path(key);

this.transfer(key);

int newHeight = this.height();

// 是否因为转换而导致降层了

boolean haveReduceHeight = originHeight >newHeight;

int pathStartIndex = haveReduceHeight ? 1 : 0;

Node curNode = this.root;

for (int i = pathStartIndex; i < path.size(); i++) {

boolean haveSub = (i != (path.size() - 1));

// haveSub = !curNode.isLeaf();

Node nextCurNode = null;

int nextCodeIndex = 0;

if(haveSub) {

nextCodeIndex = path.get(i + 1);

nextCurNode =curNode.getSubNode(nextCodeIndex);

}

for (int j = 0; j < curNode.keySize(); j++) {

DataItem keyItem =curNode.getKey(j);

if (keyItem.key ==key) {

if(curNode.isLeaf()) {

curNode.removeKey(j);

return true;

} else{

// 替换到下一个

DataItem subDataItem =nextCurNode.getLastKey();

// 此节点和子节点调换

curNode.removeKey(j);

curNode.addKey(j, subDataItem);

nextCurNode.removeLastKey();

nextCurNode.addKey(nextCodeIndex, keyItem);

// 跳出j的循环,继续i的循环

break;

}

}

}

if(haveSub) {

curNode =nextCurNode;

}

}

return true;

}

九. 2-3-4树辅助功能和测试

为了便于印证上述逻辑是否有问题.我们需要定义如下的一些函数:

输出一个节点的信息, 在Node类中

/**

*

* @Description : 输出节点信息

* @return : java.lang.String

**/@OverridepublicString toString() {

StringBuilder sb= newStringBuilder();

sb.append("cur key:");

sb.append(this.simpleString());if (this.parent != null) {

sb.append("parent:" + this.parent.simpleString());

}if (this.subNodeSize() > 0) {

sb.append("sub:");for (Node tmpNode : this.subNodes) {

sb.append(tmpNode.simpleString());

}

}returnsb.toString();

}

水平方向遍历(TwoThreeFourTree类中)

public ListlevelPost() {

List resultList = new ArrayList();if (this.root != null) {

Queue queue = newLinkedList();

queue.offer(this.root);while (queue.size() != 0) {

Node curNode=queue.poll();

resultList.add(curNode);for(Node tmpNode : curNode.subNodes) {

queue.offer(tmpNode);

}

}

}returnresultList;

}

新建一个测试类(TwoThreeFourTreeTest):

创建示例图片的2-3-4树:

privateTwoThreeFourTree getDemoTree() {

TwoThreeFourTree tree= newTwoThreeFourTree();int inputs[] = {50, 23, 76, 10, 16,20, 31, 42, 60, 83,5, 9, 15, 17, 19,22, 25, 27, 33, 45,55, 71, 77, 92};for (int i = 0; i < inputs.length; i++) {int tmpInput =inputs[i];

tree.insert(tmpInput);

}returntree;

}

输出示例树:

private void printList(Listnodes) {for(Node node : nodes) {

LOG.info(node.toString());

}

}

@Testpublic voidtestDemo() {

TwoThreeFourTree tree= this.getDemoTree();

List resultNode =tree.levelPost();this.printList(resultNode);

}

测试路径:

@Testpublic voidtestPath() {

TwoThreeFourTree tree= this.getDemoTree();int key = 0;

List path = null;

key= 20;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 42;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 45;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 23;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 50;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 16;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 19;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

key= 31;

path=tree.path(key);

LOG.info("key:" + key + ",path:" +path);

}

测试转换:

@Testpublic voidtestTransfer() {

TwoThreeFourTree tree= this.getDemoTree();int key = 0;

List resultNode = null;

key= 31;

tree.transfer(key);

resultNode=tree.levelPost();this.printList(resultNode);

}

测试删除:

@Testpublic voidtestDelete() {

TwoThreeFourTree tree= this.getDemoTree();int key = 0;

List resultNode = null;

key= 31;

tree.delete(key);

resultNode=tree.levelPost();this.printList(resultNode);

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值