目錄javascript
@java
平衡二叉樹又稱AVL樹。它能夠是一顆空樹,或者具備如下性質的二叉排序樹:它的左子樹和右子樹的高度之差(平衡因子)的絕對值不超過1且它的左子樹和右子樹都是一顆平衡二叉樹。node
從上面簡單的定義咱們能夠得出幾個重要的信息:編程
在定義中提到了樹的高度和深度,我敢確定有不少讀者必定對樹的高度和深度有所誤解!最可愛的誤解就是樹的高度和深度沒有區別,認爲樹的高度就是深度。宜春就忍不了了,必須得嗶嗶幾句...設計模式
樹的高度和深度本質區別:深度是從根節點數到它的葉節點,高度是從葉節點數到它的根節點。數組
二叉樹的深度是從根節點開始自頂向下逐層累加的;而二叉樹高度是從葉節點開始自底向上逐層累加的。雖然樹的深度和高度同樣,可是具體到樹的某個節點,其深度和高度是不同的。併發
其次就是對樹的高度和深度是從1數起,仍是從0數起。固然我也有本身的答案,可是衆說紛紜,博主就不說其對與錯了,就很少嗶嗶了。可是我仍是比較認同這張圖的觀點:
可參考:https://www.zhihu.com/question/40286584ide
判斷一棵平衡二叉樹(AVL樹)有以下必要條件:學習
條件一:必須是二叉搜索樹。
條件二:每一個節點的左子樹和右子樹的高度差至多爲1。測試
很少嗶嗶,平衡因子 = 左子樹深度/高度 - 右子樹深度/高度
對於上圖平衡二叉樹而言:
5的結點平衡因子就是 3 - 2 = 1;
2的結點平衡因子就是 1 - 2 = -1;
4的結點平衡因子就是 1 - 0 = 1;
6的結點平衡因子就是 0 - 1 = -1;
對於上圖非平衡二叉樹而言:
3 的結點平衡因子就是 2 - 4 = -2;
1 的結點平衡因子就是 0 - 1 = -1;
4 的結點平衡因子就是 0 - 3 = -3;
5 的結點平衡因子就是 0 - 2 = -2;
6 的結點平衡因子就是 0 - 1 = -1;
特別注意:葉子結點平衡因子都是爲 0
因爲普通的二叉查找樹會容易失去」平衡「,極端狀況下,二叉查找樹會退化成線性的鏈表,致使插入和查找的複雜度降低到 O(n)
,因此,這也是平衡二叉樹設計的初衷。那麼平衡二叉樹如何保持」平衡「呢?
不難看出平衡二叉樹是一棵高度平衡的二叉查找樹。因此,要構建跟維繫一棵平衡二叉樹就比普通的二叉樹要複雜的多。在構建一棵平衡二叉樹的過程當中,當有新的節點要插入時,檢查是否因插入後而破壞了樹的平衡,若是是,則須要作旋轉去改變樹的結構。關於旋轉,我相信使用文字描述是很難表達清楚的,仍是得靠經典的兩個圖來理解最好不過了!不要不信噢,固然你能夠嘗試讀下文字描述左旋:
左旋簡單來講就是將節點的右支往左拉,右子節點變成父節點,並把晉升以後多餘的左子節點出讓給降級節點的右子節點。
相信你已經暈了。固然能夠試着看看下面的經典動圖理解!
左旋:
==試着用動態和下面的左旋結果圖分析分析,想象一下,估計分分鐘明白左旋!!!==
//左旋轉方法代碼 private void leftRotate() { //建立新的結點,以當前根結點的值 Node newNode = new Node(value); //把新的結點的左子樹設置成當前結點的左子樹 newNode.left = left; //把新的結點的右子樹設置成帶你過去結點的右子樹的左子樹 newNode.right = right.left; //把當前結點的值替換成右子結點的值 value = right.value; //把當前結點的右子樹設置成當前結點右子樹的右子樹 right = right.right; //把當前結點的左子樹(左子結點)設置成新的結點 left = newNode; }
相應的右旋就很好理解了:
反之就是右旋,這裏就再也不舉例了!
小結:當二叉排序樹每一個節點的左子樹和右子樹的高度差超過1的時候,就須要經過旋轉節點來維持平衡!旋轉又分爲左旋、右旋、雙旋轉。
啥?雙旋轉?是的,顧名思義,在一些添加節點的狀況下旋轉一次是不能達到平衡的,須要進行第二次旋轉,
當新節點插入後,有可能會有致使樹不平衡,而可能出現的狀況就有4種,分別稱做左左,左右,右左,右右。
==而所謂的「左」和「右」無非就是表明新節點所插入的位置是左仍是右!==
第一個左右表明位於根節點的左或者右,
第二個左右表明位於 【最接近插入節點的擁有兩個子節點的父節點】 位置的左或者右
==固然針對於第二個左右是我我的的看法,不必定徹底正確。有本身想法的讀者,歡迎留言指正!==
下面以左左爲例,分析一波:
其中要特別注意的是:
右右、左左只須要旋轉一次就能夠平衡。
左右、右左要旋轉兩次才能把樹調整平衡!
==其中旋轉的條件就是:當二叉排序樹每一個節點的左子樹和右子樹的高度差超過1的時候!==
// 建立AVLTree class AVLTree { private Node root; public Node getRoot() { return root; } // 查找要刪除的結點 public Node search(int value) { if (root == null) { return null; } else { return root.search(value); } } // 查找父結點 public Node searchParent(int value) { if (root == null) { return null; } else { return root.searchParent(value); } } // 編寫方法: // 1. 返回的 以node 爲根結點的二叉排序樹的最小結點的值 // 2. 刪除node 爲根結點的二叉排序樹的最小結點 /** * * @param node 傳入的結點(當作二叉排序樹的根結點) * * @return 返回的 以node 爲根結點的二叉排序樹的最小結點的值 */ public int delRightTreeMin(Node node) { Node target = node; // 循環的查找左子節點,就會找到最小值 while (target.left != null) { target = target.left; } // 這時 target就指向了最小結點 // 刪除最小結點 delNode(target.value); return target.value; } // 刪除結點 public void delNode(int value) { if (root == null) { return; } else { // 1.需求先去找到要刪除的結點 targetNode Node targetNode = search(value); // 若是沒有找到要刪除的結點 if (targetNode == null) { return; } // 若是咱們發現當前這顆二叉排序樹只有一個結點 if (root.left == null && root.right == null) { root = null; return; } // 去找到targetNode的父結點 Node parent = searchParent(value); // 若是要刪除的結點是葉子結點 if (targetNode.left == null && targetNode.right == null) { // 判斷targetNode 是父結點的左子結點,仍是右子結點 if (parent.left != null && parent.left.value == value) { // 是左子結點 parent.left = null; } else if (parent.right != null && parent.right.value == value) {// 是由子結點 parent.right = null; } } else if (targetNode.left != null && targetNode.right != null) { // 刪除有兩顆子樹的節點 int minVal = delRightTreeMin(targetNode.right); targetNode.value = minVal; } else { // 刪除只有一顆子樹的結點 // 若是要刪除的結點有左子結點 if (targetNode.left != null) { if (parent != null) { // 若是 targetNode 是 parent 的左子結點 if (parent.left.value == value) { parent.left = targetNode.left; } else { // targetNode 是 parent 的右子結點 parent.right = targetNode.left; } } else { root = targetNode.left; } } else { // 若是要刪除的結點有右子結點 if (parent != null) { // 若是 targetNode 是 parent 的左子結點 if (parent.left.value == value) { parent.left = targetNode.right; } else { // 若是 targetNode 是 parent 的右子結點 parent.right = targetNode.right; } } else { root = targetNode.right; } } } } } // 添加結點的方法 public void add(Node node) { if (root == null) { root = node;// 若是root爲空則直接讓root指向node } else { root.add(node); } } // 中序遍歷 public void infixOrder() { if (root != null) { root.infixOrder(); } else { System.out.println("二叉排序樹爲空,不能遍歷"); } } } // 建立Node結點 class Node { int value; Node left; Node right; public Node(int value) { this.value = value; } // 返回左子樹的高度 public int leftHeight() { if (left == null) { return 0; } return left.height(); } // 返回右子樹的高度 public int rightHeight() { if (right == null) { return 0; } return right.height(); } // 返回 以該結點爲根結點的樹的高度 public int height() { return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1; } //左旋轉方法 private void leftRotate() { //建立新的結點,以當前根結點的值 Node newNode = new Node(value); //把新的結點的左子樹設置成當前結點的左子樹 newNode.left = left; //把新的結點的右子樹設置成帶你過去結點的右子樹的左子樹 newNode.right = right.left; //把當前結點的值替換成右子結點的值 value = right.value; //把當前結點的右子樹設置成當前結點右子樹的右子樹 right = right.right; //把當前結點的左子樹(左子結點)設置成新的結點 left = newNode; } //右旋轉 private void rightRotate() { Node newNode = new Node(value); newNode.right = right; newNode.left = left.right; value = left.value; left = left.left; right = newNode; } // 查找要刪除的結點 /** * * @param value * 但願刪除的結點的值 * @return 若是找到返回該結點,不然返回null */ public Node search(int value) { if (value == this.value) { // 找到就是該結點 return this; } else if (value < this.value) {// 若是查找的值小於當前結點,向左子樹遞歸查找 // 若是左子結點爲空 if (this.left == null) { return null; } return this.left.search(value); } else { // 若是查找的值不小於當前結點,向右子樹遞歸查找 if (this.right == null) { return null; } return this.right.search(value); } } // 查找要刪除結點的父結點 /** * * @param value 要找到的結點的值 * * @return 返回的是要刪除的結點的父結點,若是沒有就返回null */ public Node searchParent(int value) { // 若是當前結點就是要刪除的結點的父結點,就返回 if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) { return this; } else { // 若是查找的值小於當前結點的值, 而且當前結點的左子結點不爲空 if (value < this.value && this.left != null) { return this.left.searchParent(value); // 向左子樹遞歸查找 } else if (value >= this.value && this.right != null) { return this.right.searchParent(value); // 向右子樹遞歸查找 } else { return null; // 沒有找到父結點 } } } @Override public String toString() { return "Node [value=" + value + "]"; } // 添加結點的方法 // 遞歸的形式添加結點,注意須要知足二叉排序樹的要求 public void add(Node node) { if (node == null) { return; } // 判斷傳入的結點的值,和當前子樹的根結點的值關係 if (node.value < this.value) { // 若是當前結點左子結點爲null if (this.left == null) { this.left = node; } else { // 遞歸的向左子樹添加 this.left.add(node); } } else { // 添加的結點的值大於 當前結點的值 if (this.right == null) { this.right = node; } else { // 遞歸的向右子樹添加 this.right.add(node); } } //當添加完一個結點後,若是: (右子樹的高度-左子樹的高度) > 1 , 左旋轉 if(rightHeight() - leftHeight() > 1) { //若是它的右子樹的左子樹的高度大於它的右子樹的右子樹的高度 if(right != null && right.leftHeight() > right.rightHeight()) { //先對右子結點進行右旋轉 right.rightRotate(); //而後在對當前結點進行左旋轉 leftRotate(); //左旋轉.. } else { //直接進行左旋轉便可 leftRotate(); } return ; //必需要!!! } //當添加完一個結點後,若是 (左子樹的高度 - 右子樹的高度) > 1, 右旋轉 if(leftHeight() - rightHeight() > 1) { //若是它的左子樹的右子樹高度大於它的左子樹的高度 if(left != null && left.rightHeight() > left.leftHeight()) { //先對當前結點的左結點(左子樹)->左旋轉 left.leftRotate(); //再對當前結點進行右旋轉 rightRotate(); } else { //直接進行右旋轉便可 rightRotate(); } } } // 中序遍歷 public void infixOrder() { if (this.left != null) { this.left.infixOrder(); } System.out.println(this); if (this.right != null) { this.right.infixOrder(); } } } public class AVLTreeDemo { public static void main(String[] args) { int[] arr = { 14, 21, 7, 3, 8, 9 };//任意測試節點數組 //建立一個 AVLTree對象 AVLTree avlTree = new AVLTree(); //添加結點 for(int i=0; i < arr.length; i++) { avlTree.add(new Node(arr[i])); } //遍歷 System.out.println("中序遍歷"); avlTree.infixOrder(); System.out.println("平衡處理..."); System.out.println("樹的高度=" + avlTree.getRoot().height()); System.out.println("樹的左子樹高度=" + avlTree.getRoot().leftHeight()); System.out.println("樹的右子樹高度=" + avlTree.getRoot().rightHeight()); System.out.println("當前的根結點=" + avlTree.getRoot()); } }
一、平衡二叉樹又稱AVL樹。
二、平衡二叉樹查詢、插入、刪除的時間複雜度都是 O(logN)
。
三、插入節點失去平衡的狀況有4種,左左,左右,右左,右右。
四、右右、左左只須要旋轉一次就能夠平衡,而左右、右左要旋轉兩次才能把樹調整平衡!
五、失去平衡最多也只要旋轉2次,因此,調整平衡的過程的時間複雜度爲O(1)
。
雖然平衡二叉樹有效的解決了極端相似蛇皮單鏈表的狀況,可是平衡二叉樹也不是完美的,AVL樹最大的缺點就是刪除節點時有可能由於失衡,致使須要從刪除節點的父節點開始,不斷的回溯到根節點,若是這棵AVL樹很高的話,那中間就要判斷不少個節點,效率就顯然變的低下!所以咱們後面將要學習2-3樹以及紅-黑樹,抽空寫嘍....
若是本文對你有一點點幫助,那麼請點個讚唄,你的贊同是我最大的動力,謝謝~
最後,如有不足或者不正之處,歡迎指正批評,感激涕零!若是有疑問歡迎留言,絕對第一時間回覆!
歡迎各位關注個人公衆號,裏面有一些java學習資料和一大波java電子書籍,好比說周志明老師的深刻java虛擬機、java編程思想、核心技術卷、大話設計模式、java併發編程實戰.....都是java的聖經,不說了快上Tomcat車,咋們走!最主要的是一塊兒探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...