2-3 查找樹
定義(來源:wiki)
2–3樹是一種樹型數據結構,內部節點(存在子節點的節點)要么有2個孩子和1個數據元素,要么有3個孩子和2個數據元素,葉子節點沒有孩子,並且有1個或2個數據元素。


-
定義
如果一個內部節點擁有一個數據元素、兩個子節點,則此節點為2節點。
如果一個內部節點擁有兩個數據元素、三個子節點,則此節點為3節點。
當且僅當以下敘述中有一條成立時,T為2–3樹:
- T為空。即T不包含任何節點。
- T為擁有數據元素a的2節點。若T的左孩子為L、右孩子為R,則
- L和R是等高的非空2–3樹;
- a大於L中的所有數據元素;
- a小於等於R中的所有數據元素。
- T為擁有數據元素a和b的3節點,其中a < b。若T的左孩子為L、中孩子為M、右孩子為R,則
- L、M、和R是等高的非空2–3樹;
- a大於L中的所有數據元素,並且小於等於M中的所有數據元素;
- b大於M中的所有數據元素,並且小於等於R中的所有數據元素。
查找
首先我們說一下查找
2-3查找樹的查找和二叉樹很類似,無非就是進行比較然后選擇下一個查找的方向。 (這幾張圖不知道來源,知道的呲我一聲)

插入
2-3查找樹的插入
我們可以思考一下,為什么要兩個結點。在前面可以知道,二叉查找樹變成鏈表的原因就是因為新插入的結點沒有選擇的”權利”,當我們插入一個元素的時候,實際上它的位置已經確定了, 我們並不能對它進行操作。那么2-3查找樹是怎么做到賦予“權利”的呢?秘密便是這個多出來結點,他可以緩存新插入的結點。(具體我們將在插入的時候講)
前面我們知道,2-3查找樹分為2結點和3結點,so,插入就分為了2結點插入和3結點插入。
**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個結點的問題。