2-3-4树---红黑树基础

Posted 后端开发与算法

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-3-4树---红黑树基础相关的知识,希望对你有一定的参考价值。

前一节中学习过二分查找树,它已经能很好的运行在许多应用程序中,但它在最坏的情况下性能很糟糕。在这里学习一种新的二分查找树,它的运行时间都是对数级别的。理想情况下我们希望能保证二分查找的平衡性,在一棵含有N个结点的的树中,我们希望树高为~lg(N),这样我们就能保证所有查找在lgN次比较内结束,就和二分查找一样。

2-3-4查找树

为了保证查找树的平衡树,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。

2-3-4树和红黑树一样,也是平衡树。只不过不是二叉树,它的子节点数目可以达到4个。

每个节点存储的数据项可以达到3个。名字中的2,3,4是指节点可能包含的子节点数目。具体而言:

  1. 若父节点中存有1个数据项,则必有2个子节点。

  2. 若父节点中存有2个数据项,则必有3个子节点。

  3. 若父节点中存有3个数据项,则必有4个子节点。

也就是说子节点的数目是父节点中数据项的数目加一。因为以上三个规则,使得除了叶结点外,其他节点必有2到4个子节点,不可能只有一个子节点。所以不叫1-2-3-4树。而且2-3-4树中所有叶结点总是在同一层。

2-3-4树实现

DataItem类表示节点中存储的数据项的数据类型。

/** * 存储的数据类型 * 自定义对象 * key long 类型 * @param <T> */public static class DataItem<T>{ private long key; private T data; public long getKey() { return key; } public void setKey(long key) { this.key = key; } public T getData() { return data; } public void setData(T data) { this.data = data; }
@Override public String toString() { return "{" + "key=" + key + ", data=" + data + '}'; } public DataItem(T data) { this.data = data; this.key = data.hashCode(); }}

结点类

1.表示结点中存储数据的方式

   包括两个数组类型,childArray和ItemArray。childArray有四个数据单元,来存储子节点,ItemArray有三个存储单元用于存储DataItem对象(的引用)
2.主要有三个方法
   ①根据关键key在itemArray中查找
   ②insertItem 插入到ItemArray中,并保持有序(根据关键key大小排序)

/** * 结点类 * 1.表示结点中存储数据的方式 * 包括两个数组类型,childArray和ItemArray。childArray有四个数据单元, * 来存储子节点,ItemArray有三个存储单元用于存储DataItem对象(的引用) * 2.主要有三个方法 * ①根据关键key在itemArray中查找 * ②insertItem 插入到ItemArray中,并保持有序(根据关键key大小排序) * ③removeItem 根据关键key删除数据,并保持有序 */class Node<T>{ /** * 子节点数组大小 */ private static final int ORDER = 4; /** * 当前节点中存储的数据,不大于3 */ private int numItems; /** * 父节点 */ private Node parent; /** * 数据数组 */ private DataItem<T>[] itemArray = new DataItem[ORDER - 1]; /** * 子节点引用数组 */ private Node<T>[] childArray = new Node[ORDER]; /** * 将参数的结点作为子节点拼接 * @param childIndex * @param child */ public void connectChild(int childIndex,Node child){ childArray[childIndex] = child; if (child != null){ child.parent = this; } }
/** * 断开参数确定的节点与当前节点的连接,这个节点一定是当前节点的子节点。 */ public Node disconnectChild(int childIndex){ Node tempNode = childArray[childIndex]; //断开连接 childArray[childIndex] = null; //返回要这个子节点 return tempNode; }
/** * 获取子节点 * @param childIndex * @return */ public Node getChild(int childIndex){ return childArray[childIndex]; }
/** * 获取当前节点的子节点个数 * @param node * @return */ public int getChildSize(Node node){ Node[] childArray = node.childArray; int i = 0; for (; i < childArray.length; i++) { if (childArray[i] == null){ return i; } } return i; } /** * 获取父节点 * @return */ public Node getParent() { return parent; }
/** * 获取实际叶节点的存储数目 * @return */ public int getNumItems() { return numItems; }
/** * //是否是叶结点 * @return */ public boolean isLeaf(){ //叶结点没有子节点 return (childArray[0]==null) ? true : false; }
/** * 判断当前节点是否已满 * @return */ public boolean isFull(){ return (numItems == ORDER - 1) ? true : false; }
/** * 根据key查询 * @param key * @return */ public DataItem findItem(long key){ for (int i = 0; i < ORDER - 1; i++) { // 如果数组未满未找到直接返回 if (itemArray[i] == null){ break; }else if (itemArray[i].key == key){ return itemArray[i]; } } return null; }
/** * 节点未满时插入数据方法 * @param dataItem * @return */ public int insertData(DataItem dataItem){ numItems++; long key = dataItem.key; // 从数据节点的右边第二个开始查找 //未满则代表最少有一个位置 itemArray 1 1 1 for (int i = ORDER - 2; i >= 0; i--) { if (itemArray[i] == null){ continue; }else{ // 获取当前数据key long itKey = itemArray[i].key; if (itKey > key){ // 如果key小于当前key 将当前数据后移 itemArray[i + 1] = itemArray[i]; }else{ // 否则在当前节点后插入 itemArray[i + 1] = dataItem; return i + 1; } } } /** * 插入第一个节点的位置 * 1.如果初始节点为null则上面什么都不做,则会插入在第一个节点的位置 * 2.如果上面的操作都当前key小于当前节点中的所有key 仍然为插入第一个节点的位置 */ itemArray[0] = dataItem; return 0; }
/** * 获取查询的下一个子节点 * 示例 * (10 20 30) k ---层 * /| / | | | k+1 ---层 * (5 6) (12 15) (22 23) (31 33) * 查找15时返回(12,15)子节点, * 查询35时返回(31,33)子节点 * @param theKey 查找的key * @return */ public Node getNextChild(long theKey){ int i = 0; // 当前节点的个数 int numItems = this.numItems; for (; i < numItems; i++) { // 如果查询的结点key大于当前节点的值 则向下查找 if (this.itemArray[i].key > theKey){ return this.childArray[i]; } } // 如果查询的节点key始终小于当前key 即从最右节点查找 i == numItems return this.childArray[i]; }
/** * 从后往前移除结点 * @return */ public DataItem removeItem(){ if (numItems == 0) return null; numItems --; DataItem dataItem = itemArray[numItems]; itemArray[numItems] = null; return dataItem; }
/** * 打印当前节点的数据 * @return */ public String displayItem(){ StringBuffer buffer = new StringBuffer(); buffer.append("["); for (int i = 0; i < numItems; i++) { buffer.append(itemArray[i]); if (i != (numItems - 1)){ buffer.append(", "); } } buffer.append("]"); return buffer.toString(); }
}

TreeTwoThreeFour类

它表示一颗完整的2-3-4树。它只有一个数据项:root,类型为Node。我们操作一棵树,只需要知道它的根就行了。

关键方法

find:根据关键字查找树中是否存在。从根开始,依次调用getNextChild方法来向下查找,在每个节点上都调用Node类中的findItem方法在当前节点中查找。当在底层的叶结点查找完毕,整个查找过程就结束了。若仍未找到,则查找失败,返回-1。

insert:与find方法类似,不断向下查找,直到叶结点,插入数据项。这个过程中遇到满节点会先执行分裂操作,调用split方法,再来插入数据项。

split:思路如下

(1)满节点是根节点

* 1.创建一个新的节点,作为要分裂兄弟的父结点* 2.再创建一个新的节点,作为要分裂的兄弟右节点,位于节点的右侧* 3.将数据C移动到创建的兄弟节点,放在右侧* 4.将数据B移动到创建的父节点,数据A保留原地* 5.将需要分裂的节点最右边两个子节点断开连接,重新拼接在兄弟节点上

2-3-4树---红黑树基础(二)

(2)满节点不是根

* 1.创建一个新节点,与要分裂的节点是兄弟节点,放在其右侧* 2.将数据C放入新建节点,将数据B移动到父节点的相应位置* 3.数据节点A保留原来的位置* 4.将原节点最右边的两个子节点(原节点为满节点一定有四个子节点或叶节点)从要分裂的地方断开,*      连接到新建的兄弟节点上

public class TreeTwoThreeFour<T> {
/** * 根节点 */ Node root = new Node(); /** * 根据key查询方法 * 1.根据键key值在当前节点中查找,如果找到则返回 * 2.如果当前节点中未找到则在child中查询,getNextChild返回子节点的数据 * @param key * @return */ public DataItem find(long key){ Node currNode = root; while (currNode != null){ DataItem item = currNode.findItem(key); if (item != null){ return item; }else if(currNode.isLeaf()){ // 如果叶子节点也没有 return null; }else{ // 获取下一个查询节点 currNode = currNode.getNextChild(key); } } return null; }
/** * 插入节点数据 * 1.与find方法类似,不断向下查找找到叶节点,插入数据项 * 2.判断当前插入节点是否是满节点,如果是则先进行分裂操作,再插入数据项操作 * @param dataItem */ public void insert(DataItem dataItem){ Node currNode = root; long key = dataItem.key; while (true){ if (currNode.isFull()){ // 如果是满节点 先进行分裂操作 split(currNode); // 回到分裂出的父节点上 currNode = currNode.getParent(); // 向下查找 currNode = currNode.getNextChild(key); }else if(currNode.isLeaf()){ // 是叶节点 非满节点 break; }else{ // 向下查找 currNode = currNode.getNextChild(key); } } currNode.insertData(dataItem); }

/** * 分裂节点 * 一、处理节点为根节点 * (A B C) ----处理节点 * / | * 1 2 3 4 * * 1.创建一个新的节点,作为要分裂兄弟的父结点 * 2.再创建一个新的节点,作为要分裂的兄弟右节点,位于节点的右侧 * 3.将数据C移动到创建的兄弟节点,放在右侧 * 4.将数据B移动到创建的父节点,数据A保留原地 * 5.将需要分裂的节点最右边两个子节点断开连接,重新拼接在兄弟节点上 * 二、处理节点不为根节点 * (50) * / * (20) (A , B , C) ----处理节点 * / / | * (15) (22) (55) (62) (71) (83) * 1.创建一个新节点,与要分裂的节点是兄弟节点,放在其右侧 * 2.将数据C放入新建节点,将数据B移动到父节点的相应位置 * 3.数据节点A保留原来的位置 * 4.将原节点最右边的两个子节点(原节点为满节点一定有四个子节点或叶节点)从要分裂的地方断开, * 连接到新建的兄弟节点上 * * @param node */ public void split(Node node){ Node parent; // 存储当前节点的数据项 DataItem itemC = node.removeItem(); DataItem itemB = node.removeItem(); // 断开子节点的连接 Node childB = node.disconnectChild(2); Node childC = node.disconnectChild(3); // 创建一个新节点 作为右结点 Node newRight = new Node(); /** * 获取父节点 * 如果当前节点是根节点 */ if (node == root){ // 创建一个新的根节点 root = new Node(); // 新节点为父节点 parent = root; // 拼接当前节点 root.connectChild(0,node); }else{ /** * 不是根节点则赋值当前节点 */ parent = node.getParent(); } /** * 将数据itemB插入到父节点中,并重新建立子节点的联系 */ // 将节点B插入父节点 返回父节点的索引位置 int insertIndex = parent.insertData(itemB); int numItems = parent.getNumItems(); /** * 更新右兄弟节点的连接 */ for (int i = numItems - 1; i > insertIndex; i--) { // 断开以前的父亲节点的连接 Node temp = parent.disconnectChild(i); parent.connectChild(i+1,temp); } // 将新节点与父亲节点连接 parent.connectChild(insertIndex+1,newRight); // 处理兄弟节点C newRight.insertData(itemC); // 将B和C的子节点关联在兄弟节点上 newRight.connectChild(0,childB); newRight.connectChild(1,childC); }
/** * 展示所有节点数据 */ public void displayTree(){ resDisplayTree(root,0,0);
}    /** * 绘制树 * @param root * @param level * @param childNum * @return */ private void resDisplayTree(Node root, int level, int childNum) { System.out.println("level="+level+" child="+childNum+" "); String item = root.displayItem(); System.out.println(item); int numItems = root.getNumItems(); for (int i = 0; i < numItems + 1; i++) { Node child = root.getChild(i); if (child!=null){ resDisplayTree(child,level+1,i); } } return; }}

插入数据10,20,30,40,50,60,70后形成的2-3-4树为

总结:

 * 1、首先分析一个大操作分为几个部分,先进行什么操作,再进行什么操作,把操作的顺序和操作的类别搞清楚。

 * 2、抽象出每个小的操作过程,不考虑具体实现,封装成函数名称。

 * 3、对操作过程进行具体分析,从上到下,对每一种可能情况进行具体分析,这可能会涉及更具体的操作,可以根据情况直接实现。或者再一次进行函数的封装。

 * 4、编写具体函数从下到上,先分析小的操作实现,一步一步到大的操作上去。


以上是关于2-3-4树---红黑树基础的主要内容,如果未能解决你的问题,请参考以下文章

从2-3-4树模型到红黑树实现

数据结构与算法:树 红黑树

数据结构与算法:树 红黑树

从红黑树的本质出发,彻底理解红黑树!

从红黑树的本质出发,彻底理解红黑树!

从2-3树到红黑树,BB+B树