1. 程式人生 > >平衡二叉樹的java實現

平衡二叉樹的java實現

14. bubuko 概念 tro 細心 後繼 inf 特點 演變

轉載請註明出處!

一、概念

平衡二叉樹是一種特殊的二叉搜索樹,關於二叉搜索樹,請查看上一篇博客二叉搜索樹的java實現,那它有什麽特別的地方呢,了解二叉搜索樹的基本都清楚,在按順序向插入二叉搜索樹中插入值,最後會形成一個類似鏈表形式的樹,而我們設計二叉搜索樹的初衷,顯然是看中了它的查找速度與它的高度成正比,如果每一顆二叉樹都像鏈表一樣,那就沒什麽意思了,所以就設計出來了平衡二叉樹,相對於二叉搜索樹,平衡二叉樹的一個特點就是,在該樹中,任意一個節點,它的左右子樹的差的絕對值一定小於2。關於它的演變什麽的,請自行網上搜索答案。在本文中,為了方便,也是采用了int型值插入。

二、平衡二叉樹的構建

它的類相對於二叉搜索樹也並沒有什麽特殊之處,不做過多講解,直接上代碼

 1 static class Node{
 2         Node parent;
 3         Node leftChild;
 4         Node rightChild;
 5         int val;
 6         public Node(Node parent, Node leftChild, Node rightChild,int val) {
 7             super();
 8             this.parent = parent;
 9
this.leftChild = leftChild; 10 this.rightChild = rightChild; 11 this.val = val; 12 } 13 14 public Node(int val){ 15 this(null,null,null,val); 16 } 17 18 public Node(Node node,int val){ 19 this
(node,null,null,val); 20 } 21 22 }

三、增加

在談及平衡二叉樹的增加,先來考慮什麽樣的情況會打破這個平衡,假如A樹已經是一顆平衡二叉樹,但現在要往裏面插入一個元素,有這兩種結果,一、平衡未打破,這種肯定皆大歡喜,二、平衡被打破了。那一般要考慮三個問題

一、平衡被打破之前是什麽狀態?

二、被打破之後又是一個什麽樣的狀態?

三、平衡被打破了,該怎麽調整,使它又重新成為一個平衡二叉樹呢?

這裏截取打破平衡後左子樹的高度比右子樹高度高2的所有可能情況(若右子樹高,情況一樣,這裏只選取一種分析),下面的圖,只是代表著那個被打破平衡的點的子樹(被打破平衡的點就是這個節點的左右子樹高度差的絕對值大於或等於2,當然,這裏只能等於2),不代表整棵樹。

技術分享圖片

這是第一種情況,其中A節點和B節點只是平衡二叉樹的某一個子集合,要想打破這個平衡,那麽插入的節點C必然在B的子節點上,即左右子節點,調整後面一起說

技術分享圖片

這是第二種情況,其中A、B、C、D四個節點也是該平衡樹的某個子集合,同樣要打破這個平衡,那麽,插入的節點F必然在D節點上。

技術分享圖片

第三種情況,其中A、B、C、D、E五個節點也是該平衡樹的某個子集合,同樣要打破這個平衡,那麽,插入的節點F必然在D節點和E節點上。

(我個人所想到的所有可能情況,若還有其他情況,請在評論中指出,謝謝!)

或許細心的人已經發現,第二種和第三種情況就是由第一種情況變化而來的,如分別在A節點的右孩子和B節點的右孩子上添加子節點,就變化成了第二種和第三種情況(這裏並不是說第一種情況直接加上這些節點就變成了第二或第三種情況)

這裏只詳細分析第一種情況

技術分享圖片

要使A節點的左右子樹差的絕對值小於2,此時只需將B節點來替換A節點,A節點成為B節點的右孩子。若A節點有父節點,則A的父節點的子節點要去指向B節點,而A節點的父節點要去指向B節點,先看這段代碼吧。這段操作也就是右旋操作

 1 /**
 2      * 在這種情況,因為A和B節點均沒有右孩子節點,
 3      * 所以不用考慮太多
 4      * @param aNode 代表A節點
 5      * @return
 6      */
 7     public Node leftRotation(Node aNode){
 8         if(aNode != null){
 9             Node bNode = aNode.leftChild;// 先用一個變量來存儲B節點
10             bNode.parent = aNode.parent;// 重新分配A節點的父節點指向
11             //判斷A節點的父節點是否存在
12             if(aNode.parent != null){// A節點不是根節點
13                 /**
14                  * 分兩種情況
15                  *   1、A節點位於其父節點左邊,則B節點也要位於左邊
16                  *   2、A節點位於其父節點右邊,則B節點也要位於右邊
17                  */
18                 if(aNode.parent.leftChild == aNode){
19                     aNode.parent.leftChild = bNode;
20                 }else{
21                     aNode.parent.rightChild = bNode;
22                 }
23             }else{// 說明A節點是根節點,直接將B節點置為根節點
24                 this.root = bNode;
25             }
26             bNode.rightChild = aNode;// 將B節點的右孩子置為A節點
27             aNode.parent = bNode;// 將A節點的父節點置為B節點
28             return bNode;// 返回旋轉的節點
29         }
30         return null;
31     }

而對於第一種情況的這個圖

技術分享圖片

涉及的情況又不一樣,假如按照上面那種情況一樣去右旋,那麽得到的圖是或許是這樣的

技術分享圖片

這好像又不平衡,似乎和原圖是一個對稱的。不太行的通。如果將C節點替換B節點位置,而B節點成為C節點的左節點,這樣就成為了上一段代碼的那種情況。這段B節點替換成為C節點的代碼如下,這裏操作也就是,先左旋後右旋

 1 /**
 2      * 
 3      * @param bNode 代表B節點
 4      * @return
 5      */
 6     public Node rightRotation(Node bNode){
 7         if(bNode != null){
 8             Node cNode = bNode.rightChild;// 用臨時變量存儲C節點
 9             cNode.parent = bNode.parent;
10             // 這裏因為bNode節點父節點存在,所以不需要判斷。加判斷也行,
11             if(bNode.parent.rightChild == bNode){
12                 bNode.parent.rightChild = cNode;
13             }else{
14                 bNode.parent.leftChild = cNode;
15             }
16             cNode.leftChild = bNode;
17             bNode.parent = cNode;
18             return cNode;
19         }
20         return null;
21     }

代碼邏輯和上一段代碼一樣。變換過來後,再按照上面的右旋再操作一次,就變成了平衡樹了。

對於第二種和第三種情況的分析和第一種類似,再把代碼修改一下,適合三種情況,即可。完整代碼如下。

 1      public Node rightRotation(Node node){
 2         if(node != null){
 3             Node leftChild = node.leftChild;// 用變量存儲node節點的左子節點
 4             node.leftChild = leftChild.rightChild;// 將leftChild節點的右子節點賦值給node節點的左節點
 5             if(leftChild.rightChild != null){// 如果leftChild的右節點存在,則需將該右節點的父節點指給node節點
 6                 leftChild.rightChild.parent = node;
 7             }
 8             leftChild.parent = node.parent;
 9             if(node.parent == null){// 即表明node節點為根節點
10                 this.root = leftChild;
11             }else if(node.parent.rightChild == node){// 即node節點在它原父節點的右子樹中
12                 node.parent.rightChild = leftChild;
13             }else if(node.parent.leftChild == node){
14                 node.parent.leftChild = leftChild;
15             }
16             leftChild.rightChild = node;
17             node.parent = leftChild;
18             return leftChild;
19         }
20         return null;
21     }

以上是右旋代碼。邏輯參考以上分析

 1     public Node leftRotation(Node node){
 2         if(node != null){
 3             Node rightChild = node.rightChild;
 4             node.rightChild = rightChild.leftChild;
 5             if(rightChild.leftChild != null){
 6                 rightChild.leftChild.parent = node;
 7             }
 8             rightChild.parent = node.parent;
 9             if(node.parent == null){
10                 this.root = rightChild;
11             }else if(node.parent.rightChild == node){
12                 node.parent.rightChild = rightChild;
13             }else if(node.parent.leftChild == node){
14                 node.parent.leftChild = rightChild;
15             }
16             rightChild.leftChild = node;
17             node.parent = rightChild;
18             
19         }
20         return null;
21     }

至此,打破平衡後,經過一系列操作達到平衡,由以上可知,大致有以下四種操作情況

一、只需要經過一次右旋即可達到平衡

二、只需要經過一次左旋即可達到平衡

三、需先經過左旋,再經過右旋也可達到平衡

四、需先經過右旋,再經過左旋也可達到平衡

那問題就來了,怎麽判斷被打破的平衡要經歷哪種操作才能達到平衡呢?

經過了解,這四種情況,還可大致分為兩大類,如下(以下的A節點就是被打破平衡的那個節點)

第一大類,A節點的左子樹高度比右子樹高度高2,最終需要經過右旋操作(可能需要先左後右)

第二大類,A節點的左子樹高度比右子樹高度低2,最終需要經過左旋操作(可能需要先右後左)

所以很容易想到,在插入節點後,判斷插入的節點是在A節點的左子樹還是右子樹(因為插入之前已經是平衡二叉樹)再決定采用哪個大類操作,在大類操作裏再去細分要不要經歷兩步操作。

插入元素代碼如下

 1     public boolean put(int val){
 2         return putVal(root,val);
 3     }
 4     private boolean putVal(Node node,int val){
 5         if(node == null){// 初始化根節點
 6             node = new Node(val);
 7             root = node;
 8             size++;
 9             return true;
10         }
11         Node temp = node;
12         Node p;
13         int t;
14         /**
15          * 通過do while循環叠代獲取最佳節點,
16          */
17         do{ 
18             p = temp;
19             t = temp.val-val;
20             if(t > 0){
21                 temp = temp.leftChild;
22             }else if(t < 0){
23                 temp = temp.rightChild;
24             }else{
25                 temp.val = val;
26                 return false;
27             }
28         }while(temp != null);
29         Node newNode = new Node(p, val);
30         if(t > 0){
31             p.leftChild = newNode;
32         }else if(t < 0){
33             p.rightChild = newNode;
34         }
35         rebuild(p);// 使二叉樹平衡的方法
36         size++;
37         return true;
38     }

這部分代碼,詳細分析可看上一篇博客,二叉搜索樹的java實現。繼續看rebuild方法的代碼,這段代碼采用了從插入節點父節點進行向上回溯去查找失去平衡的節點

 1     private void rebuild(Node p){
 2         while(p != null){
 3             if(calcNodeBalanceValue(p) == 2){// 說明左子樹高,需要右旋或者 先左旋後右旋
 4                 fixAfterInsertion(p,LEFT);// 調整操作
 5             }else if(calcNodeBalanceValue(p) == -2){
 6                 fixAfterInsertion(p,RIGHT);
 7             }
 8             p = p.parent;
 9         }
10     }

那個calcNodeBalanceValue方法就是計算該參數的左右子樹高度之差的方法。fixAfterInsertion方法是根據不同類型進行不同調整的方法,代碼如下

 1     private int calcNodeBalanceValue(Node node){
 2             if(node != null){
 3                 return getHeightByNode(node);
 4             }
 5             return 0;
 6     }
 7     // 計算node節點的高度
 8     public int getChildDepth(Node node){
 9         if(node == null){
10             return 0;
11         }
12         return 1+Math.max(getChildDepth(node.leftChild),getChildDepth(node.rightChild));
13     }
14     public int getHeightByNode(Node node){
15         if(node == null){
16             return 0;
17         }
18         return getChildDepth(node.leftChild)-getChildDepth(node.rightChild);
19     }

 1 /**
 2      * 調整樹結構
 3      * @param p
 4      * @param type
 5      */
 6     private void fixAfterInsertion(Node p, int type) {
 7         // TODO Auto-generated method stub
 8         if(type == LEFT){
 9             final Node leftChild = p.leftChild;
10             if(leftChild.leftChild != null){//右旋
11                 rightRotation(p);
12             }else if(leftChild.rightChild != null){// 先左旋後右旋
13                 leftRotation(leftChild);
14                 rightRotation(p);
15             }
16         }else{
17             final Node rightChild = p.rightChild;
18             if(rightChild.rightChild != null){// 左旋
19                 leftRotation(p);
20             }else if(rightChild.leftChild != null){// 先右旋,後左旋
21                 rightRotation(p);
22                 leftRotation(rightChild);
23             }
24         }
25     }

在對每個大類再具體分析,我這裏采用了 左右子樹是否為空的判斷來決定它是單旋還是雙旋,我思考的原因:如果代碼執行到了這個方法,那麽肯定平衡被打破了,就暫且拿第一個大類來說 ,A的左子樹高度要比右子樹高2,意味平衡被打破了,再去結合上面分析的第一種情況,當插入元素後樹結構是以下結構,那肯定是單旋

技術分享圖片

如果是以下結構,那肯定是這種結構,由上面分析,這種結構必須的雙旋。

技術分享圖片

除了這兩種情況,並沒有其他旋轉情況了。所以,我這裏是根據插入的節點是位於B節點的左右方來決定是單旋還是雙旋,(在這裏,不保證結論完全正確,若有錯誤,還望大家指正)。

以上就是平衡二叉樹的插入操作,以及後續的調整操作代碼

四、刪除

先來上一段二叉樹的刪除代碼,關於具體的刪除邏輯,請查看上一篇博客,這裏只討論重調整操作

  1 p);
  2             }else if(rightChild.leftChild != null){// 先右旋,後左旋
  3                 rightRotation(p);
  4                 leftRotation(rightChild);
  5             }
  6         }
  7     }
  8     private int calcNodeBalanceValue(Node node){
  9             if(node != null){
 10                 return getHeightByNode(node);
 11             }
 12             return 0;
 13     }
 14     public void print(){
 15         print(this.root);
 16     }
 17     public Node getNode(int val){
 18         Node temp = root;
 19         int t;
 20         do{
 21             t = temp.val-val;
 22             if(t > 0){
 23                 temp = temp.leftChild;
 24             }else if(t < 0){
 25                 temp = temp.rightChild;
 26             }else{
 27                 return temp;
 28             }
 29         }while(temp != null);
 30         return null;
 31     }
 32     public boolean delete(int val){
 33         Node node = getNode(val);
 34         if(node == null){
 35             return false;
 36         }
 37         boolean flag = false;
 38         Node p = null;
 39         Node parent = node.parent;
 40         Node leftChild = node.leftChild;
 41         Node rightChild = node.rightChild;
 42         //以下所有父節點為空的情況,則表明刪除的節點是根節點
 43         if(leftChild == null && rightChild == null){//沒有子節點
 44             if(parent != null){
 45                 if(parent.leftChild == node){
 46                     parent.leftChild = null;
 47                 }else if(parent.rightChild == node){
 48                     parent.rightChild = null;
 49                 }
 50             }else{//不存在父節點,則表明刪除節點為根節點
 51                 root = null;
 52             }
 53             p = parent;
 54             node = null;
 55             flag =  true;
 56         }else if(leftChild == null && rightChild != null){// 只有右節點
 57             if(parent != null && parent.val > val){// 存在父節點,且node位置為父節點的左邊
 58                 parent.leftChild = rightChild;
 59             }else if(parent != null && parent.val < val){// 存在父節點,且node位置為父節點的右邊
 60                 parent.rightChild = rightChild;
 61             }else{
 62                 root = rightChild;
 63             }
 64             p = parent;
 65             node = null;
 66             flag =  true;
 67         }else if(leftChild != null && rightChild == null){// 只有左節點
 68             if(parent != null && parent.val > val){// 存在父節點,且node位置為父節點的左邊
 69                 parent.leftChild = leftChild;
 70             }else if(parent != null && parent.val < val){// 存在父節點,且node位置為父節點的右邊
 71                 parent.rightChild = leftChild;
 72             }else{
 73                 root = leftChild;
 74             }
 75             p = parent;
 76             flag =  true;
 77         }else if(leftChild != null && rightChild != null){// 兩個子節點都存在
 78             Node successor = getSuccessor(node);// 這種情況,一定存在後繼節點
 79             int temp = successor.val;
 80             boolean delete = delete(temp);
 81             if(delete){
 82                 node.val = temp;
 83             }
 84             p = successor;
 85             successor = null;
 86             flag =  true;
 87         }
 88         if(flag){
 89             rebuild(p);
 90         }
 91         return flag;
 92     }
 93     
 94     /**
 95      * 找到node節點的後繼節點
 96      * 1、先判斷該節點有沒有右子樹,如果有,則從右節點的左子樹中尋找後繼節點,沒有則進行下一步
 97      * 2、查找該節點的父節點,若該父節點的右節點等於該節點,則繼續尋找父節點,
 98      *   直至父節點為Null或找到不等於該節點的右節點。
 99      * 理由,後繼節點一定比該節點大,若存在右子樹,則後繼節點一定存在右子樹中,這是第一步的理由
100      *      若不存在右子樹,則也可能存在該節點的某個祖父節點(即該節點的父節點,或更上層父節點)的右子樹中,
101      *      對其叠代查找,若有,則返回該節點,沒有則返回null
102      * @param node
103      * @return
104      */
105     private Node getSuccessor(Node node){
106         if(node.rightChild != null){
107             Node rightChild = node.rightChild;
108             while(rightChild.leftChild != null){
109                 rightChild = rightChild.leftChild;
110             }
111             return rightChild;
112         }
113         Node parent = node.parent;
114         while(parent != null && (node == parent.rightChild)){
115             node = parent;
116             parent = parent.parent;
117         }
118         return parent;
119     }

這裏也采用了插入操作的調整平衡代碼。不做過多分析

五、遍歷

這裏采用了兩種遍歷,一種是中序遍歷打印數據,第二種是層次遍歷,以便查看調整後的數據是否正確

中序遍歷

 1     public void print(){
 2         print(this.root);
 3     }
 4     private void print(Node node){
 5         if(node != null){
 6             print(node.leftChild);
 7             System.out.println(node.val+",");
 8             print(node.rightChild);
 9         }
10     }

層次遍歷

 1 /**
 2      * 層次遍歷
 3      */
 4     public void printLeft(){
 5         if(this.root == null){
 6             return;
 7         }
 8         Queue<Node> queue = new LinkedList<>();
 9         Node temp = null;
10         queue.add(root);
11         while(!queue.isEmpty()){
12             temp = queue.poll();
13             System.out.print("節點值:"+temp.val+",平衡值:"+calcNodeBalanceValue(temp)+"\n");
14             if(temp.leftChild != null){
15                 queue.add(temp.leftChild);
16             }
17             if(temp.rightChild != null){
18                 queue.add(temp.rightChild);
19             }
20         }
21     }

六、測試

測試代碼如下

 1 @Test
 2     public void test_balanceTree(){
 3         BalanceBinaryTree bbt = new BalanceBinaryTree();
 4         bbt.put(10);
 5         bbt.put(9);
 6         bbt.put(11);
 7         bbt.put(7);
 8         bbt.put(12);
 9         bbt.put(8);
10         bbt.put(38);
11         bbt.put(24);
12         bbt.put(17);
13         bbt.put(4);
14         bbt.put(3);
15         System.out.println("----刪除前的層次遍歷-----");
16         bbt.printLeft();
17         System.out.println("------中序遍歷---------");
18         bbt.print();
19         System.out.println();
20         bbt.delete(9);
21         System.out.println("----刪除後的層次遍歷-----");
22         bbt.printLeft();
23         System.out.println("------中序遍歷---------");
24         bbt.print();
25     }

運行結果

技術分享圖片技術分享圖片

-------------------------------------------------------------------------------------------分界線----------------------------------------------------------------------------------------------------------------------------------------------

以上就是我對平衡二叉樹的了解,若有不足或錯誤之處,還望指正,謝謝!

平衡二叉樹的java實現