最近在閑看博客時看到一篇專門寫紅黑樹的實現原理,以Java的TreeMap為例講解,寫的很不錯,仔細看下來發現很多地方不是很理解,畢竟沒有對樹的理解并沒有很深,所以決定一步一步的將與樹相關的擴展實現都了解一遍,沿著下面的學習路線開始,大家也可以參考以下。
- 樹的基本知識
- 二叉樹的知識
- 二叉查找樹
- 平衡二叉樹
- 紅黑樹
- B樹,B-樹,B+樹
附上上面的將紅黑樹的blog:史上最清晰的紅黑樹講解
樹的基本概念
樹
Tree
是n(n≥0)個結點的有限集。在任意一棵非空樹中:(1)有且僅有一個特定的被稱為根root
的結點;(2)當n>1時,其余結點可分為m(m>0)個互不相交的有限集T1,T2,…,Tm,其中每一個集合本身又是一棵樹,并且稱為根的子樹SubTree
。
- 結點擁有的子樹數稱為結點的度(Degree)。度為0的結點稱為葉子(Leaf)或終端結點。度不為0的結點稱為非終端結點或分支結點。
- 樹的度是樹內各結點的度的最大值。
- 結點的子樹的根稱為該結點的孩子(Child),相應地,該結點稱為孩子的雙親(Parent)。
- 結點的層次(Level)是從根結點開始計算起,根為第一層,根的孩子為第二層,依次類推。樹中結點的最大層次稱為樹的深度(Depth)或高度。
- 如果將樹中結點的各子樹看成從左至右是有次序的(即不能互換),則稱該樹為有序樹,否則稱為無序樹。
二叉樹的基本概念
二叉樹(Binary Tree)的特點是每個結點至多具有兩棵子樹(即在二叉樹中不存在度大于2的結點),并且子樹之間有左右之分。
二叉樹的性質:
在二叉樹的第i層上至多有2i-1個結點(i≥1)。
深度為k的二叉樹至多有2k-1個結點(k≥1)。
對任何一棵二叉樹,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。
具有n個結點的完全二叉樹的深度為不大于log2n的最大整數加1。
-
如果對一棵有n個結點的完全二叉樹的結點按層序編號(從第1層到最后一層,每層從左到右),則對任一結點i(1≤i≤n),有
a、如果i=1,則結點i是二叉樹的根,無雙親;如果i>1,則其雙親是結點x(其中x是不大于i/2的最大整數)。
b、如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子是結點2i。
c、如果2i+1>n,則結點i無右孩子;否則其右孩子是結點2i+1。
二叉查找樹
二叉查找樹(BinarySearch Tree,也叫二叉搜索樹,或稱二叉排序樹Binary Sort Tree)或者是一棵空樹,或者是具有下列性質的二叉樹:
- 若它的左子樹不為空,則左子樹上所有結點的值均小于它的根結點的值;
- 若它的右子樹不為空,則右子樹上所有結點的值均大于它的根結點的值;
- 它的左、右子樹也分別為二叉查找樹。
結點定義:
public class TreeNode<T> {
/**
* 結點的值
*/
private T value;
/**
* 左結點
*/
private TreeNode<T> left;
/**
* 右結點
*/
private TreeNode<T> right;
/**
* 父親結點
*/
private TreeNode<T> parent;
/**
* 頻率
*/
private int freq;
}
插入
根據二叉查找樹的性質,插入一個節點的時候,如果根節點為空,就此節點作為根節點,如果根節點不為空,就要先和根節點比較,如果比根節點的值小,就插入到根節點的左子樹中,如果比根節點的值大就插入到根節點的右子樹中,如此遞歸下去,找到插入的位置。重復節點的插入用值域中的freq標記。如圖2是一個插入的過程。
二叉查找樹的時間復雜度要看這棵樹的形態,如果比較接近一一棵完全二叉樹,那么時間復雜度在O(logn)左右,如果遇到如圖3這樣的二叉樹的話,那么時間復雜度就會恢復到線性的O(n)了。
平衡二叉樹會很好的解決如圖3這種情況。
核心代碼如下:
private boolean insert(SearchNode<T> curr, SearchNode<T> insertNode, SearchNode<T> parent, boolean currIsLeft) {
if (curr == null) {
curr = insertNode;
if (currIsLeft) {
parent.setLeft(curr);
} else {
parent.setRight(curr);
}
} else {
int result = curr.getValue().compareTo(insertNode.getValue());
// 如果當前值大于插入的值
if (result > 0) {
return insert((SearchNode<T>)curr.getLeft(), insertNode, curr, true);
} else if (result < 0) {
return insert((SearchNode<T>)curr.getRight(), insertNode, curr, false);
}else {
curr.freq++;
}
}
return true;
}
查找
在二叉查找樹中查找x的過程如下:
- 若二叉樹是空樹,則查找失敗。
- 若x等于根結點的數據,則查找成功,否則。
- 若x小于根結點的數據,則遞歸查找其左子樹,否則。
- 遞歸查找其右子樹。
核心代碼如下:
protected TreeNode<T> find0(TreeNode<T> node, T value) {
if (node == null) {
return null;
}
int result = node.getValue().compareTo(value);
if (result > 0) {
return find0(node.getLeft(), value);
} else if (result < 0) {
return find0(node.getRight(), value);
}
return node;
}
刪除
刪除會麻煩一點,如果是葉子節點的話,直接刪除就可以了。如果只有一個孩子的話,就讓它的父親指向它的兒子,然后刪除這個節點。圖4顯示了一棵初始樹和4節點被刪除后的結果。先用一個臨時指針指向4節點,再讓4節點的地址指向它的孩子,這個時候2節點的右兒子就變成了3節點,最后刪除臨時節點指向的空間,也就是4節點。
刪除有兩個兒子的節點會比較復雜一些。一般的刪除策略是用其右子樹最小的數據代替該節點的數據并遞歸的刪除掉右子樹中最小數據的節點。因為右子樹中數據最小的節點肯定沒有左兒子,所以刪除的時候容易一些。圖5顯示了一棵初始樹和2節點被刪除后的結果。首先在2節點的右子樹中找到最小的節點3,然后把3的數據賦值給2節點,這個時候2節點的數據變為3,然后的工作就是刪除右子樹中的3節點了,采用遞歸刪除。
我們發現對2節點右子樹的查找進行了兩遍,第一遍找到最小節點并賦值,第二遍刪除這個最小的節點,這樣的效率并不是很高。你能不能寫出只查找一次就可以實現賦值和刪除兩個功能的函數呢?
如果刪除的次數不是很多的話,有一種刪除的方法會比較快一點,名字叫懶惰刪除法:當一個元素要被刪除時,它仍留在樹中,只是多了一個刪除的標記。這種方法的優點是刪除那一步的時間開銷就可以避免了,如果重新插入刪除的節點的話,插入時也避免了分配空間的時間開銷。缺點是樹的深度會增加,查找的時間復雜度會增加,插入的時間可能會增加。
核心代碼如下:
protected void deleteNode(TreeNode<T> deleteNodeParent, TreeNode<T> deleteNode) {
if (deleteNodeParent == null) {
// 左右子樹都為空
if (deleteNode.getLeft() == null && deleteNode.getRight() == null) {
root = null;
} else if (deleteNode.getLeft() == null || deleteNode.getRight() == null) {
// 存在左子樹或右子樹
if (deleteNode.getLeft() != null) {
root = deleteNode.getLeft();
} else {
root = deleteNode.getRight();
}
} else {
// 左右子樹都不為空
TreeNode<T> temp = deleteNode;
TreeNode<T> rightLeft = deleteNode.getRight();
while (rightLeft.getLeft() != null) {
temp = rightLeft;
rightLeft = rightLeft.getLeft();
}
if(temp == deleteNode) {
deleteNode.setRight(rightLeft.getRight());
}else {
temp.setLeft(rightLeft.getRight());
}
deleteNode.setValue(rightLeft.getValue());
}
} else {
// 左右子樹都為空
if (deleteNode.getLeft() == null && deleteNode.getRight() == null) {
// 根結點
if (deleteNodeParent.getLeft() == deleteNode) {
deleteNodeParent.setLeft(null);
} else {
deleteNodeParent.setRight(null);
}
} else if (deleteNode.getLeft() == null || deleteNode.getRight() == null) {
// 存在左子樹或右子樹
if (deleteNode.getLeft() != null) {
if (deleteNodeParent.getLeft() == deleteNode) {
// 如果待刪除結點是左結點,且其存在左結點
deleteNodeParent.setLeft(deleteNode.getLeft());
} else {
// 如果待刪除結點是左結點,且其存在右結點
deleteNodeParent.setRight(deleteNode.getLeft());
}
} else {
if (deleteNodeParent.getRight() == deleteNode) {
deleteNodeParent.setRight(deleteNode.getRight());
} else {
deleteNodeParent.setLeft(deleteNode.getRight());
}
}
} else {
// 左右子樹都不為空
TreeNode<T> temp = deleteNode;
TreeNode<T> rightLeft = deleteNode.getRight();
while (rightLeft.getLeft() != null) {
temp = rightLeft;
rightLeft = rightLeft.getLeft();
}
if(temp == deleteNode) {
deleteNode.setRight(rightLeft.getRight());
}else {
temp.setLeft(rightLeft.getRight());
}
deleteNode.setValue(rightLeft.getValue());
}
}
}
參考資料: