二叉排序樹集中了數組的查找優勢以及鏈表的插入、刪除優勢,因此在數據結構中占有一定的地位。但在一定的情況下二叉排序樹又有可能變為鏈表,例如插入從1~100的數,這時進行數據查找的效率就要降低。
為了解決二叉排序樹這種左右子樹深度不均勻的情況引入了一種平衡二叉樹(AVLTree):任何一個節點的左右子樹深度差不超過1.通過這個限定,阻止了二叉樹的左右子樹深度差較大的情況,維持了二叉樹的穩定。
如何讓二叉樹的左右子樹深度差不超過1呢?這就需要對節點進行旋轉,也就是當某個節點的左右子樹深度超過1時需要對這個節點進行旋轉(旋轉之后依舊是左子樹小於節點小於右子樹),重新調整樹的結構。
例如:這兩棵二叉樹雖然結構不同,但是都是二叉排序樹,所謂的旋轉就是把左邊的深度為3的樹旋轉為右邊深度為2的二叉樹。
在平衡二叉樹進行插入操作時遇到的不平衡情況有多種,但是這么多種情況都可以分解為一下四中基礎情景:把它叫做:左左、左右、右右、右左。
在解釋這四種情景之前需要先明白一個定義:最小不平衡節點—插入一個節點之后,距離這個插入節點最近的不平衡節點就是最小不平衡節點(如上圖左樹的10節點)。所有的旋轉都是在最小不平衡節點的基礎上進行的。
繼續解釋四種情景命名意義:左左:節點插入在最小不平衡節點的左子樹的左子樹上。 左右:節點插入在最小不平衡節點的左子樹的右子樹上面
右:節點插入在最小不平衡樹的右子樹的右子樹上面。 右左:節點插入在最小不平衡樹的右子樹的左子樹上面。
下面就具體分析這四種情況:
左左:右旋
左左簡單不用詳解。
左右:先左旋再右旋
這里有人又有疑問了,上面的左左(圖2)看明白了,可這里左右情景為什么要旋轉兩次呢?為什么先左旋,再右旋呢?
先別急,看看這種情況:(圖4)
毫無疑問這也是 左右 情景(左左情景有很多種,圖3演示的是最基礎的情景,所有 的左左情景的旋轉情況和圖3都是一樣的),那么該怎么旋轉呢?
直接右旋不對吧?因為6節點的右子樹(以根節點10為中心,靠近內部的子樹)6-8經過旋轉之后要充當10節點的左子樹,這樣會導致依舊不平衡。所以在這種左右情景下需要進行兩次旋轉,先把6的右子樹降低高度,然后在進行右旋。即:
把圖7 情景和圖3的情景一樣,這就是為什么 左右情景 需要先左旋再右旋的原因。
在這里可以記作:最小不平衡節點的左節點的內部(以根節點做對稱軸,偏向對稱軸的為內部。也就是以7為節點的子樹)的子樹高度高於外部子樹的高度時需要進行兩次旋轉。
右右:左旋
右右情景直接左旋即可。不在詳解
右左:先右旋,再左旋
為什么這樣旋轉明白了吧?如同左右情景,考慮到圖10的 右左情景
這種情景旋轉如圖11
旋轉的四種情景就這些了。需要說明的是,下面這兩對情景旋轉是一樣的。
圖12都是右左情景,具體看代碼的旋轉方法就明白了在第一次右旋的時候進行的操作。private Node<T> rotateSingleRight(Node<T> node);
圖13都是左右情景,第一次左旋見:private Node<T> rotateSingleLeft(Node<T> node);
旋轉情景弄明白之后就是怎么代碼實現了,在實現代碼之前需要考慮如何進行樹高判斷。這里就根據定義來,|左子樹樹高-右子樹樹高|<2。如果大於等於2則該節點就不在 平衡,需要進行旋轉操作。因此在程序中節點中需要定義一個height屬性來存儲該節點的樹高。
由於平衡二叉樹的性質,二叉樹的高度不會很高,程序使用遞歸進行數據插入查找不會造成棧溢出異常,所以程序采用遞歸操作進行插入查找。
平衡的判定策略是在進行遞歸回溯的時候依照回溯路徑更新節點的樹高,然后根據|左子樹樹高-右子樹樹高|<2來判定該節點是否失衡,進一步對是夠旋轉進行判定。
程序中的平衡判定策略比較漂亮,當時就是一直卡在這里無法繼續進行,然后參考了 AVL樹-自平衡二叉查找樹(Java實現) 之后采用這種方法才得以解決。
平衡二叉樹的刪除操作。
對於平衡二叉樹的刪除操作,只要明白一點就可以了:
如果該節點沒有左右子樹(該節點為葉子節點)或者只有其中一個子樹則可以直接進行刪除
否則需要繼續進行判定該節點:如果該節點的外部(內外:以根節點做對稱軸,靠近對稱軸的子樹為內部子樹)子樹樹高低於內部子樹樹高,則找到該節點內部子樹的最值(最值:如果內部子樹是該節點的右子樹則數值為右子樹的最小值;如果內部節點是該節點的左子樹則數值為該節點左子樹的最大值)進行數值交換,交換之后刪除該節點即可。
刪除之后進行回溯的時候要更新節點的樹高,然后判斷節點是否平衡,不平衡進行旋轉。這時對旋轉次數的判定就不同於插入時的判定。
如圖14 刪除11節點
這種情景需不需要進行兩次旋轉?該如何判定?
毫無疑問肯定是要進行一次右旋的,但是在右旋之前是不是要進行一次左旋呢?
這就要根據最小不平衡節點的左節點6進行判定,如果6的左節點樹高低於6的右節點樹高則需要進行一次左旋,最后進行一次右旋結束。
如果6的左子樹樹高高於6的右子樹樹高則不需進行左旋可以直接對10節點進行右旋結束操作。
如圖15,這種情況肯定需要進行左旋,至於在左旋之前要不要對13節點進行右旋,相信知道該如何判斷了。
根據13節點的左右子樹高度來判斷,左子樹(內部)高於右子樹(外部)高度則需要進行左旋,圖15這種情景是不需要的。
知道了各種旋轉的判定標准,程序中就沒有其他什么難點了,下面看一下代碼:
package com.zpj.datastructure.avlTree; /** * @author PerKins Zhu * @date:2016年8月30日 下午8:01:03 * @version :1.1 * */ // 存儲數據類型必須實現Comparable接口,實現比較方法 public class AVLTree<T extends Comparable<T>> { private Node<T> root; // 定義節點存儲數據 private static class Node<T> { Node<T> left;// 左孩子 Node<T> right;// 右孩子 T data; // 存儲數據 int height; // 樹高 public Node(Node<T> left, Node<T> right, T data) { this.left = left; this.right = right; this.data = data; this.height = 0; } } // 對外公開的方法進行插入 public Node<T> insert(T data) { return root = insert(data, root); } // 私有方法進行遞歸插入,返回插入節點 private Node<T> insert(T data, Node<T> node) { // 遞歸終止條件 if (node == null) return new Node<T>(null, null, data); // 比較插入數據和待插入節點的大小 int compareResult = data.compareTo(node.data); if (compareResult > 0) {// 插入node的右子樹 node.right = insert(data, node.right); // 回調時判斷是否平衡 if (getHeight(node.right) - getHeight(node.left) == 2) {// 不平衡進行旋轉 // 判斷是需要進行兩次旋轉還是需要進行一次旋轉 int compareResult02 = data.compareTo(node.right.data); if (compareResult02 > 0)// 進行一次左旋(右右) node = rotateSingleLeft(node); else // 進行兩次旋轉,先右旋,再左旋 node = rotateDoubleLeft(node); } } else if (compareResult < 0) {// 插入node的左子樹 node.left = insert(data, node.left); // 回調時進行判斷是否平衡 if (getHeight(node.left) - getHeight(node.right) == 2) {// 進行旋轉 // 判斷是需要進行兩次旋轉還是需要進行一次旋轉 int intcompareResult02 = data.compareTo(node.left.data); if (intcompareResult02 < 0)// 進行一次左旋(左左) node = rotateSingleRight(node); else // 進行兩次旋轉,先左旋,再右旋 node = rotateDoubleRight(node); } } // 重新計算該節點的樹高 node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; return node; } // 右右情況--進行左旋 private Node<T> rotateSingleLeft(Node<T> node) { Node<T> rightNode = node.right; node.right = rightNode.left; rightNode.left = node; // 旋轉結束計算樹高 node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; rightNode.height = Math.max(node.height, getHeight(rightNode.right)) + 1; return rightNode; } // 左左情況--進行右旋 private Node<T> rotateSingleRight(Node<T> node) { Node<T> leftNode = node.left; node.left = leftNode.right; leftNode.right = node; // 旋轉結束計算樹高 node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; leftNode.height = Math.max(getHeight(leftNode.left), node.height) + 1; return leftNode; } // 右左情況--先右旋再左旋 private Node<T> rotateDoubleLeft(Node<T> node) { // 先進行右旋 node.right = rotateSingleRight(node.right); // 再加上左旋 node = rotateSingleLeft(node); return node; } // 左右--先左旋再右旋 private Node<T> rotateDoubleRight(Node<T> node) { // 先進行左旋 node.left = rotateSingleLeft(node.left); // 在進行右旋 node = rotateSingleRight(node); return node; } // 計算樹高 private int getHeight(Node<T> node) { return node == null ? -1 : node.height; } // public 方法供外部進行刪除調用 public Node<T> remove(T data) { return root = remove(data, root); } // 遞歸進行刪除,返回比較節點 private Node<T> remove(T data, Node<T> node) { if (node == null) {// 不存在此節店,返回null.不需要調整樹高 return null; } int compareResult = data.compareTo(node.data); if (compareResult == 0) {// 存在此節點進入 /** * 找到節點之后進行節點刪除操作 判斷node是否有子樹,如果沒有子樹或者只有一個子樹則直接進行刪除 * 如果有兩個子樹,則需要判斷node的平衡系數balance * 如果balance為0或者1則把node和node的左子樹的最大值進行交換 否則把node和右子樹的最小值進行交換 * 交換數據之后刪除該節點 刪除之后判斷delete節點的父節點是否平衡,如果不平衡進行節點旋轉 * 旋轉之后返回delete節點的父節點進行回溯 * */ if (node.left != null && node.right != null) { // 此節點存在左右子樹 // 判斷node節點的balance,然后進行數據交換刪除節點 int balance = getHeight(node.left) - getHeight(node.right); Node<T> temp = node;// 保存需要進行刪除的node節點 if (balance == -1) { // 與右子樹的最小值進行交換 exChangeRightData(node, node.right); } else { // 與左子樹的最大值進行交換 exChangeLeftData(node, node.left); } // 此時已經交換完成並且把節點刪除完成,則需要重新計算該節點的樹高 temp.height = Math.max(getHeight(temp.left), getHeight(temp.right)) + 1; // 注意此處,返回的是temp,也就是保存的需要刪除的節點,而不是替換的節點 return temp; } else { // 把node的子節點返回調用處等於刪除了node節點 // 此處隱含了一個node.left ==null && node.right == null 的條件,這時返回null return node.left != null ? node.left : node.right; } } else if (compareResult > 0) {// 沒找到需要刪除的節點繼續遞歸進行尋找 node.right = remove(data, node.right); // 刪除之后進行樹高更新 node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; // 如果不平衡則進行右旋調整。 if (getHeight(node.left) - getHeight(node.right) == 2) {// 進行旋轉 Node<T> leftSon = node.left; // 判斷是否需要進行兩次右旋還是一次右旋 // 判斷條件就是比較leftSon節點的左右子節點樹高 if (leftSon.left.height > leftSon.right.height) { // 右旋一次 node = rotateSingleRight(node); } else { // 兩次旋轉,先左旋,后右旋 node = rotateDoubleRight(node); } } return node; } else if (compareResult < 0) {// 沒找到需要刪除的節點繼續遞歸進行尋找 node.left = remove(data, node.left); // 刪除之后進行樹高更新 node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; // 如果不平衡進行左旋操作 if (getHeight(node.left) - getHeight(node.right) == 2) {// 進行旋轉 Node<T> rightSon = node.right; // 判斷是否需要進行兩次右旋還是一次右旋 // 判斷條件就是比較rightSon節點的左右子節點樹高 if (rightSon.right.height > rightSon.left.height) { node = rotateSingleLeft(node); } else { // 先右旋再左旋 node = rotateDoubleLeft(node); } } return node; } return null; } // 遞歸尋找right節點的最大值 private Node<T> exChangeLeftData(Node<T> node, Node<T> right) { if (right.right != null) { right.right = exChangeLeftData(node, right.right); } else { // 數據進行替換 node.data = right.data; // 此處已經把替換節點刪除 return right.left; } right.height = Math.max(getHeight(right.left), getHeight(right.right)) + 1; // 回溯判斷left是否平衡,如果不平衡則進行左旋操作。 int isbanlance = getHeight(right.left) - getHeight(right.right); if (isbanlance == 2) {// 進行旋轉 Node<T> leftSon = node.left; // 判斷是否需要進行兩次右旋還是一次右旋 // 判斷條件就是比較leftSon節點的左右子節點樹高 if (leftSon.left.height > leftSon.right.height) { // 右旋一次 return node = rotateSingleRight(node); } else { // 兩次旋轉,先左旋,后右旋 return node = rotateDoubleRight(node); } } return right; } // 遞歸尋找left節點的最小值 private Node<T> exChangeRightData(Node<T> node, Node<T> left) { if (left.left != null) { left.left = exChangeRightData(node, left.left); } else { node.data = left.data; // 此處已經把替換節點刪除 return left.right; } left.height = Math.max(getHeight(left.left), getHeight(left.right)) + 1; // 回溯判斷left是否平衡,如果不平衡則進行左旋操作。 int isbanlance = getHeight(left.left) - getHeight(left.right); if (isbanlance == -2) {// 進行旋轉 Node<T> rightSon = node.right; // 判斷是否需要進行兩次右旋還是一次右旋 // 判斷條件就是比較rightSon節點的左右子節點樹高 if (rightSon.right.height > rightSon.left.height) { return node = rotateSingleLeft(node); } else { // 先右旋再左旋 return node = rotateDoubleLeft(node); } } return left; } // ************************中序輸出 輸出結果有小到大************************************* public void inorderTraverse() { inorderTraverseData(root); } // 遞歸中序遍歷 private void inorderTraverseData(Node<T> node) { if (node.left != null) { inorderTraverseData(node.left); } System.out.print(node.data + "、"); if (node.right != null) { inorderTraverseData(node.right); } } }
這段測試程序可以進行測試:
package com.zpj.datastructure.avlTree; import org.junit.Test; /** * @author PerKins Zhu * @date:2016年8月30日 下午8:42:15 * @version :1.1 * */ public class AVLTreeTest { @Test public void test01() { AVLTree tree = new AVLTree(); int array[] = { 28, 35, 5, 35, 26, 30, 1, 21, 18, 35, 7, 30, 25, 1, 7, }; for (int i = 0; i < array.length; i++) { System.out.print(array[i] + ","); tree.insert(array[i]); } System.out.println(); tree.inorderTraverse(); tree.remove(12); System.out.println(); tree.inorderTraverse(); } @Test public void test02() { AVLTree tree = new AVLTree(); int temp = 0; for (int i = 0; i < 15; i++) { int num = (int) (Math.random() * 40); System.out.print(num + ","); tree.insert(num); temp = num; } System.out.println(); tree.inorderTraverse(); tree.remove(temp);// 刪除插入的最后一個數據 System.out.println(); tree.inorderTraverse(); } }
在測試過程中對幾種特殊情況都進行了測試,到目前為止沒發現有問題,如果有朋友在測試的時候發現問題,歡迎指出討論。
------------------------------------------------------