轉載請註明出處!html
平衡二叉樹是一種特殊的二叉搜索樹,關於二叉搜索樹,請查看上一篇博客二叉搜索樹的java實現,那它有什麼特別的地方呢,瞭解二叉搜索樹的基本都清楚,在按順序向插入二叉搜索樹中插入值,最後會造成一個相似鏈表形式的樹,而咱們設計二叉搜索樹的初衷,顯然是看中了它的查找速度與它的高度成正比,若是每一顆二叉樹都像鏈表同樣,那就沒什麼意思了,因此就設計出來了平衡二叉樹,相對於二叉搜索樹,平衡二叉樹的一個特色就是,在該樹中,任意一個節點,它的左右子樹的差的絕對值必定小於2。關於它的演變什麼的,請自行網上搜索答案。在本文中,爲了方便,也是採用了int型值插入。java
它的類相對於二叉搜索樹也並無什麼特殊之處,不作過多講解,直接上代碼node
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樹已是一顆平衡二叉樹,但如今要往裏面插入一個元素,有這兩種結果,1、平衡未打破,這種確定皆大歡喜,2、平衡被打破了。那通常要考慮三個問題測試
1、平衡被打破以前是什麼狀態?ui
2、被打破以後又是一個什麼樣的狀態?this
3、平衡被打破了,該怎麼調整,使它又從新成爲一個平衡二叉樹呢?spa
這裏截取打破平衡後左子樹的高度比右子樹高度高2的全部可能狀況(若右子樹高,狀況同樣,這裏只選取一種分析),下面的圖,只是表明着那個被打破平衡的點的子樹(被打破平衡的點就是這個節點的左右子樹高度差的絕對值大於或等於2,固然,這裏只能等於2),不表明整棵樹。設計
這是第一種狀況,其中A節點和B節點只是平衡二叉樹的某一個子集合,要想打破這個平衡,那麼插入的節點C必然在B的子節點上,即左右子節點,調整後面一塊兒說3d
這是第二種狀況,其中A、B、C、D四個節點也是該平衡樹的某個子集合,一樣要打破這個平衡,那麼,插入的節點F必然在D節點上。code
第三種狀況,其中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 * 一、A節點位於其父節點左邊,則B節點也要位於左邊 16 * 二、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 }
至此,打破平衡後,通過一系列操做達到平衡,由以上可知,大體有如下四種操做狀況
1、只須要通過一次右旋便可達到平衡
2、只須要通過一次左旋便可達到平衡
3、需先通過左旋,再通過右旋也可達到平衡
4、需先通過右旋,再通過左旋也可達到平衡
那問題就來了,怎麼判斷被打破的平衡要經歷哪一種操做才能達到平衡呢?
通過了解,這四種狀況,還可大體分爲兩大類,以下(如下的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 * 調整樹結構 ,該方法有瑕疵,本想直接修改,但爲了起參考做用就留在這,正解見評論。修改與2019-08-03 12:13 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 * 一、先判斷該節點有沒有右子樹,若是有,則從右節點的左子樹中尋找後繼節點,沒有則進行下一步 97 * 二、查找該節點的父節點,若該父節點的右節點等於該節點,則繼續尋找父節點, 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 }
運行結果
-------------------------------------------------------------------------------------------分界線----------------------------------------------------------------------------------------------------------------------------------------------
以上就是我對平衡二叉樹的瞭解,如有不足或錯誤之處,還望指正,謝謝!