2–3樹是一種樹型數據結構,內部節點(存在子節點的節點)要麼有2個孩子和1個數據元素,要麼有3個孩子和2個數據元素,葉子節點沒有孩子,而且有1個或2個數據元素。html
定義java
若是一個內部節點擁有一個數據元素、兩個子節點,則此節點爲2節點。node
若是一個內部節點擁有兩個數據元素、三個子節點,則此節點爲3節點。算法
當且僅當如下敘述中有一條成立時,T爲2–3樹:緩存
首先咱們說一下查找數據結構
2-3查找樹的查找和二叉樹很相似,無非就是進行比較而後選擇下一個查找的方向。 (這幾張圖不知道來源,知道的呲我一聲)svg
2-3查找樹的插入函數
咱們能夠思考一下,爲何要兩個結點。在前面能夠知道,二叉查找樹變成鏈表的緣由就是由於新插入的結點沒有選擇的」權利」,當咱們插入一個元素的時候,實際上它的位置已經肯定了, 咱們並不能對它進行操做。那麼2-3查找樹是怎麼作到賦予「權利」的呢?祕密即是這個多出來結點,他能夠緩存新插入的結點。(具體咱們將在插入的時候講)this
前面咱們知道,2-3查找樹分爲2結點和3結點,so,插入就分爲了2結點插入和3結點插入。spa
**2-結點插入:**向2-結點插入一個新的結點和向而插入插入一個結點很相似,可是咱們並非將結點「吊」在結點的末尾,由於這樣就沒辦法保持樹的平衡。咱們能夠將2-結點替換成3-結點便可,將其中的鍵插入這個3-結點便可。(至關於緩存了這個結點)
3-結點插入: 3結點插入比較麻煩,emm能夠說是特別麻煩,它分爲3種狀況。
向一棵只含有3-結點的樹插入新鍵。
假如2-3樹只有一個3-結點,那麼當咱們插入一個新的結點的時候,咱們先假設結點變成了4-結點,而後使得中間的結點爲根結點,左邊的結點爲其左結點,右邊的結點爲其右結點,而後構成一棵2-3樹,樹的高度加1。
向父結點爲2-結點的3-結點中插入新鍵。
和上面的狀況相似,咱們將新的節點插入3-結點使之成爲4-結點,而後將結點中的中間結點」升「到其父節點(2-結點)中的合適的位置,使其父節點成爲一個3-節點,而後將左右節點分別掛在這個3-結點的恰當位置,樹的高度不發生改變
向父節點爲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個結點的問題。