2-3 查找树

定义(来源:wiki)

2–3树是一种树型数据结构,内部节点(存在子节点的节点)要么有2个孩子和1个数据元素,要么有3个孩子和2个数据元素,叶子节点没有孩子,并且有1个或2个数据元素。

2个结点

  • 定义

    如果一个内部节点拥有一个数据元素、两个子节点,则此节点为2节点

    如果一个内部节点拥有两个数据元素、三个子节点,则此节点为3节点

    当且仅当以下叙述中有一条成立时,T为2–3树:

    • T为空。即T不包含任何节点。
    • T为拥有数据元素a的2节点。若T的左孩子为L、右孩子为R,则
      • LR是等高的非空2–3树;
      • a大于L中的所有数据元素;
      • a小于等于R中的所有数据元素。
    • T为拥有数据元素ab的3节点,其中a < b。若T的左孩子为L、中孩子为M、右孩子为R,则
      • LM、和R是等高的非空2–3树;
      • a大于L中的所有数据元素,并且小于等于M中的所有数据元素;
      • b大于M中的所有数据元素,并且小于等于R中的所有数据元素。

查找

首先我们说一下查找

2-3查找树的查找和二叉树很类似,无非就是进行比较然后选择下一个查找的方向。 (这几张图不知道来源,知道的呲我一声)

2-3树查找

插入

2-3查找树的插入

我们可以思考一下,为什么要两个结点。在前面可以知道,二叉查找树变成链表的原因就是因为新插入的结点没有选择的”权利”,当我们插入一个元素的时候,实际上它的位置已经确定了, 我们并不能对它进行操作。那么2-3查找树是怎么做到赋予“权利”的呢?秘密便是这个多出来结点,他可以缓存新插入的结点。(具体我们将在插入的时候讲)

前面我们知道,2-3查找树分为2结点3结点,so,插入就分为了2结点插入和3结点插入。

**2-结点插入:**向2-结点插入一个新的结点和向而插入插入一个结点很类似,但是我们并不是将结点“吊”在结点的末尾,因为这样就没办法保持树的平衡。我们可以将2-结点替换成3-结点即可,将其中的键插入这个3-结点即可。(相当于缓存了这个结点)

 

3-结点插入: 3结点插入比较麻烦,emm可以说是特别麻烦,它分为3种情况。

  1. 向一棵只含有3-结点的树插入新键。

    假如2-3树只有一个3-结点,那么当我们插入一个新的结点的时候,我们先假设结点变成了4-结点,然后使得中间的结点为根结点,左边的结点为其左结点,右边的结点为其右结点,然后构成一棵2-3树,树的高度加1

     
  2. 向父结点为2-结点的3-结点中插入新键。

    和上面的情况类似,我们将新的节点插入3-结点使之成为4-结点,然后将结点中的中间结点”升“到其父节点(2-结点)中的合适的位置,使其父节点成为一个3-节点,然后将左右节点分别挂在这个3-结点的恰当位置,树的高度不发生改变

 
  1. 向父节点为3-结点的3-结点中插入新键。

    这种情况有点类似递归:当我们的结点为3-结点的时候,我们插入新的结点会将中间的元素”升“父节点,然后父节点为4-结点,右将中间的结点”升“到其父结点的父结点,……如此进行递归操作,直到遇到的结点不再是3-结点。

 

JAVA代码实现2-3树

接下来就是最难的操作来了,实现这个算法,2-3查找树的算法比较麻烦,所以我们不得不将问题分割,分割求解能将问题变得简单。参考博客

接下来就是最难的操作来了,实现这个算法,2-3查找树的算法比较麻烦,所以我们不得不将问题分割,分割求解能将问题变得简单。参考博客

首先我们定义数据结构,作用在注释已经写的很清楚了。

public class Tree23<Key extends Comparable<Key>,Value> {
/**
* 保存key和value的键值对
* @param <Key>
* @param <Value>
*/
private class Data<Key extends Comparable<Key>,Value>{
private Key key;
private Value value; public Data(Key key, Value value) {
this.key = key;
this.value = value;
}
public void displayData(){
System.out.println("/" + key+"---"+value);
}
} /**
* 保存树结点的类
* @param <Key>
* @param <Value>
*/
private class Node23<Key extends Comparable<Key>,Value>{ public void displayNode() {
for(int i = 0; i < itemNum; i++){
itemDatas[i].displayData();
}
System.out.println("/");
} private static final int N = 3;
// 该结点的父节点
private Node23 parent;
// 子节点,子节点有3个,分别是左子节点,中间子节点和右子节点
private Node23[] chirldNodes = new Node23[N];
// 代表结点保存的数据(为一个或者两个)
private Data[] itemDatas = new Data[N - 1];
// 结点保存的数据个数
private int itemNum = 0; /**
* 判断是否是叶子结点
* @return
*/
private boolean isLeaf(){
// 假如不是叶子结点。必有左子树(可以想一想为什么?)
return chirldNodes[0] == null;
} /**
* 判断结点储存数据是否满了
* (也就是是否存了两个键值对)
* @return
*/
private boolean isFull(){
return itemNum == N-1;
} /**
* 返回该节点的父节点
* @return
*/
private Node23 getParent(){
return this.parent;
} /**
* 将子节点连接
* @param index 连接的位置(左子树,中子树,还是右子树)
* @param child
*/
private void connectChild(int index,Node23 child){
chirldNodes[index] = child;
if (child != null){
child.parent = this;
}
} /**
* 解除该节点和某个结点之间的连接
* @param index 解除链接的位置
* @return
*/
private Node23 disconnectChild(int index){
Node23 temp = chirldNodes[index];
chirldNodes[index] = null;
return temp;
} /**
* 获取结点左或右的键值对
* @param index 0为左,1为右
* @return
*/
private Data getData(int index){
return itemDatas[index];
} /**
* 获得某个位置的子树
* @param index 0为左指数,1为中子树,2为右子树
* @return
*/
private Node23 getChild(int index){
return chirldNodes[index];
} /**
* @return 返回结点中键值对的数量,空则返回-1
*/
public int getItemNum(){
return itemNum;
} /**
* 寻找key在结点的位置
* @param key
* @return 结点没有key则放回-1
*/
private int findItem(Key key){
for (int i = 0; i < itemNum; i++) {
if (itemDatas[i] == null){
break;
}else if (itemDatas[i].key.compareTo(key) == 0){
return i;
}
}
return -1;
} /**
* 向结点插入键值对:前提是结点未满
* @param data
* @return 返回插入的位置 0或则1
*/
private int insertData(Data data){
itemNum ++;
for (int i = N -2; i >= 0 ; i--) {
if (itemDatas[i] == null){
continue;
}else{
if (data.key.compareTo(itemDatas[i].key)<0){
itemDatas[i+1] = itemDatas[i];
}else{
itemDatas[i+1] = data;
return i+1;
}
}
}
itemDatas[0] = data;
return 0;
} /**
* 移除最后一个键值对(也就是有右边的键值对则移右边的,没有则移左边的)
* @return 返回被移除的键值对
*/
private Data removeItem(){
Data temp = itemDatas[itemNum - 1];
itemDatas[itemNum - 1] = null;
itemNum --;
return temp;
}
}
/**
* 根节点
*/
private Node23 root = new Node23();
……接下来就是一堆方法了
}

主要是两个方法:find查找方法和Insert插入方法:看注释

/**
*查找含有key的键值对
* @param key
* @return 返回键值对中的value
*/
public Value find(Key key) {
Node23 curNode = root;
int childNum;
while (true) {
if ((childNum = curNode.findItem(key)) != -1) {
return (Value) curNode.itemDatas[childNum].value;
}
// 假如到了叶子节点还没有找到,则树中不包含key
else if (curNode.isLeaf()) {
return null;
} else {
curNode = getNextChild(curNode,key);
}
}
} /**
* 在key的条件下获得结点的子节点(可能为左子结点,中间子节点,右子节点)
* @param node
* @param key
* @return 返回子节点,若结点包含key,则返回传参结点
*/
private Node23 getNextChild(Node23 node,Key key){
for (int i = 0; i < node.getItemNum(); i++) {
if (node.getData(i).key.compareTo(key)>0){
return node.getChild(i);
}
else if (node.getData(i).key.compareTo(key) == 0){
return node;
}
}
return node.getChild(node.getItemNum());
} /**
* 最重要的插入函数
* @param key
* @param value
*/
public void insert(Key key,Value value){
Data data = new Data(key,value);
Node23 curNode = root;
// 一直找到叶节点
while(true){
if (curNode.isLeaf()){
break;
}else{
curNode = getNextChild(curNode,key);
for (int i = 0; i < curNode.getItemNum(); i++) {
// 假如key在node中则进行更新
if (curNode.getData(i).key.compareTo(key) == 0){
curNode.getData(i).value =value;
return;
}
}
}
} // 若插入key的结点已经满了,即3-结点插入
if (curNode.isFull()){
split(curNode,data);
}
// 2-结点插入
else {
// 直接插入即可
curNode.insertData(data);
}
} /**
* 这个函数是裂变函数,主要是裂变结点。
* 这个函数有点复杂,我们要把握住原理就好了
* @param node 被裂变的结点
* @param data 要被保存的键值对
*/
private void split(Node23 node, Data data) {
Node23 parent = node.getParent();
// newNode用来保存最大的键值对
Node23 newNode = new Node23();
// newNode2用来保存中间key的键值对
Node23 newNode2 = new Node23();
Data mid; if (data.key.compareTo(node.getData(0).key)<0){
newNode.insertData(node.removeItem());
mid = node.removeItem();
node.insertData(data);
}else if (data.key.compareTo(node.getData(1).key)<0){
newNode.insertData(node.removeItem());
mid = data;
}else{
mid = node.removeItem();
newNode.insertData(data);
}
if (node == root){
root = newNode2;
}
/**
* 将newNode2和node以及newNode连接起来
* 其中node连接到newNode2的左子树,newNode
* 连接到newNode2的右子树
*/
newNode2.insertData(mid);
newNode2.connectChild(0,node);
newNode2.connectChild(1,newNode);
/**
* 将结点的父节点和newNode2结点连接起来
*/
connectNode(parent,newNode2);
} /**
* 链接node和parent
* @param parent
* @param node node中只含有一个键值对结点
*/
private void connectNode(Node23 parent, Node23 node) {
Data data = node.getData(0);
if (node == root){
return;
}
// 假如父节点为3-结点
if (parent.isFull()){
// 爷爷结点(爷爷救葫芦娃)
Node23 gParent = parent.getParent();
Node23 newNode = new Node23();
Node23 temp1,temp2;
Data itemData; if (data.key.compareTo(parent.getData(0).key)<0){
temp1 = parent.disconnectChild(1);
temp2 = parent.disconnectChild(2);
newNode.connectChild(0,temp1);
newNode.connectChild(1,temp2);
newNode.insertData(parent.removeItem()); itemData = parent.removeItem();
parent.insertData(itemData);
parent.connectChild(0,node);
parent.connectChild(1,newNode);
}else if(data.key.compareTo(parent.getData(1).key)<0){
temp1 = parent.disconnectChild(0);
temp2 = parent.disconnectChild(2);
Node23 tempNode = new Node23(); newNode.insertData(parent.removeItem());
newNode.connectChild(0,newNode.disconnectChild(1));
newNode.connectChild(1,temp2); tempNode.insertData(parent.removeItem());
tempNode.connectChild(0,temp1);
tempNode.connectChild(1,node.disconnectChild(0)); parent.insertData(node.removeItem());
parent.connectChild(0,tempNode);
parent.connectChild(1,newNode);
} else{
itemData = parent.removeItem(); newNode.insertData(parent.removeItem());
newNode.connectChild(0,parent.disconnectChild(0));
newNode.connectChild(1,parent.disconnectChild(1));
parent.disconnectChild(2);
parent.insertData(itemData);
parent.connectChild(0,newNode);
parent.connectChild(1,node);
}
// 进行递归
connectNode(gParent,parent);
}
// 假如父节点为2结点
else{
if (data.key.compareTo(parent.getData(0).key)<0){
Node23 tempNode = parent.disconnectChild(1);
parent.connectChild(0,node.disconnectChild(0));
parent.connectChild(1,node.disconnectChild(1));
parent.connectChild(2,tempNode);
}else{
parent.connectChild(1,node.disconnectChild(0));
parent.connectChild(2,node.disconnectChild(1));
}
parent.insertData(node.getData(0));
}
}

2-3查找树的原理很简单,甚至说代码实现起来难度都不是很大,但是却很繁琐,因为它有很多种情况,而在红黑树中,用巧妙的方法使用了2个结点解决了3个结点的问题。

2-3 查找树及其Java实现的更多相关文章

  1. java实现二叉树查找树

    二叉树(binary)是一种特殊的树.二叉树的每个节点最多只能有2个子节点: 二叉树 由于二叉树的子节点数目确定,所以可以直接采用上图方式在内存中实现.每个节点有一个左子节点(left childre ...

  2. 手把手教你用java实现二分查找树及其相关操作

    二分查找树(Binary Search Tree)的基本操作有搜索.求最大值.求最小值.求前继.求后继.插入及删除. 对二分查找树的进行基本操作所花费的时间与树的高度成比例.例如有n个节点的完全二叉树 ...

  3. Java数据结构(十五)—— 多路查找树

    多路查找树 二叉树和B树 二叉树的问题分析 二叉树操作效率高 二叉树需要加载到内存,若二叉树的节点多存在如下问题: 问题1:构建二叉树时,需多次进行I/O操作,对与速度有影响 问题2:节点海量造成二叉 ...

  4. 数据结构:JAVA_二叉数查找树基本实现(中)

    数据结构:二叉数查找树基本实现(JAVA语言版) 1.写在前面 二叉查找树得以广泛应用的一个重要原因是它能保持键的有序性,因此我们可以把它作为实现有序符号表API中的众多方法的基础. 也就是说我们构建 ...

  5. 浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树

    http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的 ...

  6. Trie 树 及Java实现

    来源于英文“retrieval”.   Trie树就是字符树,其核心思想就是空间换时间. 举个简单的例子.   给你100000个长度不超过10的单词.对于每一个单词,我们要判断他出没出现过,如果出现 ...

  7. AVL树的JAVA实现及AVL树的旋转算法

    1,AVL树又称平衡二叉树,它首先是一颗二叉查找树,但在二叉查找树中,某个结点的左右子树高度之差的绝对值可能会超过1,称之为不平衡.而在平衡二叉树中,任何结点的左右子树高度之差的绝对值会小于等于 1. ...

  8. K:单词查找树(Trie)

      单词查找树,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串.Trie可以看作是一个确定有限状态自动机(DFA).与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中 ...

  9. 2-3-4树的java实现

    一.什么是2-3-4树 2-3-4树和红黑树一样,也是平衡树.只不过不是二叉树,它的子节点数目可以达到4个. 每个节点存储的数据项可以达到3个.名字中的2,3,4是指节点可能包含的子节点数目.具体而言 ...

随机推荐

  1. easyui前台改变datagrid某单元格的值

    有时候前台完成某个操作后要修改datagrid的值, 也许这个datagrid是没有保存的, 所以要修改后才能传递到后台; 也许要其他操作过后才需请求后台; 这些情况都需要前台对datagrid的单元 ...

  2. 【Leetcode_easy】859. Buddy Strings

    problem 859. Buddy Strings solution: class Solution { public: bool buddyStrings(string A, string B) ...

  3. 关于Js异常

    一.Javascript的异常处理机制 当javascript代码中出现错误的时候,js引擎就会根据js的调用栈逐级寻找对应的catch,如果没有找到相应的catch handler或catch ha ...

  4. VS混淆/反编译/远程调试/Spy++的Tools工具

    VS的Tools工具(混淆/反编译/远程调试/Spy++等) https://blog.csdn.net/chunyexiyu/article/details/14445605 参考:http://b ...

  5. 各种软件安装的URL

    Python爬虫动态抓取的工具 PhantomJS

  6. windows强大的快捷键

    1 电脑锁屏 有些时候,需要暂时离开座位去处理其他事,可是电脑还有数据再跑. 关掉的话,数据就白跑了,不关的话,又不想让别人看到我电脑的资料. 那么就按住windows键后,再按L键. 这样电脑就直接 ...

  7. Ubuntu16.04安装qt

    5.11官方下载网站: http://download.qt.io/official_releases/qt/5.11/5.11.1/ 可以直接下载linux系统下的.run安装包: 安装方式:htt ...

  8. 《Mysql - Count(*) 的优化》

    一:Count(*) 的实现方式? - 要明确的是,在不同的 MySQL 引擎中,count(*) 有不同的实现方式. - MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) ...

  9. Web基础和servlet基础

    TomCat的目录结构 Bin:脚本目录(存放启动.关闭这些命令) Conf:存放配置文件的目录 Lib:存放jar包 Logs: 存放日志文件 Temp: 临时文件 Webapps: 项目发布目录 ...

  10. PAT(B) 1088 三人行(Java)

    题目链接:1088 三人行 (20 point(s)) 参考博客:PAT (Basic Level) Practice (中文)1088 三人行 (20 分)(Java实现)吃口雪花 题目描述 子曰: ...