目錄
1、什么是樹
2、相關術語
3、二叉樹
3.1、二叉樹的類型
3.2、二叉樹的性質
3.3、二叉樹的結構
3.4、二叉樹的遍歷
3.4.1、遍歷的分類
3.4.1.1、前序遍歷
3.4.1.2、中序遍歷
3.4.1.3、后序遍歷
3.4.1.4、層次遍歷
4、通用樹(N叉樹)
4.1、通用樹的表示
4.2、代碼實現
5、線索二叉樹
5.1、常規二叉樹遍歷的問題
5.2、線索二叉樹的動機
5.3、線索二叉樹的類型
5.4、線索二叉樹的代碼實現
5.5、線索二叉樹的示例
5.6、線索二叉樹的操作
5.6.1、中序線索二叉樹中查找中序后繼
5.6.2、中序線索二叉樹的中序遍歷
5.6.3、中序線索二叉樹中查找前序后繼
5.6.4、中序線索二叉樹的前序遍歷
5.6.5、中序線索二叉樹插入結點
6、表達式樹
6.1、表達式樹舉例
6.2、表達式樹代碼實現
7、二叉搜索樹
7.1、二叉搜索樹的作用
7.2、二叉搜索樹的性質
7.3、二叉搜索樹的代碼實現
7.4、二叉搜索樹的操作
7.4.1、在二叉搜索樹中尋找元素
7.4.2、在二叉搜索樹中尋找最小元素
7.4.3、在二叉搜索樹中尋找最大元素
7.4.4、在二叉搜索樹中尋找中序序列前驅和后繼
7.4.5、在二叉搜索樹中插入元素
7.4.6、在二叉搜索樹中刪除元素
8、平衡二叉搜索樹
8.1、完全平衡二叉搜索樹
8.2、AVL樹
8.2.1、AVL樹的性質
8.2.2、AVL樹的最小/最大結點
8.2.3、AVL樹的定義
8.2.4、AVL樹的旋轉
8.2.4.1、單旋轉
8.2.4.2、雙旋轉
8.2.5、AVL樹的插入操作
正文
1、什么是樹
- 樹是一種類似鏈表的數據結構,不過鏈表的結點是以線性方式簡單地指向其后繼指點,而樹的一個結點可以指向許多個結點。樹是一種典型的非線性結構。樹結構是表達具有層次特性的圖結構的一種方法。
2、相關術語
- 根結點:根結點就是一個沒有雙親結點的結點。一棵樹中最多有一個根結點(如圖2-1的結點A就是根結點)。
- 邊:邊表示從雙親結點到孩子結點的鏈接(如圖2-1的所有鏈接)。
- 葉子結點:沒有孩子結點的結點叫做葉子結點(如圖2-1中的E、J、K、H、I )。
- 兄弟結點:擁有相同的雙親結點的所有孩子結點叫做兄弟結點(如圖2-1中的B、C、D是A的兄弟結點)。
- 祖先結點:如果存在一條從根結點到q的路徑,且結點p出現在這條路徑上,那么就可以把p叫做q的祖先結點(如圖2-1中A、C、G是K的祖先結點)。
- 結點的大小:結點的大小是指子孫的個數,包括其自身(如圖2-1中子樹C的大小為3)。
- 樹的層:位于相同深度的所有結點的集合叫做樹的層(如圖2-2中的1和4具有相同的層)。
- 結點的深度:是指從根結點到該結點的路徑長度(如圖2-2中的5深度為2,3-4-5)。
- 結點的高度:是指從結點到最深結點的路徑長度。樹的高度是指根結點到樹中最深結點的路徑長度。只含有根結點的樹的高度是0(如圖2-2樹的高度為2)。
- 斜樹:如果樹中除了葉子結點外,其余每個結點都只有一個孩子結點,則這種樹稱為斜樹(如圖2-3)。
3、二叉樹
-
如果一棵樹中的每個結點有0、1或2個孩子結點,那么這個樹就稱為二叉樹。空樹也是一棵有效的二叉樹。一棵二叉樹可以看作由根結點和兩棵不相交的子樹組成,如圖3-1所示。
圖3-1 二叉樹示例
3.1、二叉樹的類型
-
嚴格二叉樹:二叉樹中的每個結點要么有兩個孩子結點,要么沒有孩子結點(如圖3-2所示)。
圖3-2 嚴格二叉樹 -
滿二叉樹:二叉樹的每個結點恰好有兩個孩子結點且所有葉子結點在同一層(如圖3-3所示)。
圖3-3 滿二叉樹 -
完全二叉樹:假定二叉樹的高度為h。對于完全二叉樹,如果將所有結點從根結點開始從左至右,從上至下,依次編號(假定根結點編號為1),那么將得到從1~n(n為結點總數)的完整序列。如果所有葉子結點的深度為h或者h-1,且結點在編號過程中沒有漏掉任何數字,那么就叫做完全二叉樹(如圖3-4所示)。
圖3-4 完全二叉樹
3.2、二叉樹的性質
假定樹的高度為h,且根結點的深度為0。
從圖3-5可得出如下性質:
- 滿二叉樹的結點個數n為[2^(h+1)] -1。因為該樹總共有h層,每一層結點都是滿的。
- 滿二叉樹的葉子結點個數是2^h。
3.3、二叉樹的結構
-
表示一個結點的方法之一是定義兩個指針,一個指向左孩子結點,另一個指向右孩子結點,中間為數據字段(如圖3-6所示)。
圖3-6 二叉樹的結構 - 代碼實現:
public class BinaryTreeNode {
private int data;
private BinaryTreeNode left;
private BinaryTreeNode right;
public int getData(){
return data;
}
public void setData(int data) {
this.data = data;
}
public BinaryTreeNode getLeft() {
return left;
}
public void setLeft(BinaryTreeNode left) {
this.left = left;
}
public BinaryTreeNode getRight() {
return right;
}
public void setRight(BinaryTreeNode right) {
this.right = right;
}
}
3.4、二叉樹的遍歷
訪問樹中所有結點的過程叫做遍歷,遍歷的目標是按照某種特定的順序訪問樹的所有結點。
3.4.1、遍歷的分類
- 前序遍歷(DLR):訪問當前結點,遍歷左子樹,再遍歷右子樹。
- 中序遍歷(LDR):先遍歷左子樹,訪問當前結點,再遍歷右子樹。
- 后序遍歷(LRD):先遍歷左子樹,再遍歷右子樹,訪問當前結點。
-
層次遍歷。
以下圖所示的樹為例。
圖3-7 二叉樹的遍歷
3.4.1.1、前序遍歷
- 基于前序遍歷,圖3-7所示樹的前序遍歷結果如下:
C:\Application\java\jdk\bin\java.exe ..."
前序遍歷結果:
1
2
4
5
3
6
7
Process finished with exit code 0
- 遞歸前序遍歷,代碼實現如下:
public void PreOrder(BinaryTreeNode root){
if(root!=null){
System.out.print(root.getData());
PreOrder(root.getLeft());
PreOrder(root.getRight());
}
}
- 非遞歸前序遍歷,需要采用一個棧來記錄當前結點,以便在完成左子樹遍歷后能返回到右子樹進行遍歷。首先處理當前結點,在遍歷左子樹之前,把當前結點保留到棧中,當遍歷完左子樹之后,該元素出棧,然后找到其右子樹進行遍歷,直至棧空。代碼實現如下:
public void PreOrderNonRecursive(BinaryTreeNode root){
if(root==null){
return;
}
Stack s=new Stack();
while (true){
while (root!=null){
System.out.print(root.getData());
s.push(root);
root=root.getLeft();
}
if(s.isEmpty()){
break;
}
root=(BinaryTreeNode) s.pop();
root=root.getRight();
}
}
3.4.1.2、中序遍歷
- 基于中序遍歷,圖3-7所示樹的中序遍歷結果如下:
C:\Application\java\jdk\bin\java.exe ..."
中序遍歷結果:
4
2
5
1
6
3
7
Process finished with exit code 0
- 遞歸中序遍歷,代碼實現如下:
public void InOrder(BinaryTreeNode root){
if(root!=null){
InOrder(root.getLeft());
System.out.print(root.getData());
InOrder(root.getRight());
}
}
- 非遞歸中序遍歷,與前序遍歷的區別是,首先要移動到結點的左子樹,完成左子樹的遍歷后,再將當前結點出棧進行處理。
public void InOrderNonRecursive(BinaryTreeNode root){
if(root==null){
return;
}
Stack s=new Stack();
while (true){
while (root!=null){
s.push(root);
root=root.getLeft();
}
if(s.isEmpty()){
break;
}
root=(BinaryTreeNode)s.pop();
System.out.print(root.getData()+"\n");
root=root.getRight();
}
}
3.4.1.3、后序遍歷
- 基于后序遍歷,圖3-7所示樹的后序遍歷結果如下:
C:\Application\java\jdk\bin\java.exe ..."
后序遍歷結果:
4
5
2
6
7
3
1
Process finished with exit code 0
- 遞歸后序遍歷,代碼實現如下:
public void PostOrder(BinaryTreeNode root){
if(root!=null){
PostOrder(root.getLeft());
PostOrder(root.getRight());
System.out.print(root.getData());
}
}
- 非遞歸后序遍歷,在前序和中序遍歷中,當元素出棧后就不需要再訪問這個結點了。但是后序遍歷中,每個結點需要訪問兩次。這意味著,在遍歷完左子樹后,需要訪問當前結點,之后遍歷完右子樹時,還需要訪問當前結點。但只有在第二次訪問時才處理該結點。
- 解決辦法是,當從棧中出棧一個元素時,檢查這個元素與棧頂元素的右子結點是否相同。如果相同,則說明已經完成了左右子樹的遍歷。此時只要再將棧頂元素出棧并輸出該結點元素。
public void PostOrderNonRecursive(BinaryTreeNode root){
Stack stack=new Stack();
while (true){
if(root!=null){
//尋找最左葉子結點
stack.push(root);
root=root.getLeft();
}else {
if(stack.isEmpty()){
return;
}else {
//判斷當前結點是否有右子節點
if(stack.top().getRight==null){
root=stack.pop();
System.out.print(root.getData()+"\n");
//判斷該結點是否為棧頂右子節點
while(root==stack.top().getRight()){
System.out.print(stack.top().getData()+"\n");
root=stack.pop();
if(stack.isEmpty()){
return;
}
}
}
}
if(!stack.isEmpty()){
//遍歷結點右子樹
root=stack.top().getRight();
}else {
root=null;
}
}
}
}
3.4.1.4、層次遍歷
- 基于層次遍歷,圖3-7所示樹的層次遍歷結果如下:
C:\Application\java\jdk\bin\java.exe ..."
層次遍歷結果:
1
2
3
4
5
6
7
Process finished with exit code 0
- 層次遍歷,代碼實現如下:
public void LevelOrder(BinaryTreeNode root){
BinaryTreeNode temp;
LLBinaryTreeQueue queue=LLBinaryTreeQueue.creteQueue();
if(root==null){
return;
}
queue.enQueue(root);
while (!queue.isEmpty()){
temp=queue.deQueue();
//處理當前結點
System.out.print(temp.getData()+"\n");
if(temp.getLeft()!=null){
queue.enQueue(temp.getLeft());
}
if(temp.getRight()!=null){
queue.enQueue(temp.getRight());
}
}
}
4、通用樹(N叉樹)
-
上一節中,討論了每個結點最多兩個孩子結點的二叉樹,這種樹可以用兩個指針來表示。但是若一棵樹中每個結點可以有任意多個子結點,該如何表示?例如下圖:
圖4-1 N叉樹
4.1、通用樹的表示
- 同一個雙親結點(兄弟)的孩子結點從左至右排列。
-
雙親結點只指向第一個孩子結點,刪除從雙親結點到其他孩子結點的鏈接。
圖4-2 解決示例
4.2、代碼實現
public class TreeNode {
private int data;
private TreeNode firstChild;
private TreeNode nextSibling;
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public TreeNode getFirstChild() {
return firstChild;
}
public void setFirstChild(TreeNode firstChild) {
this.firstChild = firstChild;
}
public TreeNode getNextSibling() {
return nextSibling;
}
public void setNextSibling(TreeNode nextSibling) {
this.nextSibling = nextSibling;
}
}
5、線索二叉樹
- 在3.4中介紹了二叉樹的前序、中序和后序遍歷,這些遍歷方式使用棧作為輔助數據結構,而層次遍歷中使用隊列作為輔助數據結構。線索二叉樹遍歷將擯棄這些輔助數據結構。
5.1、常規二叉樹遍歷的問題
- 棧和隊列需要的存儲空間很大。
- 任意一棵二叉樹往往存在大量的空指針。例如:擁有n個結點的二叉樹有n+1個空指針。
- 很難直接找到一個給定結點的后繼結點(前序、中序和后序后繼)。
5.2、線索二叉樹的動機
- 在空指針中存儲一些有用的信息,就不需要將這些信息存儲在棧/隊列中,空左指針將包含前驅信息,而空右指針將包含后繼信息。這些特殊的指針就叫做線索。
5.3、線索二叉樹的類型
根據線索中存儲的信息是依據前序、中序或后序排列,線索二叉樹有以下3種類型:
- 前序線索二叉樹:空左指針存儲前序序列前驅信息,空右指針存儲前序序列后繼信息。
- 中序線索二叉樹:空左指針存儲中序序列前驅信息,空右指針存儲中序序列后繼信息。
- 后序線索二叉樹:空左指針存儲后序序列前驅信息,空右指針存儲后序序列后繼信息。
注:5.4中代碼實現為中序線索二叉樹。
5.4、線索二叉樹的結構
如圖所示:
代碼實現:
public class ThreadedBinaryTreeNode {
private ThreadedBinaryTreeNode left;
private int LTag;
private int data;
private int RTag;
private ThreadedBinaryTreeNode right;
public ThreadedBinaryTreeNode getLeft() {
return left;
}
public void setLeft(ThreadedBinaryTreeNode left) {
this.left = left;
}
public int getLTag() {
return LTag;
}
public void setLTag(int LTag) {
this.LTag = LTag;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public int getRTag() {
return RTag;
}
public void setRTag(int RTag) {
this.RTag = RTag;
}
public ThreadedBinaryTreeNode getRight() {
return right;
}
public void setRight(ThreadedBinaryTreeNode right) {
this.right = right;
}
}
5.5、線索二叉樹的示例
- 假設一棵二叉樹的中序序列為:2、5、1、16、11、31,先序序列為:1,5,2,11,16,31。
-
構建如下圖5-3所示,虛線箭頭表示線索。此時,最左邊結點(2)的左指針和最右邊結點(31)是懸空的。
圖5-3 示例 -
在線索二叉樹中,使用一個啞結點Dummy,如圖5-4所示。
圖5-4 啞結點 -
根據上述約定,圖5-3可以進一步表示為圖5-5所示。
圖5-5 示例(2)
5.6、線索二叉樹的操作
5.6.1、中序線索二叉樹中查找中序后繼
- 策略:如果當前結點的RTag=0,則返回Right;如果當前結點的RTag=1,說明當前結點有右子樹,依據中序遍歷規則,先遍歷左子樹,再訪問當前結點,最后遍歷右子樹。可以推出,需要找到右子樹的最左孩子結點(相當于遍歷右子樹的最先結點),即為當前結點的后繼。
- 代碼實現:
ThreadedBinaryTreeNode InorderSuccessor(ThreadedBinaryTreeNode P){
ThreadedBinaryTreeNode position;
if(P.getRTag()==0){
return P.getRight();
}else {
position=P.getRight();
while (position.getLTag()==1){
position=position.getLeft();
}
return position;
}
}
5.6.2、中序線索二叉樹的中序遍歷
- 策略:從啞結點開始,遞歸調用InorderSuccessor()來訪問每一個結點,直至再次達到啞結點。
- 代碼實現:
void InorderTraversal(ThreadedBinaryTreeNode root){
ThreadedBinaryTreeNode p=InorderPrecursor(root);
while (p!=root){
p=InorderPrecursor(p);
System.out.print(p.getData());
}
}
5.6.3、中序線索二叉樹中查找前序后繼
- 策略:如果當前結點的LTag=1,則返回Left;如果當前結點的LTag=0,說明當前結點沒有左子樹,那么返回右子樹包含當前結點的最近結點的右孩子結點。
- 代碼實現:
ThreadedBinaryTreeNode PreorderSuccessor(ThreadedBinaryTreeNode p){
ThreadedBinaryTreeNode position;
if(p.getLTag()==1){
return p.getLeft();
}else {
position=p;
while (position.getRTag()==0){
position=position.getRight();
}
return p.getRight();
}
}
5.6.4、中序線索二叉樹的前序遍歷
- 策略:與中序遍歷相同,從啞結點開始,遞歸調用PreorderSuccessor()函數訪問每一個結點,直至再次回到啞結點。
- 代碼實現:
void PreorderTraversal(ThreadedBinaryTreeNode root){
ThreadedBinaryTreeNode p=PreorderSuccessor(root);
while (p!=root){
p=PreorderSuccessor(p);
System.out.print(p.getData());
}
}
5.6.5、中序線索二叉樹插入結點
假設有兩個結點P和Q,現在要將Q連接到P的右邊。
-
策略:
(情況一):結點P沒有右孩子結點。在這種情況下,只需要將Q連接到P上,同時改變其左指針和右指針即可。如圖5-6所示。
圖5-6 情況一
(情況二):結點P有右孩子結點(假設為R)。在此情況下,需要遍歷R的左子樹,并找到最左邊的結點,然后更新這個結點的左指針和右指針。如圖5-7所示。
圖5-7 情況二 代碼實現:
void InsertRightInoderTBT(ThreadedBinaryTreeNode p,ThreadedBinaryTreeNode q){
ThreadedBinaryTreeNode temp;
q.setRight(p.getRight());
q.setRTag(p.getRTag());
q.setLeft(p);
q.setLTag(0);
p.setRTag(1);
if(q.getRTag()==1){
//第二種情況
temp=q.getRight();
while (temp.getLTag()==1){
temp=temp.getLeft();
}
temp.setLeft(q);
}
}
6、表達式樹
-
用來表示表達式的樹叫做表達式樹。在表達式樹中,葉子結點是操作數,而非葉子結點是操作符。下圖為表達式樹:(A+B*C)/D所對應的一個簡單表達式樹。
圖6-1 表達式樹
6.1、表達式樹舉例
- 策略:假定每次讀入一個符號。如果符號是操作數,就創建一個結點,并把指向該結點的指針入棧。如果符號是操作符,則從棧中彈出兩個指向樹T1和T2的指針,并產生一棵新樹,該樹以讀到的操作符作為根結點,兩個指針分別作為根結點的左孩子結點和右孩子結點,再將指向新樹的指針入棧。
-
實現:輸入后綴表達式為:A B C * + D /。
(1)、前3個符號是操作數,所以產生3個結點,并入棧,如圖6-2所示。
圖6-2 操作數入棧
(2)、接下來讀入操作符:*,因此棧中指向兩棵樹的指針出棧,形成一棵新樹,最后指向新樹的指針入棧,如圖6-3所示。
圖6-3 讀入操作符‘*’
(3)、接下來讀入操作符:+,因此棧中指向兩棵樹的指針出棧,形成一棵新樹,最后指向新樹的指針入棧,如圖6-4所示。
圖6-4 讀入操作符‘+’
(4)、接下來讀入操作數:D,產生包含一個結點的樹,將指向該樹的指針入棧,如圖6-5所示。
圖6-5 讀入操作數‘D’
(5)、最后讀入操作符:/,將棧中指針所對應的兩棵樹合并,并將指向最后樹的指針入棧,如圖6-6所示。
圖6-6 讀入操作符‘/’
6.2、表達式樹代碼實現
BinaryTreeNodeChar BuildExprTree(char[] postfixExpr,int size){
LLBinaryTreeStack s=new LLBinaryTreeStack();
for(int i=0;i<size;i++){
if(postfixExpr[i]!='+'&&postfixExpr[i]!='-'&&postfixExpr[i]!='*'&&postfixExpr[i]!='/'){
BinaryTreeNodeChar newNode=new BinaryTreeNodeChar(postfixExpr[i],null,null);
s.push2(newNode);
}
else {
BinaryTreeNodeChar T2=s.pop2();
BinaryTreeNodeChar T1=s.pop2();
BinaryTreeNodeChar newNode=new BinaryTreeNodeChar(postfixExpr[i],T1,T2);
s.push2(newNode);
}
}
return s.top2();
}
7、二叉搜索樹
7.1、二叉搜索樹的作用
- 二叉搜索樹(BST),主要是用來實現搜索操作,在這種表示中,對結點所包含的數據進行了一定的約束。因此它使得最壞情況下平均搜索的時間復雜度降低至O(logn)。
7.2、二叉搜索樹的性質
- 一個結點的左子樹只能包含鍵值小于該結點鍵值的結點。
- 一個結點的右子樹只能包含鍵值大于該結點鍵值的結點。
- 左子樹和右子樹必須都是二叉搜索樹。
7.3、二叉搜索樹的代碼實現
public class BinarySearchTreeNode {
private int data;
private BinarySearchTreeNode left;
private BinarySearchTreeNode right;
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public BinarySearchTreeNode getLeft() {
return left;
}
public void setLeft(BinarySearchTreeNode left) {
this.left = left;
}
public BinarySearchTreeNode getRight() {
return right;
}
public void setRight(BinarySearchTreeNode right) {
this.right = right;
}
}
7.4、二叉搜索樹的操作
7.4.1、在二叉搜索樹中尋找元素
- 從根結點開始,基于二叉搜索樹的性質移動到左子樹或者右子樹繼續搜索。如果待搜索數據和根結點數據一致,則返回當前結點。如果待搜索數據小于當前結點數據,則搜索當前結點的左子樹;否則,搜索當前結點的右子樹。如果數據不存在,則返回空指針。代碼如下:
BinarySearchTreeNode FindRecursive(BinarySearchTreeNode root,int data){
if(root==null){
return null;
}
if(data<root.getData()){
return FindRecursive(root.getLeft(),data);
}else if(data>root.getData()){
return FindRecursive(root.getRight(),data);
}
return root;
}
- 上述算法的非遞歸代碼實現如下:
BinarySearchTreeNode Find(BinarySearchTreeNode root,int data){
if(root==null){
return null;
}
while (root!=null){
if(data==root.getData()){
return root;
}else if(data<root.getData()){
root=root.getLeft();
}else {
root=root.getRight();
}
}
return null;
}
7.4.2、在二叉搜索樹中尋找最小元素
- 在二叉搜索樹中,最左邊的結點為最小元素,因為它沒有左子結點。代碼實現如下:
BinarySearchTreeNode FindMinRecursive(BinarySearchTreeNode root){
if(root==null){
return null;
}else {
if(root.getLeft()==null){
return root;
}else {
return FindMinRecursive(root.getLeft());
}
}
}
- 上述算法的非遞歸代碼實現如下:
BinarySearchTreeNode FindMin(BinarySearchTreeNode root){
if(root==null){
return null;
}
while (root.getLeft()!=null){
root=root.getLeft();
}
return root;
}
7.4.3、在二叉搜索樹中尋找最大元素
- 在二叉搜索樹中,最右邊的結點為最大元素,因為它沒有右子結點。代碼實現如下:
BinarySearchTreeNode FindMaxRecursive(BinarySearchTreeNode root){
if(root==null){
return null;
}else {
if(root.getRight()==null){
return root;
}else {
return FindMaxRecursive(root.getRight());
}
}
}
- 上述算法的非遞歸代碼實現如下:
BinarySearchTreeNode FindMax(BinarySearchTreeNode root){
if(root==null){
return null;
}
while (root.getRight()!=null){
root=root.getRight();
}
return root;
}
7.4.4、在二叉搜索樹中尋找中序序列前驅和后繼
-
假定二叉搜索樹中所有關鍵字值是唯一的,那么樹中結點X的中序序列前驅和后繼是什么?如果X有兩個孩子結點,那么中序序列前驅為其左子樹中值最大的元素,而其后繼為其右子樹中的最小元素。如圖7-1所示。
圖7-1 示例1 -
如果它沒有左孩子結點,則該結點中序序列前驅是其第一個左祖先結點。如圖7-2所示。
圖7-2 示例2
7.4.5、在二叉搜索樹中插入元素
-
策略:為了在二叉搜索樹中插入數據,首先需要找到插入該數據的位置。以圖7-3為例,虛線結點表示要插入的元素(5)。遍歷這棵樹,在鍵值4的節點處,需要訪問其右子樹,但是由于結點4沒有右子樹,所以5沒有在樹中,因此這就是要插入的位置。
圖7-3 插入元素示例 代碼實現:
BinarySearchTreeNode Insert(BinarySearchTreeNode root,int data){
if(root==null){
root=new BinarySearchTreeNode();
root.setData(data);
root.setLeft(null);
root.setRight(null);
}else {
if(data<root.getData()){
root.setLeft(Insert(root.getLeft(),data));
}else if(data>root.getData()){
root.setRight(Insert(root.getRight(),data));
}
}
return root;
}
7.4.6、在二叉搜索樹中刪除元素
首先找到待刪除元素的位置。
-
如果待刪除元素為葉子結點,則返回NULL給其雙親結點,即將其相應的孩子結點指針設置為NULL。如下圖7-4所示。在下面的樹中刪除5,將其雙親結點2的孩子指針設置為NULL。
圖7-4 刪除葉子結點 -
如果待刪除結點有一個孩子結點,在這中情況下,只需要將待刪除結點的孩子結點返回給雙親結點。如下圖7-5所示。要刪除4,4的左子樹設置為其雙親結點的一棵子樹。
圖7-5 刪除包含一個孩子結點 -
如果待刪除結點有兩個孩子結點,從其左子樹中找到最大元素來代替這個結點的主鍵,然后再刪除那個結點。左子樹中最大元素是沒有右孩子,第二次刪除操作是很容易完成的。如圖7-6所示。要刪除8,它是根節點的右孩子結點,主鍵是8。用它的左子樹(7)最大主鍵代替它,然后再刪除7這個結點。
圖7-6 刪除包含兩個孩子結點
【備注:圖7-6結點8的右子結點應改成10】
代碼實現:
BinarySearchTreeNode Delete(BinarySearchTreeNode root,int data){
BinarySearchTreeNode temp;
if(root==null){
System.out.print("Element not there in tree");
}
else if(data<root.getData()){
root.left=Delete(root.getLeft(),data);
}
else if(data>root.getData()){
root.right=Delete(root.getRight(),data);
}
else {//找到該元素
if(root.getLeft()!=null&&root.getRight()!=null){
//用左子樹的最大值代替
temp=FindMax(root.getLeft());
root.setData(temp.getData());
root.left=Delete(root.getLeft(),root.getData());
}else{
//一個孩子結點
if(root.getLeft()==null){
root=root.getRight();
}
if(root.getRight()==null){
root=root.getLeft();
}
}
}
return root;
}
8、平衡二叉搜索樹
- 不同類型的樹在最壞情況下搜索操作的復雜度為O(n)。而高度平衡樹用符號HB(k)表示,其中k為左右子樹的高度差,通過限制樹的高度可以將最壞情況下的時間復雜度降至O(logn)。
8.1、完全平衡二叉搜索樹
-
在HB(k)中,如果k=0那么就叫做完全平衡二叉樹,即左右子樹高度差最多為0。如下圖8-1所示。
圖8-1 完全平衡二叉樹
8.2、AVL樹
- 在HB(k)中,如果k=1,那么這樣的二叉搜索樹叫做AVL樹,左子樹和右子樹高度差不超過1。
8.2.1、AVL樹的性質
- 它是一棵二叉搜索樹。
- 對任意結點X,其左子樹的高度與其右子樹的高度差不超過1。
如下圖8-2 所示,左邊的樹不是AVL樹,右邊的是AVL樹。
8.2.2、AVL樹的最小/最大結點樹
假定AVL樹的高度是h,N(h)表示高度為h的AVL樹的結點數。
-
為了得到高度為h的AVL樹的最小結點數,應該盡可能少的結點來填充這棵樹。即假定填充左子樹的高度為h-1,那么右子樹的高度只能填充到h-2。這樣,最小結點數為:N(h)=N(h-1)+N(h-2)+1(1表示根結點)。求解該遞歸式可以得到:
-
為了獲得最大結點數,需要將左子樹和右子樹都填充到高度為h-1。這樣,最大結點數:N(h)=N(h-1)+N(h-1)+1。求解該遞歸式可以得到:
8.2.3、AVL樹的定義
- 因為AVL樹是一棵BST樹,所以AVL樹的定義類似于BST樹的定義。同時把高度也作為定義中的一部分。代碼實現如下:
public class AVLTreeNode {
private int data;
private int height;
private AVLTreeNode left;
private AVLTreeNode right;
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public AVLTreeNode getLeft() {
return left;
}
public void setLeft(AVLTreeNode left) {
this.left = left;
}
public AVLTreeNode getRight() {
return right;
}
public void setRight(AVLTreeNode right) {
this.right = right;
}
}
8.2.4、AVL樹的旋轉
- 定義:當樹的結構發生變化時(例如插入或刪除結點),就需要改變樹的結構來保證AVL樹的性質。這個操作可以用旋轉來實現。因為插入/刪除一個結點,將導致子樹的高度增加1或減少1,可能會導致某個結點X的左子樹和右子樹的高度差為2,而旋轉是用來保持AVL樹性質的技術。
- 策略:在插入結點后,只有插入結點到根路徑上的結點的平衡因子(左右子樹的高度差)才可能改變。為了恢復AVL樹的性質,需要在該路徑上找到第一個不滿足AVL樹性質的結點,從這個結點到根結點的路徑上的每個結點也就都存在這個問題。所以,如果修復第一個結點的問題,那么其他結點都將自動滿足AVL樹的性質。
- 類型:假定X結點是必須要重新平衡的結點,有以下4種情況可能違反AVL樹性質。
1)在結點X的左孩子結點的左子樹中插入元素。
2)在結點X的左孩子結點的右子樹中插入元素。
3)在結點X的右孩子結點的左子樹中插入元素。
4)在結點X的右孩子結點的右子樹中插入元素。
8.2.4.1、單旋轉
-
左左旋轉(LL旋轉)(8.2.4情況1):從插入結點處開始,向上遍歷樹,并更新這個路徑上每個結點的平衡信息。例如圖8-3所示,在左邊原始AVL樹插入7后,結點9變為非平衡的(結點7插入在結點9的左孩子的左子樹中)。執行左左旋轉,得到右邊這棵樹。
圖8-3 左左旋轉
如圖8-4所示,好比在結點X,對它的左孩子結點W的左子樹A中插入一個結點,平衡被破壞,執行左左旋轉后,得到右邊這棵樹。結點X重新平衡;子樹A因為插入新結點,高度增加1,同時X變為W的右孩子結點,結點W重新平衡。
圖8-4 左左旋轉(2)
代碼實現如下:
AVLTreeNode SingleRotateLeft(AVLTreeNode X){
AVLTreeNode W=X.getLeft();
//將結點W的右子樹設置為結點X的左孩子
X.setLeft(W.getRight());
//將結點X設置為W的右孩子
W.setRight(X);
//計算結點X的高度
X.setHeight(Math.max(Height(X.getLeft()),Height(X.getRight()))+1);
//計算結點W的高度
W.setHeight(Math.max(Height(W.getLeft()),X.getHeight())+1);
return W;
}
-
右右旋轉(RR旋轉)(8.2.4情況4):例如圖8-5所示,在左邊原始AVL樹插入20后,結點10變為非平衡的(結點20插入在結點10的右孩子的右子樹中)。執行右右旋轉,得到右邊這棵樹。
圖8-5 右右旋轉
如圖8-6所示,好比在結點W,對它的右孩子結點X的右子樹C中插入一個結點,平衡被破壞,執行右右旋轉后,得到右邊這棵樹。結點W重新平衡;子樹C因為插入新結點,高度增加1,同時W變為X的左孩子結點,結點X重新平衡。
圖8-6 右右旋轉(2)
代碼實現如下:
AVLTreeNode SingleRotateRight(AVLTreeNode W){
AVLTreeNode X=W.getRight();
//將結點X的左子樹設置為W的右孩子
W.setRight(X.getLeft());
//將結點W設置為結點X的左孩子
X.setLeft(W);
//計算結點W的高度
W.setHeight(Math.max(Height(W.getLeft()),Height(W.getRight()))+1);
//計算結點X的高度
X.setHeight(Math.max(Height(X.getRight()),W.getHeight())+1);
return X;
}
8.2.4.2、雙旋轉
-
左右旋轉(LR旋轉)(8.2.4情況2):例如圖8-7所示,在左邊原始AVL樹插入7后,結點8變為非平衡的(結點7插入在結點8的左孩子的右子樹中)。執行兩次單旋轉,得到右邊這棵樹。
圖8-7 左右旋轉
如圖8-8所示,好比在結點Z,對它的左孩子結點X的右子樹C中插入一個結點,平衡被破壞。此時先對X結點進行右旋轉,完成之后再對Z結點進行左旋轉。最后便重新得到一棵AVL樹。
圖8-8 左右旋轉(2)
代碼實現如下:
AVLTreeNode DoubleRotatewithLeft(AVLTreeNode Z){
Z.setLeft(SingleRotateRight(Z.getLeft()));
return SingleRotateLeft(Z);
}
-
右左旋轉(RL旋轉)(8.2.4情況3):例如圖8-9所示,在左邊原始AVL樹插入5后,結點4變為非平衡的(結點6插入在結點4的右孩子的左子樹中)。執行兩次單旋轉,得到右邊這棵樹。
【備注:圖8-9的中間部分的根結點應為:4】
圖8-9 右左旋轉
如圖8-10所示,好比在結點X,對它的右孩子結點Z的左子樹B中插入一個結點,平衡被破壞。此時先對Z結點進行左旋轉,完成之后再對X結點進行右旋轉。最后便重新得到一棵AVL樹。
圖8-10 右左旋轉(2)
代碼實現如下:
AVLTreeNode DoubleRotatewithRight(AVLTreeNode X){
X.setLeft(SingleRotateLeft(X.getRight()));
return SingleRotateRight(X);
}
8.2.5、AVL樹的插入操作
- AVL樹的插入類似BST樹的插入。在插入一個元素后,需要檢查高度是否平衡。如果不平衡,需要調用相應的旋轉函數。代碼實現如下:
AVLTreeNode Insert(AVLTreeNode root,AVLTreeNode parent,int data){
if(root==null){
root=new AVLTreeNode();
root.setData(data);
root.setHeight(0);
root.setLeft(null);
root.setRight(null);
}
else if(data<root.getData()){
root.setLeft(Insert(root.getLeft(),root,data));
if((Height(root.getLeft())-Height(root.getRight()))==2){
if(data<root.getLeft().getData()){
//插入在左孩子的左子樹中,對root執行左左旋轉操作
root=SingleRotateLeft(root);
}else {
//插入在左孩子的右子樹中,對root執行左右旋轉操作
root=DoubleRotatewithLeft(root);
}
}
}
else if(data>root.getData()){
root.setRight(Insert(root.getRight(),root,data));
if((Height(root.getRight())-Height(root.getLeft()))==2){
if(data>root.getRight().getData()){
//插入在右孩子的右子樹中,對root執行右右旋轉操作
root=SingleRotateRight(root);
}else {
//插入在右孩子的左子樹中,對root執行右左旋轉操作
root=DoubleRotatewithRight(root);
}
}
}
/*
否則,數據已經在樹中,不必做任何操作
*/
root.setHeight(Math.max(Height(root.getLeft()),Height(root.getRight()))+1);
return root;
}