樹結構是一類重要的非線性資料結構。直觀來看,樹是以分支關係定義的層次結構。樹結構在客觀世界廣泛存在,如人類社會的族譜和各種社會組織機構都可用樹來形象表示。
樹在計算機領域中也得到廣泛應用,尤以二叉樹最為常用。如在作業系統中,用樹來表示檔案目錄的組織結構。在編譯系統中,用樹來表示源程式的語法結構。在資料庫系統中,樹結構也是資訊的重要組織形式之一。
1、樹的定義
1.1、樹的定義
樹(Tree)是n(n>=0)個結點的有限集,它或為空樹(n= 0);,或為非空樹,對千非空樹T:
- (1) 有且僅有一個稱之為根的結點;
- (2) 除根結點以外的其餘結點可分為 m(m>0)個互不相交的有限集 T1, T2 , …,Tn,其中每
一個集合本身又是一棵樹,並且稱為根的子樹(SubTree)。
1.2、樹的相關術語
這裡結合圖1 (b)為例:
- 結點:樹中的一個獨立單元。包含一個資料元素及若於指向其子樹的分支。如圖 A 、 B 、 C 、 D 等。
- 結點的度:結點擁有的子樹數稱為結點的度。例如,A的度為 3, C的度為1, F的度為0。
- 樹的度:樹的度是樹內各結點度的最大值。圖 1 (b) 所示的樹的度為3。
- 葉子/終端結點: 度為 0 的結點稱為葉子或終端結點。結點 K 、 L 、 F 、 G 、 M 、 I 、 J都是樹的葉子。
- 非終端結點:度不為 0 的結點稱為非終端結點或分支結點。除根結點之外,非終端結點也稱為內部結點。
- 雙親和孩子:結點的子樹的根稱為該結點的孩子,相應地,該結點稱為孩子的雙親。例如,B的雙親為A, B的孩子有E和F。
- 兄弟:同一個雙親的孩子之間互稱兄弟。例如,H 、 I 和J互為兄弟。
- 祖先:從根到該結點所經分支上的所有結點。例如, M 的祖先為 A 、 D 和 H。
- 子孫:以某結點為根的子樹中的任一結點都稱為該結點的子孫。如 B 的子孫為 E 、 K 、 L和 F。
- 層次:結點的層次從根開始定義起,根為 第一層,根的孩子為第二層。樹中任一結點的層次等千其雙親結點的層次加 1。
- 堂兄弟:雙親在同 一層的結點互為堂兄弟。 例如,結點 G 與E 、 F、 H 、 I 、 J互為堂兄弟。
- 樹的深度:樹中結點的最大層次稱為樹的深度或高度。圖1 (b)所示的樹的深度為4。
- 有序樹和無序樹:如果將樹中結點的各子樹看成從左至右是有次序的(即不能互換),則稱該樹為有序樹,否則稱為無序樹。在有序樹中最左邊的子樹的根稱為第一個孩子,最右邊的稱為最後一個孩子。
- 森林:是 m (m>0)棵互不相交的樹的集合。對樹中每個結點而言,其子樹的集合即為森林。由此,也可以用森林和樹相互遞迴的定義來描述樹。
1.3、二叉樹的定義
二叉樹(Binary Tree)是n(n>=0)個結點所構成的集合,它或為空樹(n =0); 或為非空樹,對於非空樹T:
- (1) 有且僅有一個稱之為根的結點;
- (2) 除根結點以外的其餘結點分為兩個互不相交的子集T1和T2, 分別稱為T的左子樹和右子樹,且T1和T2本身又都是二叉樹。
二叉樹與樹一樣具有遞迴性質,二叉樹與樹的區別主要有以下兩點:
- (1) 二叉樹每個結點至多隻有兩棵子樹(即二叉樹中不存在度大千2 的結點);
- (2) 二叉樹的子樹有左右之分,其次序不能任意顛倒。
1.4、二叉樹的性質
二叉樹具有下列重要特性:
- 性質1 二叉樹第i(i≥1)層上的結點數最多為 2^i-1^。
- 性質2 高度為k的二叉樹最多有 2^k^-1 個結點。
- 性質3 對任何二叉樹T,設n~0~、n~1~、n~2~ 分別表示度數為 0、 1、 2 的結點個數, 則 n~0~=n~2~+1。
滿二叉樹和完全二叉樹:
滿二叉樹和完全二叉樹是二叉樹的兩種特殊情形。
一棵深度為 A 且有 2^k^ -1 個結點的二叉樹稱為滿二叉樹。
如圖 3 所示是深度分別為 1、 2、 3 的滿二叉樹。 滿二叉樹的特點是每一層上的結點數都達到最大值, 即對給定的深度, 它是具有最多結點數的二叉樹。 滿二叉樹不存在度數為 1 的結點, 每個分支結點均有兩棵高度相同的子樹, 且樹葉都在最下一層上。
若一棵二叉樹至多隻有最下面的兩層結點的度數可以小於 2, 並且最下一層上的結點都集中在該層最左邊的若干位置上, 則此二叉樹稱為完全二叉樹。如圖4所示:
由定義及示例可以看出滿二叉樹是完全二叉樹, 但完全二叉樹不一定是滿二叉樹。
- 性質4 具有 個結點的完全二叉樹(包括滿二叉樹)的高度為 [log~2~n]+1(或者[log~2~(n+1)])。
- 性質5 滿二叉樹原理非空滿二叉樹的葉結點數等於其分支結點數加1。
- 性質6 —棵非空二叉樹空子樹的數目等於其結點數目加 1。
2、二叉樹實現
2.1、二叉樹儲存結構
- 順序儲存
和線性表類似,二叉樹的儲存結構也可採用順序儲存和鏈式儲存兩種方式。
順序儲存是將二叉樹所有元素編號,存入到一維陣列的對應位置。順序儲存比較適合完全二叉樹,只要從根起按層序儲存即可,依次自上而下、自左至右儲存結點元素, 即將完全二叉樹上編號為i 的結點元素儲存在如 上定義的一維陣列中下標為i-1的陣列元素中。
對於非完全二叉樹,存在空間的浪費。
- 鏈式儲存
由於採用順序儲存結構儲存一般二叉樹造成大量儲存空間的浪費, 因此, 一般二叉樹的儲存結構更多地採用連結的方式。
在鏈式儲存結構裡,我們需要對節點進行定義,每個節點包含資料、左孩子、右孩子。
左孩子指向節點的左孩子,右節點指向節點的右孩子。
下面來看一看鏈式儲存結構的具體實現。
2.2、二叉樹鏈式儲存及常見操作實現
2.2.1、節點類
這裡新增了一個父節點的屬性,方便後面的一些操作
/**
* @Author 三分惡
* @Date 2020/10/8
* @Description 二叉樹節點
*/
public class BinaryTreeNode {
private Object data; //資料
private BinaryTreeNode leftChild; //左孩子
private BinaryTreeNode rightChild; //右孩子
private BinaryTreeNode parent; //父節點
//省略getter、setter
/**
* 重寫equals方法,這裡設定為資料相等即認為是同一節點(這個規則不合理,待改進)
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
//比較物件為BinaryTreeNode類例項
if (obj instanceof BinaryTreeNode){
BinaryTreeNode compareNode= (BinaryTreeNode) obj;
//設定為資料相同即相同
if (compareNode.getData().equals(this.getData())){
return true;
}
}
return false;
}
}
2.2.2、建立
/**
* @Author 三分惡
* @Date 2020/10/8
* @Description 二叉樹-鏈式
*/
public class BinaryTree {
private BinaryTreeNode root; //根節點
public BinaryTree() {
}
public BinaryTree(BinaryTreeNode root) {
this.root = root;
}
public BinaryTreeNode getRoot() {
return root;
}
public void setRoot(BinaryTreeNode root) {
this.root = root;
}
}
2.2.2、清空
/**
* 二叉樹的清空
* 遞迴清空某個節點的子樹
* @param node
*/
public void clear(BinaryTreeNode node){
if (node!=null){
//遞迴清空左子樹
clear(node.getLeftChild());
//遞迴清空右子樹
clear(node.getRightChild());
//將該節點置為null
node=null;
}
}
/**
* 清空二叉樹
*/
public void clear(){
clear(root);
}
2.2.3、判空
判斷根節點是否存在。
/**
* 判斷二叉樹是否為空
* @return
*/
public boolean isEmpty(){
return root==null;
}
2.2.4、獲取二叉樹的高度
首先需要一種獲取以某個節點為子樹的高度方法,使用遞迴實現。如果一個節點為空,那麼這個節點肯定是一顆空樹,高度為0;如果不為空,則遍歷地比較它的左右子樹高度,高的一個為這顆子樹的最大高度,然後加上自身的高度即可。
/**
* 獲取指定節點的子樹的高度
* @param node
* @return
*/
public int height(BinaryTreeNode node){
if (node==null){
return 0;
}
//遞迴獲取左子樹高度
int l=height(node.getLeftChild());
//遞迴獲取右子樹高度
int r=height(node.getRightChild());
//子樹高度+1,因為還有節點這一層
return l>=r? (l=1):(r=1);
}
/**
* 獲取二叉樹的高度
* @return
*/
public int height(){
return height(root);
}
2.2.5、獲取節點個數
獲取二叉樹節點數,需要獲取以某個節點為根的子樹的節點數實現。
如果節點為空,則個數肯定為0;如果不為空,則算上這個節點之後,繼續遞迴計算所有子樹的節點數,全部相加即可
/**
* 獲取某個節點及子樹的節點個數
* @param node
* @return
*/
public int size(BinaryTreeNode node){
if (node==null){
return 0;
}
//遞迴獲取左子樹節點數
int l=size(node.getLeftChild());
//遞迴獲取右子樹節點數
int r=size(node.getRightChild());
return l+r+1;
}
/**
* 獲取二叉樹節點個數
* @return
*/
public int size(){
return size(root);
}
2.2.6、獲取末端葉子節點
通過遞迴左右子樹,獲得左右子樹末端的葉子節點。
/**
* 獲取節點左子樹的末端節葉子點
* @param node
* @return
*/
public BinaryTreeNode getLeftLeaf(BinaryTreeNode node){
if (node==null){
return null;
}
if (node.getLeftChild()==null){
return node;
}else{
node=getLeftLeaf(node.getLeftChild());
}
return node;
}
/**
* 獲取整個二叉樹左子樹最末端的葉子節點
* @return
*/
public BinaryTreeNode getLeftLeaf(){
return getLeftLeaf(root);
}
/**
* 獲取某個節點右子樹最末端的葉子節點
* @param node
* @return
*/
public BinaryTreeNode getRightLeaf(BinaryTreeNode node){
if (node==null){
return null;
}
if (node.getRightChild()==null){
return node;
}else{
node=getRightLeaf(node.getRightChild());
}
return node;
}
/**
* 獲取整個二叉樹右子樹最末端葉子節點
* @return
*/
public BinaryTreeNode getRightLeaf(){
return getRightLeaf(root);
}
2.2.7、插入
這裡只是實現了給節點插入左孩子、右孩子,只考慮了插入的節點的左右孩子不存在的情況。
/**
* 給某個節點插入左孩子
* @param parent
* @param newNode
*/
public void insertLeft(BinaryTreeNode parent,BinaryTreeNode newNode){
parent.setLeftChild(newNode);
newNode.setParent(parent);
}
/**
* 給某個節點插入右孩子
* @param parent
* @param newNode
*/
public void insertRitht(BinaryTreeNode parent,BinaryTreeNode newNode){
parent.setRightChild(newNode);
newNode.setParent(parent);
}
2.2.8、刪除
從二叉樹中刪除節點,稍微複雜一些,需要考慮三種情形。
- 被刪除的節點左右子樹均為空
這是最簡單的一種情形,只需要把被被刪除節點的父親節點的孩子節點指向null即可。
- 被刪除的節點左右子樹有一個為空
這種情形,只需要將被刪除元素的左子樹的父節點移動到被刪除元素的父節點,然後將被刪除元素移除即可。
- 被刪除的節點左右子樹均不為空
這是最複雜的一種情形。這裡博主偷了一個懶,選擇了填坑的方式,將需要刪除的節點刪除,挖了一個坑,找到二叉樹左子樹葉子節點,把這個節點給填進去。
具體程式碼實現:
/**
* 刪除節點
* @param subNode 遍歷的節點
* @param node 待刪除節點
* @return
*/
public BinaryTreeNode deleteNode(BinaryTreeNode subNode,BinaryTreeNode node){
if (subNode==null){
return null;
}
//父節點
BinaryTreeNode parent=null;
if (subNode.equals(node)){
parent=node.getParent();
//情形1、當前節點沒有孩子節點
if (subNode.getLeftChild()==null&&subNode.getRightChild()==null){
//刪除父節點和當前節點的關聯
this.changeChild(parent,subNode,null);
//情形2、當前節點只有左節點或右節點
} else if (subNode.getLeftChild()==null){ //情形2.1只有右孩子節點
//將父節點孩子節點設定為當前節點的右孩子
this.changeChild(parent,subNode,subNode.getRightChild());
}else if (subNode.getRightChild()==null){ //情形2,2只有左孩子節點
//將父節點孩子節點設定為當前節點的左孩子
this.changeChild(parent,subNode,subNode.getLeftChild());
}else{ //情形3、左右孩子節點都有
//左子樹末端葉子節點
BinaryTreeNode leftLeaf=getLeftLeaf(subNode);
//將父節點孩子節點設定為末端葉子節點
this.changeChild(parent,subNode,leftLeaf);
//將葉子節點父節點子節點置為null
this.changeChild(leftLeaf.getParent(),leftLeaf,null);
//葉子節點父節點
leftLeaf.setParent(parent);
//被刪除的節點置為null,幫助gc
subNode=null;
}
}
//遞迴左子樹
if (deleteNode(subNode.getLeftChild(),node)!=null){
deleteNode(subNode.getLeftChild(),node);
}else {
//遞迴右子樹
deleteNode(subNode.getRightChild(),node);
}
return subNode;
}
/**
* 替換父親節點的孩子節點
* @param parent 父親節點
* @param replacedNode 被替換的節點
* @param aimNode 替換的節點
*/
void changeChild(BinaryTreeNode parent,BinaryTreeNode replacedNode,BinaryTreeNode aimNode){
//被替換節點是左孩子
if (replacedNode==parent.getLeftChild()){
parent.setLeftChild(aimNode);
}else{
//被替換節點是右孩子
parent.setRightChild(aimNode);
}
}
2.3、遍歷二叉樹
常見的二叉樹遍歷方法有前序、中序、後序、層次等。
2.3.1、前序遍歷
前序遍歷(Preorder Traversal)是先遍歷根結點, 再遍歷左子樹, 最後才遍歷右子樹。 及時二叉樹非空, 則依次進行如下操作:
- 訪問根節點
- 前序遍歷左子樹
- 前序遍歷右子樹
/**
* 從某個節點開始先序遍歷子樹
* @param node
*/
public void preOrder(BinaryTreeNode node){
if (node!=null){
//遍歷根節點
System.out.println(node.getData());
//遍歷左子樹
preOrder(node.getLeftChild());
//遍歷右子樹
preOrder(node.getRightChild());
}
}
/**
* 先序遍歷整個二叉樹
*/
public void preOrder(){
preOrder(root);
}
2.3.2、中序遍歷
中序遍歷(Inorder Traversal)是先遍歷左子樹, 再遍歷根結點, 最後才遍歷右子樹。 即若二叉樹非空, 則依次進行如下操作:
- 中序遍歷左子樹;
- 訪問根結點;
- 中序遍歷右子樹。
/**
* 從某個節點開始中序遍歷子樹
* @param node
*/
public void inOrder(BinaryTreeNode node){
if (node!=null){
//中序遍歷左子樹
inOrder(node.getLeftChild());
//訪問根節點
System.out.println(node.getData());
//中序遍歷右子樹
inOrder(node.getRightChild());
}
}
/**
* 中序遍歷整個二叉樹
*/
public void inOrder(){
inOrder(root);
}
2.3.3、後序遍歷
後序遍歷(Postorder Traversal)是先遍歷左子樹, 再遍歷右子樹, 最後才遍歷根結點。即若二叉樹非空, 則依次進行如下操作:
- 後序遍歷左子樹;
- 後序遍歷右子樹;
- 訪問根結點。
/**
* 從某個節點開始後序遍歷子樹
* @param node
*/
public void postOrder(BinaryTreeNode node){
if (node!=null){
//後序遍歷左子樹
preOrder(node.getLeftChild());
//後序遍歷右子樹
preOrder(node.getRightChild());
//訪問根節點
System.out.println(node.getData());
}
}
/**
* 後序遍歷整個二叉樹
*/
public void postOrder(){
preOrder(root);
}
由二叉樹的先序序列和中序序列,或由其後序序列和中序序列均能唯一地確定一棵二叉樹。
2.3.4、層次遍歷
層次遍歷是指從二叉樹的第一層(根結點)開始, 從上至下逐層遍歷, 在同一層中, 則按從左到右的順序對結點逐個訪問。
由層次遍歷的操作可以推知, 在進行層次遍歷時, 對一層結點訪問完後, 再按照它們的訪問次序對各個結點的左孩子和右孩子順序訪問, 就完成了對下一層從左到右的訪問。
因此, 在進行層次遍歷時, 需設定一個佇列結構, 遍歷從二叉樹的根結點開始, 首先將根結點指標入隊, 然後從隊頭取出一個元素, 每取出一個元素, 執行兩個操作: 訪問該元素所指結點; 若該元素所指結點的左、 右孩子結點非空, 則將該元素所指結點的左孩子指標和右孩子指標順序入隊。此過程迴圈進行, 直至佇列為空, 表示二叉樹的層次遍歷結束。
所以, 對一棵非空的二叉樹進行層次遍歷可按照如下步驟進行:
- (1) 初始化一個佇列;
- (2) 二叉樹的根結點放入佇列;
- (3) 重複步驟(4)~(7)直至佇列為空;
- (4) 從佇列中取出一個結點 x;
- (5) 訪問結點x;
- (6) 如果 x 存在左子結點, 將左子結點放入佇列;
- (7) 如果 x 存在右子結點, 將右子結點放入佇列。
3、線索二叉樹
在上面我們瞭解了二叉樹的常見遍歷方法,接下來看一看二叉樹的線索化。
3.1、二叉樹的線索化
線上性結構中, 各結點的邏輯關係是順序的, 尋找某一結點的前趨結點和後繼結點很方便。 對於二叉樹, 由於它是非線性結構, 所以樹中的結點不存在前趨和後繼的概念, 但當我們對二叉樹以某種方式遍歷後, 就可以得到二叉樹中所有結點的一個線性序列, 在這種意義下, 二叉樹中的結點就有了前趨結點和後繼結點。
二叉樹通常採用二叉連結串列作為儲存結構, 在這種儲存結構下, 由於每個結點有兩個分別指向其左兒子和右兒子的指標, 所以尋找其左、 右兒子結點很方便, 但要找該結點的前趨結點和後繼結點則比較困難。
為方便尋找二叉樹中結點的前趨結點或後繼結點, 可以通過一次遍歷記下各結點在遍歷所得的線性序列中的相對位置。 儲存這種資訊的一種簡單的方法是在每個結點增加兩個指標域, 使它們分別指向依某種次序遍歷時所得到的該結點的前趨結點和後繼結點, 顯然這樣做要浪費相當數量的儲存單元。
如果仔細分析一棵具有 n 個結點的二叉樹, 就會發現,當它採用二叉連結串列作儲存結構時, 二叉樹中的所有結點共有n+1 個空指標域。 因此, 可以設法利用這些空指標碎來存放結點的前趨結點和後繼結點的指標資訊, 這種附加的指標稱為“ 線索” 。 我們可以作這樣的規定, 當某結點的左指標域為空時, 令其指向依某種方式遍歷時所得到的該結點的前趨結點, 否則指向它的左兒子; 當某結點的右指標域為空時,令其指向依某種方式遍歷時所得到的該結點的後繼結點, 否則指向它的右兒子。
增加了線索的二叉連結串列稱為線索連結串列, 相應的二叉樹稱為線索二叉樹(Threaded Binary Tree)。
為了區分一個結點的指標是指向其兒子的指標還是指向其前趨或者後繼的線索, 可以在每個結點上增加兩個線索標誌域 leftType 和 rightType, 這樣線索連結串列的結點結構為:
一棵二叉樹以某種方式遍歷並使其變成線索二叉樹的過程稱為二叉樹的線索化。對同一棵二叉樹遍歷的方式不同, 所得到的線索樹也不同, 二叉樹主要有前序、 中序和後序 3 種遍歷方式, 所以線索樹也有前序線索二叉樹、 中序線索二叉樹和後序線索二叉樹3種。
3.2、中序線索二叉樹的實現
這一節來實現中序遍歷線索二叉樹。
3.1.1、線索二叉樹節點
/**
* @Author 三分惡
* @Date 2020/10/11
* @Description 線索二叉樹節點
*/
public class ClueBinaryTreeNode {
//節點資料
int data;
//左兒子
ClueBinaryTreeNode leftNode;
//右兒子
ClueBinaryTreeNode rightNode;
//標識指標型別,其中0,1分別表示有無線索化,預設為0
int leftType;
int rightType;
}
3.1.2、建立中序線索二叉樹
建立線索二叉樹, 或者說, 對二叉樹線索化, 實質上就是遍歷一棵二叉樹, 在遍歷的過程中, 檢査當前結點的左、 右指標域是否為空, 如果為空, 將它們改為指向前趨結點或後繼結點的線索。
以圖12的二叉樹為例:
-
定義一個節點pre用來儲存當前節點,類似指標。
-
從根節點1開始遞迴,如果當前節點為空,返回;遍歷到4,此時4的前驅結點為null,結點5的前驅結點為2
-
遍歷到5的時候指向前驅結點2,前驅結點2為上一層遞迴的指標,因此指向它的前驅結點就行,再把左指標型別置為1
-
如果當前節點的前驅結點pre的右指標為null,則將它設定為當前節點,此時即4的後繼結點為2,並將右指標型別置為1
-
每處理一個節點,當前節點是下一個節點的前驅節點
來看一下具體實現:
/**
* @Author 三分惡
* @Date 2020/10/11
* @Description 中序線索二叉樹
*/
public class ClueBinaryTree {
private ClueBinaryTreeNode root; //根節點
private ClueBinaryTreeNode pre; //每個節點的前趨節點
public ClueBinaryTreeNode getRoot() {
return root;
}
public void setRoot(ClueBinaryTreeNode root) {
this.root = root;
}
/**
* 構建中序線索二叉樹
*/
public void clueBinaryNodes() {
clueBinaryNodes(root);
}
/**
* 構建中序線索二叉樹
* @param node 起始節點
*/
public void clueBinaryNodes(ClueBinaryTreeNode node) {
//當前節點如果為null,直接返回
if(node==null) {
return;
}
//遞迴處理左子樹
clueBinaryNodes(node.leftNode);
//處理前驅節點
if(node.leftNode==null){
//讓當前節點的左指標指向前驅節點
node.leftNode=pre;
//改變當前節點左指標的型別
node.leftType=1;
}
//處理前驅的右指標,如果前驅節點的右指標是null(沒有指下右子樹)
if(pre!=null&&pre.rightNode==null) {
//讓前驅節點的右指標指向當前節點
pre.rightNode=node;
//改變前驅節點的右指標型別
pre.rightType=1;
}
//每處理一個節點,當前節點是下一個節點的前驅節點
pre=node;
//處理右子樹
clueBinaryNodes(node.rightNode);
}
}
4、二叉查詢樹
二叉樹的一個重要作用是用作查詢。
4.1、二叉查詢樹的概念和操作
二叉查詢樹定義:
又稱為是二叉排序樹(Binary Sort Tree)或二叉搜尋樹。二叉排序樹或者是一棵空樹,或者是具有下列性質的二叉樹:
- 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
- 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;
- 左、右子樹也分別為二叉排序樹;
- 沒有鍵值相等的節點。
二叉查詢樹的高度決定了二叉查詢樹的查詢效率。
和普通的二叉樹相比,二叉查詢樹的節點是有序的。
二叉查詢樹的插入過程如下:
-
若當前的二叉查詢樹為空,則插入的元素為根節點;
-
若插入的元素值小於根節點值,則將元素插入到左子樹中;
-
若插入的元素值不小於根節點值,則將元素插入到右子樹中。
4.1、二叉查詢樹的實現
- 節點類:因為要比較節點大小,所以繼承Comparable類
/**
* 二叉查詢樹節點
*
* @param <T>
*/
class BSTNode<T extends Comparable<T>> {
T key; // 關鍵字(鍵值)
BSTNode<T> left; // 左孩子
BSTNode<T> right; // 右孩子
BSTNode<T> parent; // 父結點
//省略構造方法、getter、setter
}
- 插入:插入需要比較插入節點和當前節點的大小
/**
* 將結點插入到二叉樹中
*
* @param bst 二叉樹
* @param z 插入的節點
*/
private void insert(BSTree<T> bst, BSTNode<T> z) {
int cmp;
BSTNode<T> y = null;
BSTNode<T> x = bst.mRoot;
// 查詢z的插入位置
while (x != null) {
y = x;
//與當前節點比較
cmp = z.key.compareTo(x.key);
//比當前節點小,插入為左孩子
if (cmp < 0) {
x = x.left;
} else {
//比當前節點大,插入為右孩子
x = x.right;
}
}
z.parent = y;
if (y == null)
bst.mRoot = z;
else {
cmp = z.key.compareTo(y.key);
if (cmp < 0) {
y.left = z;
} else {
y.right = z;
}
}
}
/**
* 新建結點(key),並將其插入到二叉樹中
* @param key 插入結點的鍵值
*/
public void insert(T key) {
BSTNode<T> z = new BSTNode<T>(key, null, null, null);
//插入新節點
if (z != null) {
insert(this, z);
}
}
- 查詢:查詢的節點比當前節點大就去查詢右子樹,比當前節點小就去查詢左子樹。
/**
* (遞迴實現)查詢"二叉樹x"中鍵值為key的節點
* @param x
* @param key
* @return
*/
private BSTNode<T> search(BSTNode<T> x, T key) {
if (x == null) {
return x;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return search(x.left, key);
} else if (cmp > 0) {
return search(x.right, key);
} else {
return x;
}
}
public BSTNode<T> search(T key) {
return search(mRoot, key);
}
/**
* (非遞迴實現)查詢"二叉樹x"中鍵值為key的節點
* @param x
* @param key
* @return
*/
private BSTNode<T> iterativeSearch(BSTNode<T> x, T key) {
while (x != null) {
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x = x.left;
} else if (cmp > 0) {
x = x.right;
} else {
return x;
}
}
return x;
}
public BSTNode<T> iterativeSearch(T key) {
return iterativeSearch(mRoot, key);
}
其餘操作遍歷,刪除,清空等這裡不再贅言。
5、平衡二叉樹
二叉查詢樹查詢演算法的效能取決於二叉樹的結構,而 二叉查詢樹的形狀則取決於其資料集。
如果資料呈有序排列,則二叉排序樹是線性的,查詢的時間複雜度為O(n); 反之,如果二叉排序樹的結構合理,則查詢速度較快,查詢的時間複雜度為 O(log~2~n)。
樹的高度越小,查詢速度越快——從樹的形態來看,就是使樹儘可能平衡。
有資料將平衡二叉樹和AVL視作一體,本文采用了AVL樹是平衡二叉樹的一種的說法。
5.1、AVL樹
AVL樹是最先發明的自平衡二叉查詢樹。AVL樹得名於它的發明者 G.M. Adelson-Velsky 和 E.M. Landis,他們在 1962 年的論文 "An algorithm for the organization of information" 中發表了它。
AVL樹是帶平衡條件的二叉查詢樹:
- (1 ) 左子樹和右子樹的深度之差的絕對值不超過1;
- (2) 左子樹和右子樹也是平衡二叉樹。
若將二叉樹上結點的平衡因子(Balance Factor, BF)定義為該結點左子樹和右子樹的深度之
差,則平衡二叉樹上所有結點的平衡因子只可能是 -1、0和1。只要二叉樹上有一個結點的平衡因子的絕對值大於1 , 則該二叉樹就是不平衡的。
在AVL中任何節點的兩個兒子子樹的高度最大差別為1,所以它也被稱為高度平衡樹,n個結點的AVL樹最大深度約1.44log2n。得益於這個特徵,它的深度和 log~2~n 是同數量級的(其中n為結點數)。 由此,其查詢的時間複雜度是O(log~2~n)。
5.1.2、AVL樹的平衡調整方法
插入結點時, 首先按照二叉排序樹處理, 若插入結點後破壞了平衡二叉樹的特性, 需對平衡二叉樹進行調整。 調整方法是:找到離插入結點最近且平衡因子絕對值超過1的祖先結點, 以該結點為根的子樹稱為最小不平衡子樹, 可將重新平衡的範圍侷限千這棵子樹。
在平衡調整的過程中,有一個關鍵點是旋轉。
這裡有一個具體例子:
- (1) 空樹和1個結點⑬的樹顯然都是平衡的二叉樹。在插入24之後仍是平衡的, 只是根結點的平衡因子BF由0變為-1, 如圖18(a) -(c)所示。
- (2) 在繼續插入37之後, 由千結點 ⑬ 的BF值由 -1 變成 -2, 由此出現了不平衡的現象。此時好比一根扁擔出現一頭重一頭輕的現象, 若能將扁擔的支撐點由 ⑬ 改至 ㉔ , 扁擔的兩頭就平衡了。此,可以對樹做一個向左逆時針 " 旋轉 " 的操作,令結點 ㉔為根,而結點 ⑬ 為它的左子樹,此時,結點⑬ 和 ㉔ 的平衡因子都為0, 而且仍保持二叉排序樹的特性,如圖18(d)~ (e)所示。
- (3) 在繼續插入90和53之後,結點 ㊲ 的BF值由-1變成-2, 排序樹中出現了新的不平衡現象,需進行調整。但此時由於是結點邸插在結點 (90) 的左子樹上,因此不能如上做簡單調整。離插入結點最近的最小不平衡子樹是以結點 ㊲為根的子樹。這時,必須以 (53) 作為根結點,而使 ㊲ 成為它的左子樹的根,(90) 成為它的右子樹的根。這好比對樹做了兩次 “旋轉” 操作,先向右順時針旋轉,後向左逆時針旋轉(見圖18 (f)~(h)), 使二叉排序樹由不平衡轉化為平衡。
一般情況下,假設最小不平衡子樹的根結點為 A, 則失去平衡後進行調整的規律可歸納為下列4種情況。
- (1) LL 型:由於在 A 左子樹根結點的左子樹上插入結點,A 的平衡因子由 1 增至 2, 致使以A為根的子樹失去平衡,則需進行一次向右的順時針旋轉操作,如圖21所示。
圖22所示為兩個LL型調整的例項。
- (2) RR 型:由千在 A 的右子樹根結點的右子樹上插入結點, A 的平衡因子由 -1 變為 -2,致使以 A 為根結點的子樹失去平衡,則需進行一次向左的逆時針旋轉操作,如圖23所示。
圖24所示為兩個RR型調整的例項。
- (3) LR型:由千在A的左子樹根結點的右子樹上插入結點, A的平衡因子由1增至2,致使以A為根結點的子樹失去平衡, 則需進行兩次旋轉操作。 第一次對B及其右子樹進行逆時針旋轉, C轉上去成為B的根, 這時變成了LL型, 所以第二次進行LL型的順時針旋轉即可恢復平衡。 如果C原來有左子樹, 則洞整C的左子樹為B的右子樹, 如圖25所示。
LR型旋轉前後A、 B、C三個結點平衡因子的變化分為3種情況, 圖 26 所示為3種 LR型調整的例項。
- (4) RL 型:由千在 A 的右子樹根結點的左子樹上插入結點, A 的平衡因子由 -1 變為-2,致使以 A 為根結點的子樹失去平衡, 則旋轉方法和 LR 型相對稱, 也需進行兩次旋轉, 先順時針右旋, 再逆時針左旋, 如圖 27 所示。
同 LR 型旋轉類似, RL 型旋轉前後 A 、 B 、 C 三個結點的平衡因子的變化也分為 3 種情況,圖 28 所示為 3 種 RL 型調整的例項。
上述 4 種情況中,(1) 和 (2) 對稱,進行的是單旋轉的操作;(3) 和 (4) 對稱,進行的是雙旋轉的操作。
旋轉操作的正確性容易由 “保持二叉排序樹的特性:中序遍歷所得關鍵字序列自小至大有序” 證明之。 同時, 無論哪一種情況, 在經過平衡旋轉處理之後,以 B 或 C 為根的新子樹為平衡二叉樹,而且它們的深度和插入之前以 A為根的子樹相同。
因此, 當平衡的二叉排序樹因插入結點而失去平衡時, 僅需對最小不平衡子樹進行平衡旋轉處理即可。 因為經過旋轉處理之後的子樹深度和插入之前相同,因而不影響插入路徑上所有祖先結點的平衡度。
5.1.3、AVL樹的插入
在平衡的二叉排序樹BBST上插入一個新的資料元素e的遞迴演算法可描述如下。
在上面我們看到插入節點,如果破壞了AVL樹的平衡,則需要進行旋轉,即上面的四種情況:
- LL
執行一次右旋轉
- RR
執行一次左旋轉
-
LR
先左旋,後右旋 -
RL
先右旋後左旋
5.1.3、AVL樹刪除
前面已經看過二叉樹的刪除操作,AVL樹的刪除操作同樣分為三種情況:
- 刪除節點為葉子節點
- 刪除節點有左子樹或右子樹
- 刪除節點有左子樹和右子樹
只不過 AVL 樹在刪除節點後需要重新檢查平衡性並修正,同時,刪除操作與插入操作後的平衡修正區別在於,插入操作後只需要對插入棧中的彈出的第一個非平衡節點進行修正,而刪除操作需要修正棧中的所有非平衡節點。
具體程式碼實現:
public class AVLBinaryTree {
public int size;
//節點
class Node{
public int val;
public Node left,right;
public int height;
public Node(int val){
this.val=val;
left=null;
right=null;
height=1;
}
}
//新增一個節點
public Node add(Node node,int val){
if (node==null){
size++;
return new Node(val);
}
if (node.val<val) node.right=add(node.right,val);
if (node.val>val) node.left=add(node.left,val);
//更新高度
node.height=Math.max(getHeight(node.left),getHeight(node.right))+1;
//計算平衡因子
int balanceFactor=getBlalanceFactor(node);
//維護平衡
//LL
if (balanceFactor>1&&getBlalanceFactor(node.left)>=0){
return rightRotate(node);
}
//RR
if (balanceFactor<-1&&getBlalanceFactor(node.right)<=0){
return leftRotate(node);
}
//LR
if (balanceFactor>1&&getBlalanceFactor(node.left)<0){
node.left=leftRotate(node.left);
return rightRotate(node);
}
//RL
if (balanceFactor<-1&&getBlalanceFactor(node.right)>0){
node.right=rightRotate(node.right);
return leftRotate(node);
}
return node;
}
/**
* 對根節點x進行向左旋轉操作,更新height後返回新的根節點y
* @param x
* @return
*/
public Node leftRotate(Node x){
Node y=x.right;
Node T3=y.left;
y.left=x;
x.right=T3;
//更新height
x.height=Math.max(getHeight(x.left),getHeight(x.right))+1;
y.height=Math.max(getHeight(y.left),getHeight(y.right))+1;
return y;
}
/**
* 對根節點進行右旋轉操作,更新height後返回新的根節點y
* @param x
* @return
*/
public Node rightRotate(Node x){
Node y=x.left;
Node T3=y.right;
y.right=x;
x.left=T3;
//更新height
x.height=Math.max(getHeight(x.left),getHeight(x.right))+1;
y.height=Math.max(getHeight(y.left),getHeight(y.right))+1;
return y;
}
//獲得節點Node的高度
public int getHeight(Node node){
if (node==null){
return 0;
}
return node.height;
}
//獲取節點的平衡因子
private int getBlalanceFactor(Node node){
if (node==null){
return 0;
}
return getHeight(node.left)-getHeight(node.right);
}
/**
* 刪除節點
* @param node
* @param val
* @return
*/
public Node remove(Node node,int val){
if (node==null) return null;
Node retNode;
//遞迴查詢要刪除的節點
if (node.val<val){
node.left=remove(node.left,val);
retNode=node;
}else if(node.val>val){
node.right=remove(node.right,val);
retNode=node;
}else{
//找到了要刪除的節點
//情形1:被刪除節點為葉子節點
if (node.right==null){
Node leftNode=node.left;
node.left=null;
size--;
retNode=leftNode;
}
//情形2.1:被刪除節點只有右孩子
if (node.left==null){
Node leftNode=node.left;
node.left=null;
size--;
retNode=leftNode;
}
//情形2.2:被刪除節點只有左孩子
if (node.right==null){
Node rightNode=node.right;
node.right=null;
size--;
retNode=rightNode;
}else{
//情形3:被刪除節點有左、右孩子
Node minNode=minimum(node);
minNode.right=remove(node.right,minNode.val);
node.left=node.right=null;
retNode=minNode;
}
}
if (retNode==null) return retNode;
//刪除完成,開始進行二叉樹的平衡
//更新高度
retNode.height= Math.max(getHeight(retNode.left),getHeight(retNode.right)+1);
//計算平衡因子
int balanceFactor=getBlalanceFactor(retNode);
//維護平衡
//維護平衡
//LL
if (balanceFactor>1&&getBlalanceFactor(retNode.left)>=0){
return rightRotate(retNode);
}
//RR
if (balanceFactor<-1&&getBlalanceFactor(retNode.right)<=0){
return leftRotate(retNode);
}
//LR
if (balanceFactor>1&&getBlalanceFactor(retNode.left)<0){
retNode.left=leftRotate(retNode.left);
return rightRotate(retNode);
}
//RL
if (balanceFactor<-1&&getBlalanceFactor(retNode.right)>0){
retNode.right=rightRotate(retNode.right);
return leftRotate(retNode);
}
return retNode;
}
//獲取該節點的整個子樹的最小值
public Node minimum(Node node){
if (node.left==null){
return node;
}
return minimum(node.left);
}
}
5.2、紅黑樹
紅黑樹是一種常見的自平衡二叉查詢樹,常用於關聯陣列、字典,在各種語言的底層實現中被廣泛應用,Java 的 TreeMap 和 TreeSet 就是基於紅黑樹實現的。
5.2.1、紅黑樹的定義和性質
紅黑樹::紅黑樹是一種自平衡二叉查詢樹,是在電腦科學中用到的一種資料結構,典型的用途是實現關聯陣列。它是在1972年由魯道夫·貝爾發明的,稱之為"對稱二叉B樹",它現在的名字來自Leo J. Guibas 和 Robert Sedgewick 於1978年寫的一篇論文中。
紅黑樹是具有如下性質的二叉查詢樹:
-
(1)每個節點是黑色或者紅色
-
(2)根節點是黑色。
-
(3)每個葉子節點是黑色。
-
(4)從任意一個節點到葉子節點,所經過的黑色節點數目必須相等
-
(5) 空節點被認為是黑色的
5.2.2、紅黑樹的平衡調整方法
作為一種平衡二叉樹,紅黑樹的自平衡調整方法和AVL類似。關鍵也是在旋轉。旋轉同樣也是左旋和右旋。找到了兩個左旋和右旋的動圖。
和AVL樹不同的是,紅黑樹還有顏色性質,所以還會進行變色來平衡紅黑樹。
5.2.2、紅黑樹的插入
紅黑樹的插入和AVL樹類似,同樣是插入節點後需要對二叉樹的平衡性進行修復。
新插入的節點是紅色的,插入修復操作如果遇到父節點的顏色為黑則修復操作結束。也就是說,只有在父節點為紅色節點的時候是需要插入修復操作的。
插入修復操作分為以下的三種情況,而且新插入的節點的父節點都是紅色的:
- 叔叔節點也為紅色。
- 叔叔節點為空,且祖父節點、父節點和新節點處於一條斜線上。
- 叔叔節點為空,且祖父節點、父節點和新節點不處於一條斜線上。
- 情形1
情形1的操作是將父節點和叔叔節點與祖父節點的顏色互換,這樣就符合了RBTRee的定義。即維持了高度的平衡,修復後顏色也符合RBTree定義的第三條和第四條。下圖中,操作完成後A節點變成了新的節點。如果A節點的父節點不是黑色的話,則繼續做修復操作。
- 情形2
情形2的操作是將B節點進行右旋操作,並且和父節點A互換顏色。通過該修復操作RBTRee的高度和顏色都符合紅黑樹的定義。如果B和C節點都是右節點的話,只要將操作變成左旋就可以了。
- 情形3:
情形3的操作是將C節點進行左旋,這樣就從情形3轉換成情形2了,然後針對情形2進行操作處理就行了。情形2操作做了一個右旋操作和顏色互換來達到目的。如果樹的結構是下圖的映象結構,則只需要將對應的左旋變成右旋,右旋變成左旋即可。
- 總結
插入後的修復操作是一個向root節點回溯的操作,一旦牽涉的節點都符合了紅黑樹的定義,修復操作結束。之所以會向上回溯是由於情形操作會將父節點,叔叔節點和祖父節點進行換顏色,有可能會導致祖父節點不平衡(紅黑樹定義3)。這個時候需要對祖父節點為起點進行調節(向上回溯)。
祖父節點調節後如果還是遇到它的祖父顏色問題,操作就會繼續向上回溯,直到root節點為止,根據定義root節點永遠是黑色的。在向上的追溯的過程中,針對插入的3中情況進行調節。直到符合紅黑樹的定義為止。直到牽涉的節點都符合了紅黑樹的定義,修復操作結束。
如果上面的3中情況如果對應的操作是在右子樹上,做對應的映象操作就是了。
5.2.3、紅黑樹的刪除
紅黑樹的刪除大體上和二叉查詢樹的刪除類似,如果是葉子節點就直接刪除,如果是非葉子節點,會用對應的中序遍歷的後繼節點來頂替要刪除節點的位置。
但是,紅黑樹刪除之後需要做修復的操作,使樹符合紅黑樹的定義。
刪除修復操作在遇到被刪除的節點是紅色節點或者到達root節點時,修復操作完畢。
刪除修復操作是針對刪除黑色節點才有的,當黑色節點被刪除後會讓整個樹不符合RBTree的定義的第四條。需要做的處理是從兄弟節點上借調黑色的節點過來,如果兄弟節點沒有黑節點可以借調的話,就只能往上追溯,將每一級的黑節點數減去一個,使得整棵樹符合紅黑樹的定義。
刪除操作的總體思想是從兄弟節點借調黑色節點使樹保持區域性的平衡,如果區域性的平衡達到了,就看整體的樹是否是平衡的,如果不平衡就接著向上追溯調整。
(刪除黑色節點後)刪除修復操作分四種情況:
- 情形1:待刪除的節點的兄弟節點是紅色的節點
由於兄弟節點是紅色節點的時候,無法借調黑節點,所以需要將兄弟節點提升到父節點,由於兄弟節點是紅色的,根據紅黑樹的定義,兄弟節點的子節點是黑色的,就可以從它的子節點借調了。
情形1這樣轉換之後就會變成後面的情形2,情形 3,或者情形 4進行處理了。上升操作需要對C做一個左旋操作,如果是映象結構的樹只需要做對應的右旋操作即可。
之所以要做情形1操作是因為兄弟節點是紅色的,無法借到一個黑節點來填補刪除的黑節點。
- 情形2:待刪除的節點的兄弟節點是黑色的節點,且兄弟節點的子節點都是黑色的
情形2的刪除操作是由於兄弟節點可以消除一個黑色節點,因為兄弟節點和兄弟節點的子節點都是黑色的,所以可以將兄弟節點變紅,這樣就可以保證樹的區域性的顏色符合定義了。這個時候需要將父節點A變成新的節點,繼續向上調整,直到整顆樹的顏色符合紅黑樹的定義為止。
情形2這種情況下之所以要將兄弟節點變紅,是因為如果把兄弟節點借調過來,會導致兄弟的結構不符合紅黑樹的定義,這樣的情況下只能是將兄弟節點也變成紅色來達到顏色的平衡。當將兄弟節點也變紅之後,達到了區域性的平衡了,但是對於祖父節點來說是不符合定義4的。這樣就需要回溯到父節點,接著進行修復操作。
- 情形3:待調整的節點的兄弟節點是黑色的節點,且兄弟節點的左子節點是紅色的,右節點是黑色的(兄弟節點在右邊),如果兄弟節點在左邊的話,就是兄弟節點的右子節點是紅色的,左節點是黑色的
情形3的刪除操作是一箇中間步驟,它的目的是將左邊的紅色節點借調過來,這樣就可以轉換成情形4狀態了,在情形4狀態下可以將D,E節點都階段過來,通過將兩個節點變成黑色來保證紅黑樹的整體平衡。
之所以說情形3是一箇中間狀態,是因為根據紅黑樹的定義來說,下圖並不是平衡的,他是通過case 2操作完後向上回溯出現的狀態。之所以會出現情形3和後面的情形4的情況,是因為可以通過借用侄子節點的紅色,變成黑色來符合紅黑樹定義4.
- 情形4:待調整的節點的兄弟節點是黑色的節點,且右子節點是是紅色的(兄弟節點在右邊),如果兄弟節點在左邊,則就是對應的就是左節點是紅色的
情形4的操作是真正的節點借調操作,通過將兄弟節點以及兄弟節點的右節點借調過來,並將兄弟節點的右子節點變成紅色來達到借調兩個黑節點的目的,這樣的話,整棵樹還是符合紅黑樹的定義的。
情形這種情況的發生只有在待刪除的節點的兄弟節點為黑,且子節點不全部為黑,才有可能借調到兩個節點來做黑節點使用,從而保持整棵樹都符合紅黑樹的定義。
圖36:紅黑樹刪除情形4
程式碼實現:
- 節點類
public class RBTreeNode<T extends Comparable<T>> {
private T value;//node value
private RBTreeNode<T> left;//left child pointer
private RBTreeNode<T> right;//right child pointer
private RBTreeNode<T> parent;//parent pointer
private boolean red;//color is red or not red
//省略getter、setter,構造方法
}
- 紅黑樹
public class RBTree<T extends Comparable<T>> {
private final RBTreeNode<T> root;
//node number
private java.util.concurrent.atomic.AtomicLong size =
new java.util.concurrent.atomic.AtomicLong(0);
//in overwrite mode,all node's value can not has same value
//in non-overwrite mode,node can have same value, suggest don't use non-overwrite mode.
private volatile boolean overrideMode=true;
public RBTree(){
this.root = new RBTreeNode<T>();
}
public RBTree(boolean overrideMode){
this();
this.overrideMode=overrideMode;
}
public boolean isOverrideMode() {
return overrideMode;
}
public void setOverrideMode(boolean overrideMode) {
this.overrideMode = overrideMode;
}
/**
* number of tree number
* @return
*/
public long getSize() {
return size.get();
}
/**
* get the root node
* @return
*/
private RBTreeNode<T> getRoot(){
return root.getLeft();
}
/**
* add value to a new node,if this value exist in this tree,
* if value exist,it will return the exist value.otherwise return null
* if override mode is true,if value exist in the tree,
* it will override the old value in the tree
*
* @param value
* @return
*/
public T addNode(T value){
RBTreeNode<T> t = new RBTreeNode<T>(value);
return addNode(t);
}
/**
* find the value by give value(include key,key used for search,
* other field is not used,@see compare method).if this value not exist return null
* @param value
* @return
*/
public T find(T value){
RBTreeNode<T> dataRoot = getRoot();
while(dataRoot!=null){
int cmp = dataRoot.getValue().compareTo(value);
if(cmp<0){
dataRoot = dataRoot.getRight();
}else if(cmp>0){
dataRoot = dataRoot.getLeft();
}else{
return dataRoot.getValue();
}
}
return null;
}
/**
* remove the node by give value,if this value not exists in tree return null
* @param value include search key
* @return the value contain in the removed node
*/
public T remove(T value){
RBTreeNode<T> dataRoot = getRoot();
RBTreeNode<T> parent = root;
while(dataRoot!=null){
int cmp = dataRoot.getValue().compareTo(value);
if(cmp<0){
parent = dataRoot;
dataRoot = dataRoot.getRight();
}else if(cmp>0){
parent = dataRoot;
dataRoot = dataRoot.getLeft();
}else{
if(dataRoot.getRight()!=null){
RBTreeNode<T> min = removeMin(dataRoot.getRight());
//x used for fix color balance
RBTreeNode<T> x = min.getRight()==null ? min.getParent() : min.getRight();
boolean isParent = min.getRight()==null;
min.setLeft(dataRoot.getLeft());
setParent(dataRoot.getLeft(),min);
if(parent.getLeft()==dataRoot){
parent.setLeft(min);
}else{
parent.setRight(min);
}
setParent(min,parent);
boolean curMinIsBlack = min.isBlack();
//inherit dataRoot's color
min.setRed(dataRoot.isRed());
if(min!=dataRoot.getRight()){
min.setRight(dataRoot.getRight());
setParent(dataRoot.getRight(),min);
}
//remove a black node,need fix color
if(curMinIsBlack){
if(min!=dataRoot.getRight()){
fixRemove(x,isParent);
}else if(min.getRight()!=null){
fixRemove(min.getRight(),false);
}else{
fixRemove(min,true);
}
}
}else{
setParent(dataRoot.getLeft(),parent);
if(parent.getLeft()==dataRoot){
parent.setLeft(dataRoot.getLeft());
}else{
parent.setRight(dataRoot.getLeft());
}
//current node is black and tree is not empty
if(dataRoot.isBlack() && !(root.getLeft()==null)){
RBTreeNode<T> x = dataRoot.getLeft()==null
? parent :dataRoot.getLeft();
boolean isParent = dataRoot.getLeft()==null;
fixRemove(x,isParent);
}
}
setParent(dataRoot,null);
dataRoot.setLeft(null);
dataRoot.setRight(null);
if(getRoot()!=null){
getRoot().setRed(false);
getRoot().setParent(null);
}
size.decrementAndGet();
return dataRoot.getValue();
}
}
return null;
}
/**
* fix remove action
* @param node
* @param isParent
*/
private void fixRemove(RBTreeNode<T> node,boolean isParent){
RBTreeNode<T> cur = isParent ? null : node;
boolean isRed = isParent ? false : node.isRed();
RBTreeNode<T> parent = isParent ? node : node.getParent();
while(!isRed && !isRoot(cur)){
RBTreeNode<T> sibling = getSibling(cur,parent);
//sibling is not null,due to before remove tree color is balance
//if cur is a left node
boolean isLeft = parent.getRight()==sibling;
if(sibling.isRed() && !isLeft){//case 1
//cur in right
parent.makeRed();
sibling.makeBlack();
rotateRight(parent);
}else if(sibling.isRed() && isLeft){
//cur in left
parent.makeRed();
sibling.makeBlack();
rotateLeft(parent);
}else if(isBlack(sibling.getLeft()) && isBlack(sibling.getRight())){//case 2
sibling.makeRed();
cur = parent;
isRed = cur.isRed();
parent=parent.getParent();
}else if(isLeft && !isBlack(sibling.getLeft())
&& isBlack(sibling.getRight())){//case 3
sibling.makeRed();
sibling.getLeft().makeBlack();
rotateRight(sibling);
}else if(!isLeft && !isBlack(sibling.getRight())
&& isBlack(sibling.getLeft()) ){
sibling.makeRed();
sibling.getRight().makeBlack();
rotateLeft(sibling);
}else if(isLeft && !isBlack(sibling.getRight())){//case 4
sibling.setRed(parent.isRed());
parent.makeBlack();
sibling.getRight().makeBlack();
rotateLeft(parent);
cur=getRoot();
}else if(!isLeft && !isBlack(sibling.getLeft())){
sibling.setRed(parent.isRed());
parent.makeBlack();
sibling.getLeft().makeBlack();
rotateRight(parent);
cur=getRoot();
}
}
if(isRed){
cur.makeBlack();
}
if(getRoot()!=null){
getRoot().setRed(false);
getRoot().setParent(null);
}
}
//get sibling node
private RBTreeNode<T> getSibling(RBTreeNode<T> node,RBTreeNode<T> parent){
parent = node==null ? parent : node.getParent();
if(node==null){
return parent.getLeft()==null ? parent.getRight() : parent.getLeft();
}
if(node==parent.getLeft()){
return parent.getRight();
}else{
return parent.getLeft();
}
}
private boolean isBlack(RBTreeNode<T> node){
return node==null || node.isBlack();
}
private boolean isRoot(RBTreeNode<T> node){
return root.getLeft() == node && node.getParent()==null;
}
/**
* find the successor node
* @param node current node's right node
* @return
*/
private RBTreeNode<T> removeMin(RBTreeNode<T> node){
//find the min node
RBTreeNode<T> parent = node;
while(node!=null && node.getLeft()!=null){
parent = node;
node = node.getLeft();
}
//remove min node
if(parent==node){
return node;
}
parent.setLeft(node.getRight());
setParent(node.getRight(),parent);
//don't remove right pointer,it is used for fixed color balance
//node.setRight(null);
return node;
}
private T addNode(RBTreeNode<T> node){
node.setLeft(null);
node.setRight(null);
node.setRed(true);
setParent(node,null);
if(root.getLeft()==null){
root.setLeft(node);
//root node is black
node.setRed(false);
size.incrementAndGet();
}else{
RBTreeNode<T> x = findParentNode(node);
int cmp = x.getValue().compareTo(node.getValue());
if(this.overrideMode && cmp==0){
T v = x.getValue();
x.setValue(node.getValue());
return v;
}else if(cmp==0){
//value exists,ignore this node
return x.getValue();
}
setParent(node,x);
if(cmp>0){
x.setLeft(node);
}else{
x.setRight(node);
}
fixInsert(node);
size.incrementAndGet();
}
return null;
}
/**
* find the parent node to hold node x,if parent value equals x.value return parent.
* @param x
* @return
*/
private RBTreeNode<T> findParentNode(RBTreeNode<T> x){
RBTreeNode<T> dataRoot = getRoot();
RBTreeNode<T> child = dataRoot;
while(child!=null){
int cmp = child.getValue().compareTo(x.getValue());
if(cmp==0){
return child;
}
if(cmp>0){
dataRoot = child;
child = child.getLeft();
}else if(cmp<0){
dataRoot = child;
child = child.getRight();
}
}
return dataRoot;
}
/**
* red black tree insert fix.
* @param x
*/
private void fixInsert(RBTreeNode<T> x){
RBTreeNode<T> parent = x.getParent();
while(parent!=null && parent.isRed()){
RBTreeNode<T> uncle = getUncle(x);
if(uncle==null){//need to rotate
RBTreeNode<T> ancestor = parent.getParent();
//ancestor is not null due to before before add,tree color is balance
if(parent == ancestor.getLeft()){
boolean isRight = x == parent.getRight();
if(isRight){
rotateLeft(parent);
}
rotateRight(ancestor);
if(isRight){
x.setRed(false);
parent=null;//end loop
}else{
parent.setRed(false);
}
ancestor.setRed(true);
}else{
boolean isLeft = x == parent.getLeft();
if(isLeft){
rotateRight(parent);
}
rotateLeft(ancestor);
if(isLeft){
x.setRed(false);
parent=null;//end loop
}else{
parent.setRed(false);
}
ancestor.setRed(true);
}
}else{//uncle is red
parent.setRed(false);
uncle.setRed(false);
parent.getParent().setRed(true);
x=parent.getParent();
parent = x.getParent();
}
}
getRoot().makeBlack();
getRoot().setParent(null);
}
/**
* get uncle node
* @param node
* @return
*/
private RBTreeNode<T> getUncle(RBTreeNode<T> node){
RBTreeNode<T> parent = node.getParent();
RBTreeNode<T> ancestor = parent.getParent();
if(ancestor==null){
return null;
}
if(parent == ancestor.getLeft()){
return ancestor.getRight();
}else{
return ancestor.getLeft();
}
}
private void rotateLeft(RBTreeNode<T> node){
RBTreeNode<T> right = node.getRight();
if(right==null){
throw new java.lang.IllegalStateException("right node is null");
}
RBTreeNode<T> parent = node.getParent();
node.setRight(right.getLeft());
setParent(right.getLeft(),node);
right.setLeft(node);
setParent(node,right);
if(parent==null){//node pointer to root
//right raise to root node
root.setLeft(right);
setParent(right,null);
}else{
if(parent.getLeft()==node){
parent.setLeft(right);
}else{
parent.setRight(right);
}
//right.setParent(parent);
setParent(right,parent);
}
}
private void rotateRight(RBTreeNode<T> node){
RBTreeNode<T> left = node.getLeft();
if(left==null){
throw new java.lang.IllegalStateException("left node is null");
}
RBTreeNode<T> parent = node.getParent();
node.setLeft(left.getRight());
setParent(left.getRight(),node);
left.setRight(node);
setParent(node,left);
if(parent==null){
root.setLeft(left);
setParent(left,null);
}else{
if(parent.getLeft()==node){
parent.setLeft(left);
}else{
parent.setRight(left);
}
setParent(left,parent);
}
}
private void setParent(RBTreeNode<T> node,RBTreeNode<T> parent){
if(node!=null){
node.setParent(parent);
if(parent==root){
node.setParent(null);
}
}
}
/**
* debug method,it used print the given node and its children nodes,
* every layer output in one line
* @param root
*/
public void printTree(RBTreeNode<T> root){
java.util.LinkedList<RBTreeNode<T>> queue =new java.util.LinkedList<RBTreeNode<T>>();
java.util.LinkedList<RBTreeNode<T>> queue2 =new java.util.LinkedList<RBTreeNode<T>>();
if(root==null){
return ;
}
queue.add(root);
boolean firstQueue = true;
while(!queue.isEmpty() || !queue2.isEmpty()){
java.util.LinkedList<RBTreeNode<T>> q = firstQueue ? queue : queue2;
RBTreeNode<T> n = q.poll();
if(n!=null){
String pos = n.getParent()==null ? "" : ( n == n.getParent().getLeft()
? " LE" : " RI");
String pstr = n.getParent()==null ? "" : n.getParent().toString();
String cstr = n.isRed()?"R":"B";
cstr = n.getParent()==null ? cstr : cstr+" ";
System.out.print(n+"("+(cstr)+pstr+(pos)+")"+"\t");
if(n.getLeft()!=null){
(firstQueue ? queue2 : queue).add(n.getLeft());
}
if(n.getRight()!=null){
(firstQueue ? queue2 : queue).add(n.getRight());
}
}else{
System.out.println();
firstQueue = !firstQueue;
}
}
}
public static void main(String[] args) {
RBTree<String> bst = new RBTree<String>();
bst.addNode("d");
bst.addNode("d");
bst.addNode("c");
bst.addNode("c");
bst.addNode("b");
bst.addNode("f");
bst.addNode("a");
bst.addNode("e");
bst.addNode("g");
bst.addNode("h");
bst.remove("c");
bst.printTree(bst.getRoot());
}
}
6、樹、森林、二叉樹
上面已經學習了二叉樹以及一些特殊的二叉樹,接下來學習樹的表示及相關操作。
6.1、樹的儲存結構
表現樹的儲存結構的形式有很多,有3種比較常見。
6.1.1、雙親表示法
這種表示方法中, 以一組連續的儲存單元儲存樹的節點,每個節點除了資料域data外,還附設一個parent域用以指示其雙親節點的位置, 其結點形式如圖37所示。
這種儲存結構利用了每個結點 (除根以外)只有唯一的雙親的性質。 在這種儲存結構下 , 求結點的雙親十分方便, 也很容易求樹的根, 但求結點的孩子時需要遍歷整個結構。
6.1.2、孩子表示法
由於樹中每個節點可能有多棵子樹, 則可用多重連結串列, 即每個結點有多個指標域, 其中每個
指標指向一棵子樹的根節點,此時連結串列中的節點可以有如圖 39 所示的兩種結點節點。
圖 40 (a)所示為圖 38 中的樹的孩子表示法。 與雙親表示法相反, 孩子表示法便於那些涉及孩子的操作的實現。可以把雙親表示法和孩子表示法結合起來,即將雙親表示和孩子連結串列合在一起。 圖 40(b) 所示的就是這種儲存結構的一 例, 它和圖 40 (a)表示的是同一棵樹。
6.1.3、孩子兄弟法
又稱二叉樹表示法,或二叉連結串列表示法,即以二叉連結串列做樹的儲存結構。連結串列中結點的兩個鏈域分別指向該結點的第一個孩子結點和下一個兄弟結點,分別命名為 firstchild 域和 nextsibling域,其結點形式如圖41所示。
圖42所示為圖40中的樹的孩子兄弟連結串列。利用這種儲存結構便於實現各種樹的操作。
6.2、樹轉換為二叉樹
在這裡我們約定樹是有序的,樹中每一個節點的兒子結點按從左到右的次序順序編號。
如圖43所示的一棵樹,根節點 A有三個兒子 B、 C、 D, 可以認為節點 B為 A的第一個兒子節點, 結點 C 為 A的第二個兒子節點, 節點 D 為 A 的第三個兒子節點。
將一棵樹轉換為二叉樹的方法是:
- (1) 樹中所有相鄰兄弟之間加一條連線;
- (2) 對樹中的每個結點, 只保留它與第一個兒子結點之間的連線, 刪去它與其他兒子結點之間的連線。
- (3) 以樹的根結點為軸心, 將整棵樹順時針轉動一定的角度, 使之結構層次分明。
樹轉換為二叉樹的轉換過程示意圖如下:
6.3、二叉樹還原為樹
樹轉換為二叉樹這一轉換過程是可逆的, 可以依據二叉樹的根結點有無右兒子結點,將一棵二叉樹還原為樹, 具體方法如下:
- (1) 若某結點是其雙親的左兒子, 則把該結點的右兒子、 右兒子的右兒子、 … 都與該結點的雙親結點用線連起來;
- (2) 刪掉原二叉樹中所有的雙親結點與右兒子結點的連線;
- (3) 整理由(1)、(2)兩步所得到的樹, 使之結構層次分明。
二叉樹還原為樹的過程示意圖如下所示:
6.4、森林轉換為二叉樹
森林是若干棵樹的集合, 森林亦可用二叉樹表示。
森林轉換為二叉樹的方法如下:
- (1) 將森林中的每棵樹轉換成相應的二叉樹;
- (2) 第一棵二叉樹不動, 從第二棵二叉樹開始, 依次將後一棵二叉樹的根結點作為前一棵二叉樹根結點的右孩子, 當所有的二叉樹連在一起後, 這樣所得到的二叉樹就是由森林轉換得到的二叉樹。
森林及其轉換為二叉樹的過程如下圖所示:
6.5、樹與森林的遍歷
6.5.1、樹的遍歷
由樹結構的定義可引出兩種次序遍歷樹的方法:一種是先根(次序)遍歷樹,即:先訪問樹的根結點,然後依次先根遍歷根的每棵子樹;另一種是後根(次序)遍歷,即先依次後根遍歷每棵子樹,然後訪問根結點。
例如,對圖 38 所示的樹進行先根遍歷,可得樹的先根序列為:
R A D E B C F G H K
對該樹進行後根遍歷,則得樹的後根序列為:
D E A B G H K F C R
按照森林和樹相互遞迴的定義,可以推出森林的兩種遍歷方法:先序遍歷和中序遍歷。
6.5.2、森林的遍歷
森林的遍歷有兩種方式: 前序遍歷和中序遍歷。
前序遍歷
前序遍歷的過程:
- (1) 訪問森林中第一棵樹的根結點;
- (2) 前序遍歷第一棵樹的根結點的子樹森林;
- (3) 前序遍歷剩餘的其他子森林。
對於圖 46 所示的森林進行前序遍歷, 得到的結果序列為 A B C D E F G H I J K。
中序遍歷
中序遍歷的過程:
- (1) 中序遍歷第一棵樹的根結點的子樹森林;
- (2) 訪問森林中第一棵樹的根結點;
- (3) 中序遍歷剩餘的其他子森林。
對於圖 46 所示的森林進行中序遍歷, 得到的結果序列為 B A D E F C J H K I G 。
根據森林與二叉樹的轉換關係以及森林和二叉樹的遍歷定義可以推論: 森林前序遍歷和中序遍歷分別與所轉換的二叉樹的前序遍歷和中序遍歷的結果序列相同。
7、B樹
在前面學習了平衡二叉樹,B樹也是一種平衡查詢樹,不過不是二叉樹。
B樹也稱B-樹,它是一種多路平衡查詢樹。
一棵m階的B樹定義如下:
- 每個節點最多有m-1個關鍵字(可以存有的鍵值對)。
- 根節點最少可以只有1個關鍵字。
* 非根節點至少有m/2個關鍵字。 - 每個節點中的關鍵字都按照從小到大的順序排列,每個關鍵字的左子樹中的所有關鍵字都小於它,而右子樹中的所有關鍵字都大於它。
- 所有葉子節點都位於同一層,或者說根節點到每個葉子節點的長度都相同。
- 每個節點都存有索引和資料,也就是對應的key和value。
看一個B樹的例項(字母大小 C>B>A)
看看B樹的一些基本操作。
7.1、查詢
查詢和平衡二叉樹類似,不過B樹是多路的而已。以圖47中查詢15為例:
- (1)獲取根節點的關鍵字進行比較,當前根節點關鍵字為39,15<39,所以往找到指向左邊的子節點(二分法規則,左小右大,左邊放小於當前節點值的子節點、右邊放大於當前節點值的子節點);
- (2) 獲取到關鍵字12、22, 12<15<22,所以查詢12和22中間的節點
- (3)獲取到關鍵字13和15,因為15=15,所以返回關鍵字和指標資訊;如果沒有找到所包含的節點,返回null。
7.2、插入
插入的時候,需要記住一個規則:判斷當前結點key的個數是否小於等於m-1,如果滿足,直接插入即可,如果不滿足,將節點的中間的key將這個節點分為左右兩部分,中間的節點放到父節點中即可。
例子:在5階B樹中,結點最多有4個key,最少有2個key(注意:下面的節點統一用一個節點表示key和value)。
-
插入18,70,50,40
-
插入22
插入22時,發現這個節點的關鍵字已經大於4了,所以需要進行分裂,分裂的規則在上面已經講了,分裂之後,如下。
- 接著插入23,25,39
分裂,得到下面的。
7.3、刪除
B樹的刪除操作相對於插入操作是相對複雜一些。
- 樹初始狀態如下
- 刪除15,這種情況是刪除葉子節點的元素,如果刪除之後,節點數還是大於m/2,這種情況只要直接刪除即可。
- 接著,把22刪除,這種情況的規則:22是非葉子節點,對於非葉子節點的刪除,我們需要用後繼key(元素)覆蓋要刪除的key,然後在後繼key所在的子支中刪除該後繼key。對於刪除22,需要將後繼元素24移到被刪除的22所在的節點。
此時發現26所在的節點只有一個元素,小於2個(m/2),這個節點不符合要求,這時候的規則(向兄弟節點借元素):如果刪除葉子節點,如果刪除元素後元素個數少於(m/2),並且它的兄弟節點的元素大於(m/2),也就是說兄弟節點的元素比最少值m/2還多,將先將父節點的元素移到該節點,然後將兄弟節點的元素再移動到父節點。這樣就滿足要求。
看看操作過程:
- 接著刪除28,刪除葉子節點,刪除後不滿足要求,所以,我們需要考慮向兄弟節點借元素,但是,兄弟節點也沒有多的節點(2個),借不了,怎麼辦呢?如果遇到這種情況,首先,還是將先將父節點的元素移到該節點,然後,將當前節點及它的兄弟節點中的key合併,形成一個新的節點。
移動之後,跟兄弟節點合併。
8、B+樹
B+樹是B樹的變體,也是一種多路搜尋樹。
B+樹·和B樹有一些共同的特性:
- 根節點至少一個元素
- 非根節點元素範圍:m/2 <= k <= m-1
B+樹和B樹也有一些不一樣的地方:
- B+樹有兩種型別的節點:非葉子結點(也稱索引結點)和葉子結點。非葉子節點不儲存資料,只儲存索引,資料都儲存在葉子節點。
- 非葉子結點中的key都按照從小到大的順序排列,對於非葉子結點中的一個key,左樹中的所有key都小於它,右子樹中的key都大於等於它。葉子結點中的記錄也按照key的大小排列。
- 每個葉子結點都存有相鄰葉子結點的指標,葉子結點本身依關鍵字的大小自小而大順序連結。
- 父節點存有右孩子的第一個元素的索引。
看一個B+樹的示例:
8.1、查詢
B+樹的查詢右兩種方式:
-
(1)從最小關鍵字起順序查詢;
-
(2)從根節點開始,進行隨機查詢
在查詢時,若非葉子節點上的關鍵字等於給定值,並不終止,而是繼續向下直到葉子節點。因此,在B+樹中,不管查詢成功與否,每次查詢都是走了一條從根到葉子節點的路徑。其餘同B樹的查詢類似。
8.2、插入
插入操作有一個規則:當節點元素數量大於m-1的時候,按中間元素分裂成左右兩部分,中間元素分裂到父節點當做索引儲存,但是,本身中間元素還是分裂右邊這一部分的。
以一顆5階B+樹的插入過程為例,5階B+樹的節點最少2個元素,最多4個元素。
- 插入5,10,15,20
- 插入25,此時元素數量大於4個了,分裂
- 接著插入26,30,繼續分裂
8.3、刪除
刪除操作比B樹簡單一些,因為葉子節點有指標的存在,向兄弟節點借元素時,不需要通過父節點了,而是可以直接通過兄弟節移動即可(前提是兄弟節點的元素大於m/2),然後更新父節點的索引;如果兄弟節點的元素不大於m/2(兄弟節點也沒有多餘的元素),則將當前節點和兄弟節點合併,並且刪除父節點中的key,
下面來看一個具體的例項:
- B+樹的初始狀態
- 刪除10,刪除後,不滿足要求,發現左邊兄弟節點有多餘的元素,所以去借元素,最後,修改父節點索引
- 刪除元素5,發現不滿足要求,並且發現左右兄弟節點都沒有多餘的元素,所以,可以選擇和兄弟節點合併,最後修改父節點索引
- 發現父節點索引也不滿足條件,所以,需要做跟上面一步一樣的操作
B+樹相比較B樹有一些優點:
- B+樹的層級更少:相較於B樹B+每個非葉子節點儲存的關鍵字數更多,樹的層級更少所以查詢資料更快
- B+樹查詢速度更穩定:B+所有關鍵字資料地址都存在葉子節點上,所以每次查詢的次數都相同所以查詢速度要比B樹更穩定
- B+樹天然具備排序功能:B+樹所有的葉子節點資料構成了一個有序連結串列,在查詢大小區間的資料時候更方便,資料緊密性很高,快取的命中率也會比B樹高
- B+樹全節點遍歷更快:B+樹遍歷整棵樹只需要遍歷所有的葉子節點即可,,而不需要像B樹一樣需要對每一層進行遍歷,這有利於資料庫做全表掃描
這裡不再給出B樹和B+樹程式碼實現,程式碼實現可見參考【26】
上一篇:重學資料結構(五、串)
本部落格為學習筆記,參考資料如下!
水平有限,難免錯漏,歡迎指正!
參考:
【1】:鄧俊輝 編著. 《資料結構與演算法》
【2】:王世民 等編著 . 《資料結構與演算法分析》
【3】: Michael T. Goodrich 等編著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》
【4】:嚴蔚敏、吳偉民 編著 . 《資料結構》
【5】:程傑 編著 . 《大話資料結構》
【6】:[Data Structure] 資料結構中各種樹
【7】:Tree
【8】:Binary Tree
【9】:Java資料結構與演算法——二叉樹及操作(包括二叉樹遍歷)
【10】:Java資料結構和演算法(十)——二叉樹
【11】:阿粉帶你玩轉二叉查詢樹
【12】:JAVA遞迴實現線索化二叉樹
【13】:二叉查詢樹(三)之 Java的實現
【14】:一步一步寫平衡二叉樹(AVL樹)
【15】:什麼是平衡二叉樹(AVL)
【16】:什麼是平衡二叉樹(AVL)
【17】:動畫 | 什麼是AVL樹?
【18】:詳解什麼是平衡二叉樹(AVL)(修訂補充版)
【19】:紅黑樹深入剖析及Java實現
【20】:平衡查詢樹之紅黑樹
【21】:漫畫:什麼是紅黑樹?
【22】:面試官問你B樹和B+樹,就把這篇文章丟給他
【23】:平衡二叉樹、B樹、B+樹、B*樹 理解其中一種你就都明白了
【24】:B樹和B+樹的插入、刪除圖文詳解
【25】:B樹Java程式碼實現以及測試
【26】:Introduction of B-Tree
【27】:B+樹詳解