定义
和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
1. 要么为空,要么:
2. 对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key有效,有节点也是一个2-3节点,所有的值比key要大。
3. 对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
如果中序遍历2-3查找树,就可以得到排好序的序列。在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。
查找
在进行2-3树的平衡之前,我们先假设已经处于平衡状态,我们先看基本的查找操作。
2-3树的查找和二叉查找树类似,要确定一个树是否属于2-3树,我们首先和其跟节点进行比较,如果相等,则查找成功;否则根据比较的条件,在其左中右子树中递归查找,如果找到的节点为空,则未找到,否则返回。查找过程如下图:
插入
往一个2-node节点插入
往2-3树中插入元素和往二叉查找树中插入元素一样,首先要进行查找,然后将节点挂到未找到的节点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。如果查找后未找到的节点是一个2-node节点,那么很容易,我们只需要将新的元素放到这个2-node节点里面使其变成一个3-node节点即可。但是如果查找的节点结束于一个3-node节点,那么可能有点麻烦。
往一个3-node节点插入
往一个3-node节点插入一个新的节点可能会遇到很多种不同的情况,下面首先从一个最简单的只包含一个3-node节点的树开始讨论。
只包含一个3-node节点
如上图,假设2-3树只包含一个3-node节点,这个节点有两个key,没有空间来插入第三个key了,最自然的方式是我们假设这个节点能存放三个元素,暂时使其变成一个4-node节点,同时他包含四个子节点。然后,我们将这个4-node节点的中间元素提升,左边的节点作为其左节点,右边的元素作为其右节点。插入完成,变为平衡2-3查找树,树的高度从0变为1。
节点是3-node,父节点是2-node
和第一种情况一样,我们也可以将新的元素插入到3-node节点中,使其成为一个临时的4-node节点,然后,将该节点中的中间元素提升到父节点即2-node节点中,使其父节点成为一个3-node节点,然后将左右节点分别挂在这个3-node节点的恰当位置。操作如下图:
节点是3-node,父节点也是3-node
当我们插入的节点是3-node的时候,我们将该节点拆分,中间元素提升至父节点,但是此时父节点是一个3-node节点,插入之后,父节点变成了4-node节点,然后继续将中间元素提升至其父节点,直至遇到一个父节点是2-node节点,然后将其变为3-node,不需要继续进行拆分。
根节点分裂
当根节点到字节点都是3-node节点的时候,这是如果我们要在字节点插入新的元素的时候,会一直查分到跟节点,在最后一步的时候,跟节点变成了一个4-node节点,这个时候,就需要将跟节点查分为两个2-node节点,树的高度加1,这个操作过程如下:
本地转换
将一个4-node拆分为2-3node涉及到6种可能的操作。这4-node可能在跟节点,也可能是2-node的左子节点或者右子节点。或者是一个3-node的左,中,右子节点。所有的这些改变都是本地的,不需要检查或者修改其他部分的节点。所以只需要常数次操作即可完成2-3树的平衡。
性质
这些本地操作保持了2-3树的平衡。对于4-node节点变形为2-3节点,变形前后树的高度没有发生变化。只有当跟节点是4-node节点,变形后树的高度才加一。如下图所示:
分析
完全平衡的2-3查找树如下图,每个根节点到叶子节点的距离是相同的:
2-3树的查找效率与树的高度是息息相关的。
- 在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN
- 在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN
距离来说,对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。
对于插入来说,只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。下面是2-3查找树的效率:
实现
直接实现2-3树比较复杂,因为:
- 需要处理不同的节点类型,非常繁琐
- 需要多次比较操作来将节点下移
- 需要上移来拆分4-node节点
- 拆分4-node节点的情况有很多种
2-3查找树实现起来比较复杂,在某些情况插入后的平衡操作可能会使得效率降低。在2-3查找树基础上改进的红黑树不仅具有较高的效率,并且实现起来较2-3查找树简单。
但是2-3查找树作为一种比较重要的概念和思路对于后文要讲到的红黑树和B树非常重要。希望本文对您了解2-3查找树有所帮助。
下面是我写的Java代码,插入没问题,删除没写,谢谢。
package com.yc.tree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
/**
* 2-3树:it is so hard for me to finish it.
* 花了王某人几天时间!!!而且代码写的真的烂,什么时候能写出
* 一副让人看一眼就很舒服的代码。特别是写split()这里方法时,
* 千万地仔细再仔细。稍不留神就是数据错误或指针指向错误。
* 我特么就是把指针指向写错了,导致后面插入bug,最鸡巴难调试。
* 总之,觉得大家在写这类代码,特别是树啊,图啊之类含指针之前,
* 最好是用笔画个草图!
* @author wb
* @param <T>
*
*
*/
public class Two_Three_Tree <T extends Comparable<T>>{
private class Node{
Node parent; //没办法,发现后面维护的时候需要向上递归
//存放值
Object[] data_K;
//存放该节点子节点的引用
Object[] node_A;
public Node(int k, int a, T data, Node parent){
data_K = new Object[k];
node_A = new Object[a];
data_K[0] = data;
this.parent = parent;
}
public Node(int k, int a, Node parent){
data_K = new Object[k];
node_A = new Object[a];
this.parent = parent;
}
public String toString(){
if(data_K != null){
StringBuilder sb = new StringBuilder("[");
for(Object k : data_K){
sb.append(k.toString()+",");
}
return sb.toString().substring(0, sb.length() - 1) + "]";
}else{
return "[]";
}
}
}
//根节点
private Node root;
public Two_Three_Tree(){
root = null;
}
public Two_Three_Tree(T data){
root = new Node(1, 2, data, null);
}
/**
* 插入元素data
* @param data
*/
@SuppressWarnings("unchecked")
public void add(T data){
if(root == null){
root = new Node(1, 2, data, null);
}else{
//找相关节点
Node current = root;
Node parent = current;
T maxtmp = null,mintmp = null;
while(current != null){
if(current.data_K.length == 1){ //如果是2-Node
maxtmp = mintmp = (T)current.data_K[0];
}else{ //否则是3-Node
int result = ((T)current.data_K[0]).compareTo((T)current.data_K[1]);
if(result > 0 ){
maxtmp = (T)current.data_K[0];
mintmp = (T)current.data_K[1];
}else if(result < 0){
maxtmp = (T)current.data_K[1];
mintmp = (T)current.data_K[0];
}
}
//分出最大值和最小值后,
if(data.compareTo(mintmp) == 0 || data.compareTo(maxtmp) == 0){
return; //如果树中已经存在要插入的数据,则返回
}
if(maxtmp == mintmp){ //2-node
parent = current;
if(data.compareTo(maxtmp) > 0){
current = (Node) current.node_A[1];
}else if(data.compareTo(maxtmp) < 0){
current = (Node)current.node_A[0];
}
}else{ //3-node
parent = current;
if(data.compareTo(maxtmp) > 0){
current = (Node)current.node_A[2];
}else if(data.compareTo(mintmp) > 0){
current = (Node)current.node_A[1];
}else{
current = (Node)current.node_A[0];
}
}
}
//找到了parent,也就是跟要添加数据最直接相关的节点
//为了不写重复代码,我选择了再进行一次比较和判断
if(parent != null && maxtmp == mintmp && maxtmp != null){
//把data和node两个数组都加一
parent.data_K = Arrays.copyOf(parent.data_K, parent.data_K.length + 1);
parent.node_A = Arrays.copyOf(parent.node_A, parent.node_A.length + 1);
//把data值加入data数组
parent.data_K[1] = data;
//下面是确保3-Node中的左key 小于 右key
swap(parent, 0, 1);
}else if(parent != null && maxtmp != mintmp && maxtmp != null){
fixForAdd(parent.parent, parent, data);
}
}
}
/**
* 在3-Node节点时添加节点对树的维护
* @param parent:你想知道原因
* @param node:你真的想知道原因
* @param data:就不告诉你
*/
@SuppressWarnings("unchecked")
private void fixForAdd(Node parent, Node node, T data) {
if(root == node && node.data_K.length == 2){ //node==3-Node && node==root
if(data.compareTo((T)node.data_K[0]) < 0){
root = new Node(1, 2, (T)node.data_K[0], null);
root.node_A[0] = new Node(1, 2, data, root);
root.node_A[1] = new Node(1, 2, (T)node.data_K[1], root);
}else if(data.compareTo((T)node.data_K[1]) > 0){
root = new Node(1, 2, (T)node.data_K[1], null);
root.node_A[0] = new Node(1, 2, (T)node.data_K[0], root);
root.node_A[1] = new Node(1, 2, data, root);
}else{
root = new Node(1, 2, data, null);
root.node_A[0] = new Node(1, 2, (T)node.data_K[0], root);
root.node_A[1] = new Node(1, 2, (T)node.data_K[1], root);
}
return;
}
/*if(node.data_K.length == 2 && parent != null && parent.data_K.length == 1){//node==3-Node && parent==2-Node
}*/
if(node.data_K.length == 2){
Node n_4Node = create4_Node(node, data);
Node current = n_4Node;
while(current != null && current.data_K.length == 3){
current = split(current, current.parent);
}
}
}
/**
* 由3-Node创建新的4-Node,当然还有一种方法就是将3-Node拓展成4-Node
* @param node_3:3-Node
* @param data:某数据
* @return:返回新建的4-Node
*/
private Node create4_Node(Node node_3, T data){
if(node_3 != null && node_3.data_K.length == 2){ //如果node_3是3-Node把他变成4-Node
Node node_4 = new Node(3, 4, node_3.parent); //新创建的4-Node指回了去
node_4.data_K[0] = node_3.data_K[0];
node_4.data_K[1] = node_3.data_K[1];
node_4.data_K[2] = data;
swap(node_4, 0, 1);
swap(node_4, 0, 2);
swap(node_4, 1, 2);
finalDealForSplit(node_4, node_3);
return node_4;
}
//没用的代码,跳过
/*if(data.compareTo((T)node_3.data_K[0]) < 0){
if(!isHave((Node)node_3.node_A[0], data)){ //没有
node_4.data_K[0] = data;
node_4.data_K[1] = node_3.data_K[0];
node_4.data_K[2] = node_3.data_K[1];
}else{
//有。(也就是data是从下面的左一个子节点传过来的,而且必定是3-Node的data.A[0].data_K[1])
node_4.data_K[0] = data;
node_4.data_K[1] = node_3.data_K[0];
node_4.data_K[2] = node_3.data_K[1];
node_4.node_A[0] = new Node(1, 2, (T)((Node)node_3.node_A[0]).data_K[0], node_4);
node_4.node_A[1] = new Node(1, 2, (T)((Node)node_3.node_A[0]).data_K[2], node_4);
node_4.node_A[2] = node_3.node_A[1];
node_4.node_A[3] = node_3.node_A[2];
Node n1_t = (Node)node_3.node_A[1];
Node n2_t = (Node)node_3.node_A[2];
n1_t.parent = n2_t.parent = node_4;
}
}else if(data.compareTo((T)node_3.data_K[1]) > 0){
if(!isHave((Node)node_3.node_A[2], data)){
node_4.data_K[0] = node_3.data_K[0];
node_4.data_K[1] = node_3.data_K[1];
node_4.data_K[2] = data;
}else{
node_4.data_K[0] = node_3.data_K[0];
node_4.data_K[1] = node_3.data_K[1];
node_4.data_K[2] = data;
node_4.node_A[0] = node_3.node_A[0];
node_4.node_A[1] = node_3.node_A[1];
node_4.node_A[2] = new Node(1, 2, (T)((Node)node_3.node_A[2]).data_K[0], node_4);
node_4.node_A[3] = new Node(1, 2, (T)((Node)node_3.node_A[2]).data_K[2], node_4);
Node n3_t = (Node)node_3.node_A[0];
Node n4_t = (Node)node_3.node_A[1];
n3_t.parent = n4_t.parent = node_4;
}
}else{
if(!isHave((Node)node_3.node_A[1], data)){
node_4.data_K[0] = node_3.data_K[0];
node_4.data_K[1] = data;
node_4.data_K[2] = node_3.data_K[1];
}else{
node_4.data_K[0] = node_3.data_K[0];
node_4.data_K[1] = data;
node_4.data_K[2] = node_3.data_K[1];
node_4.node_A[0] = node_3.node_A[0];
node_4.node_A[0] = new Node(1, 2, (T)((Node)node_3.node_A[1]).data_K[0], node_4);
node_4.node_A[0] = new Node(1, 2, (T)((Node)node_3.node_A[1]).data_K[2], node_4);
node_4.node_A[0] = node_3.node_A[2];
Node n2_t = (Node)node_3.node_A[0];
Node n3_t = (Node)node_3.node_A[2];
n2_t.parent = n3_t.parent = node_4;
}
}*/
return null;
}
/**
* 分离 node_4为2个node_2
* @param node_4
*/
@SuppressWarnings("unchecked")
private Node split(Node node_4, Node parent){
if(node_4 != null){
if(root == node_4 && node_4.data_K.length == 3){//本身是4-Node,且是根
root = new Node(1, 2, (T)node_4.data_K[1], null);
Node l_tmp = new Node(1, 2, (T)node_4.data_K[0], root);
Node r_tmp = new Node(1, 2, (T)node_4.data_K[2], root);
l_tmp.node_A[0] = node_4.node_A[0];
l_tmp.node_A[1] = node_4.node_A[1];
if( node_4.node_A[0] != null){
Node t0 = (Node)node_4.node_A[0];
t0.parent = l_tmp;
}
if( node_4.node_A[1] != null){
Node t1 = (Node)node_4.node_A[1];
t1.parent = l_tmp;
}
r_tmp.node_A[0] = node_4.node_A[2];
r_tmp.node_A[1] = node_4.node_A[3];
if( node_4.node_A[2] != null){
Node t2 = (Node)node_4.node_A[2];
t2.parent = r_tmp;
}
if( node_4.node_A[3] != null){
Node t3 = (Node)node_4.node_A[3];
t3.parent = r_tmp;
}
root.node_A[0] = l_tmp;
root.node_A[1] = r_tmp;
return root;
}else if(node_4.data_K.length == 3 && parent.data_K.length == 1){//本身是4-Node,父节点是2-Node
Node p = new Node(2, 3, parent.parent);
Node s1 = new Node(1, 2, p);
Node s2 = new Node(1, 2, p);
p.data_K[0] = node_4.data_K[1];
p.data_K[1] = parent.data_K[0];
swap(p, 0, 1);
if(node_4 == parent.node_A[0]){ //左
p.node_A[2] = parent.node_A[1];
if(parent.node_A[1] != null){
Node t = (Node)parent.node_A[1];
t.parent = p;
}
s1_S2(s1, s2, node_4, p);
p.node_A[0] = s1;
p.node_A[1] = s2;
}else if(node_4 == parent.node_A[1]){//右
p.node_A[0] = parent.node_A[0];
if(parent.node_A[0] != null){
Node t = (Node)parent.node_A[0];
t.parent = p;
}
s1_S2(s1, s2, node_4, p);
p.node_A[1] = s1;
p.node_A[2] = s2;
}
finalDealForSplit(p, parent);
if(p.parent == null){
root = p;
}
return p;
}else if(node_4.data_K.length == 3 && parent.data_K.length == 2){//本身是4-Node,父节点是3-Node
Node p = new Node(3, 4, parent.parent);
Node s1 = new Node(1, 2, p);
Node s2 = new Node(1, 2, p);
p.data_K[0] = node_4.data_K[1];
p.data_K[1] = parent.data_K[0];
p.data_K[2] = parent.data_K[1];
//如果上面(316-318)的代码写在判断里面,下面的swap可以不写,总之是为了p中的三个数据有小到大存储
swap(p, 0, 1);
swap(p, 0, 2);
swap(p, 1, 2);
if(node_4 == parent.node_A[0]){//左
p.node_A[2] = parent.node_A[1];
p.node_A[3] = parent.node_A[2];
if(parent.node_A[1] != null){
Node t3 = (Node)parent.node_A[1];
t3.parent = p;
}
if(parent.node_A[2] != null){
Node t2 = (Node)parent.node_A[2];
t2.parent = p;
}
s1_S2(s1, s2, node_4, p);
p.node_A[0] = s1;
p.node_A[1] = s2;
}else if(node_4 == parent.node_A[1]){ //中
p.node_A[0] = parent.node_A[0];
p.node_A[3] = parent.node_A[2];
if(parent.node_A[0] != null){
Node t0 = (Node)parent.node_A[0];
t0.parent = p;
}
if(parent.node_A[2] != null){
Node t2 = (Node)parent.node_A[2];
t2.parent = p;
}
s1_S2(s1, s2, node_4, p);
p.node_A[1] = s1;
p.node_A[2] = s2;
}else if(node_4 == parent.node_A[2]){ //右
p.node_A[0] = parent.node_A[0];
p.node_A[1] = parent.node_A[1];
if(parent.node_A[0] != null){
Node t0 = (Node)parent.node_A[2];
t0.parent = p;
}
if(parent.node_A[1] != null){
Node t1 = (Node)parent.node_A[1];
t1.parent = p;
}
s1_S2(s1, s2, node_4, p);
p.node_A[2] = s1;
p.node_A[3] = s2;
}
finalDealForSplit(p, parent);
if(p.parent == null){
root = p;
}
return p;
}
}
return null;
}
//
@SuppressWarnings("unchecked")
private void s1_S2(Node s1, Node s2, Node node_4,Node p){
s1.data_K[0] = node_4.data_K[0];
s1.node_A[0] = node_4.node_A[0];
s1.node_A[1] = node_4.node_A[1];
if(node_4.node_A[0] != null){
Node n1 = (Node)node_4.node_A[0];
n1.parent = s1;
}
if(node_4.node_A[1] != null){
Node n2 = (Node)node_4.node_A[1];
n2.parent = s1;
}
s2.data_K[0] = node_4.data_K[2];
s2.node_A[0] = node_4.node_A[2];
s2.node_A[1] = node_4.node_A[3];
if(node_4.node_A[2] != null){
Node n3 = (Node)node_4.node_A[2];
n3.parent = s2;
}
if(node_4.node_A[3] != null){
Node n4 = (Node)node_4.node_A[3];
n4.parent = s2;
}
}
//
private void finalDealForSplit(Node p, Node parent){
if(root == parent){
root = p;
}else{
Node pp = parent.parent;
for(int i = 0; pp != null && i < pp.node_A.length; i ++){
if(pp.node_A[i] == parent){
pp.node_A[i] = p;
break;
}
}
}
}
/*@SuppressWarnings("unchecked")
private boolean isHave(Node node, T data){
if(node != null){
for(int i = 0; i < node.data_K.length; i ++){
if(data.compareTo((T)node.data_K[i]) == 0){
return true;
}else{
continue;
}
}
}
return false;
}*/
//如果t1 > t2,则交换t1和t2
@SuppressWarnings("unchecked")
private void swap(Node node, int t1, int t2){
if(node != null && node.data_K.length > t2){
if(((T)node.data_K[t1]).compareTo((T)node.data_K[t2]) > 0){
T t = (T)node.data_K[t1];
node.data_K[t1] = node.data_K[t2];
node.data_K[t2] = t;
}
}
}
/**
* 查找
* @param data
* @return
*/
@SuppressWarnings("unchecked")
public Msg search(T data){
if(root == null){
new Exception("2-3树为空,查你妹╮(╯_╰)╭");
}
else{
int height = 1;
Node current = root;
while(current != null){
//因为2-3树中只会存在2-Node和3-Node两种节点
if(current.data_K.length == 1){ //当前节点是2-Node
if(data.compareTo((T)current.data_K[0]) < 0){
current = (Node) current.node_A[0];
}else if(data.compareTo((T)current.data_K[0]) > 0){
current = (Node) current.node_A[1];
}else{
return new Msg(current, height);
}
}
else if(current.data_K.length == 2){//当前节点是3-Node
if(data.compareTo((T)current.data_K[0]) == 0 || data.compareTo((T)current.data_K[1]) == 0){
return new Msg(current, height);
}
if(data.compareTo((T)current.data_K[0]) < 0 ){
current = (Node)current.node_A[0];
}else if(data.compareTo((T)current.data_K[1]) > 0){
current = (Node)current.node_A[2];
}else{
current = (Node)current.node_A[1];
}
}
height ++;
}
}
return null;
}
/**
* 删除
* @param data
* @return
*/
public String delete(T data){
if(root == null){
new Exception("2-3树为空,删你妹╮(╯_╰)╭");
}else{
//老实人讲老实话,2-3的删除有必要吗?我也不知道!
//查看很多文档还有博客,都未提到2-3的删除操作,或者说的很少
//从一颗标准的2-3树来看,每次的插入都会维护它使其满足从根到每个叶子节点
//的路径是一样,倘若真的要删除元素,然后去维护其为一棵标准的2-3树代价
//很高很高,这还是排除插入时的维护!!。删除时傻子做的事。
//若真的要删除,我有个想法就是删除要删除节点的前驱节点或后继节点。(而且
//这里的前驱节点或后继节点一定是叶子节点)
//有兴趣的同学可以去试试,我就不写了,因为我累了!不是,是想给同学一个
//自己动手写代码的机会。
//下面是伪代码:
/*找到要删除元素的节点node,应该很快。
没有就return "找不到指定的数据元素之类的话";
有就接着下一步:
找到该元素node的后继节点succ,然后把node节点的
值替换成后继节点succ节点的最小的值。最后把后继节点
从树中剔除掉,完事。*/
return "删除成功";
}
return "删除失败";
}
//广度优先遍历
public List<Node> breadthFirstSearch(){
return cBreadthFirstSearch(root);
}
@SuppressWarnings("unchecked")
private List<Node> cBreadthFirstSearch(Node node) {
List<Node> nodes = new ArrayList<Node>();
Deque<Node> deque = new ArrayDeque<Node>();
if(node != null){
deque.offer(node);
}
while(!deque.isEmpty()){
Node n = deque.poll();
nodes.add(n);
for(int i = 0; i < n.node_A.length; i ++){
if(n.node_A[i] != null){
deque.offer((Node)n.node_A[i]);
}
}
}
return nodes;
}
public static void main(String[] args) {
Two_Three_Tree<Integer> tree = new Two_Three_Tree<Integer>();
tree.add(12);
tree.add(10);
System.out.println(tree.breadthFirstSearch());
tree.add(11);
System.out.println(tree.breadthFirstSearch());
tree.add(14);
System.out.println(tree.breadthFirstSearch());
tree.add(16);
System.out.println(tree.breadthFirstSearch());
tree.add(18);
System.out.println(tree.breadthFirstSearch());
tree.add(20);
System.out.println(tree.breadthFirstSearch());
for(int i = 3239200; i > 0; i --){
tree.add(i);
}
System.out.println("That is all!");
//System.out.println(tree.breadthFirstSearch());
System.out.println("数据12在"+tree.search(12));
}
private class Msg{
private Node node;
private int height;
public Msg(Node node, int height){
this.node = node;
this.height = height;
}
public String toString(){
return "第"+height+"层:"+node.toString();
}
}
}
测试结果为:
[[10,12]]
[[11], [10], [12]]
[[11], [10], [12,14]]
[[11,14], [10], [12], [16]]
[[11,14], [10], [12], [16,18]]
[[14], [11], [18], [10], [12], [16], [20]]
That is all!
数据12在第21层:[12,13]