爲什麼要平衡
一般的搜索樹, 如果元素是順序加入的話, 那麼這棵樹就會退化成鏈表
什麼是平衡
對於任意一個節點, 左右子樹的高度差不超過1的樹就是平衡二叉樹, 比如下面這棵樹
height表示高度, 父節點的高度是左右子節點中最大的那個高度加一
平衡二叉樹的高度和節點數之間的關係是 O(log n)的
因爲每次加入新的節點, 都要看節點的高度, 所以節點的結構如下
class Node{
key; // 用來統計詞頻的話, key就是單詞
value; // value是單詞的出現頻率
Node left, right; // 左右節點
height; // 樹的高度
}
平衡因子
父節點的平衡因子(balance factor)是左子樹與右子樹的高度(height)之差(可正可負)
葉子節點的左右子節點爲空, 高度爲0, 平衡因子爲0
當樹中有一個節點平衡因子大於1或小於-1時, 這棵樹就不平衡, 如下圖
不平衡的情況和平衡的方法
只有一個節點, 或者兩個節點的樹是平衡的
當向一棵平衡樹插入一個新的節點時, 可能會不平衡
如果不平衡, 那麼那個新插入的節點的父親節點, 祖先節點的平衡因子都會大於1或小於-1
所以新插入節點都要向上回溯來維護其平衡性
不平衡的情況有4種:
LL
如下圖中, 是框中的節點帶來了不平衡
我們可以把框中部分用下圖來代替, 藍色部分是他們的子樹
這種情況可以稱爲 Left Left, 意思是新添了一個節點 z 到根節點 y 左子樹的左子樹, 導致根節點的平衡因子絕對值大於1
這種情況用代碼表示爲getBalanceFactor(y) > 1 && getBalanceFactor(x) > 0
, 再次說明 平衡因子是拿左子樹的height減右子樹的height, getBalanceFactor(y) = 2, getBalanceFactor(x) = 1
只需要把x的右子樹換成y, y的左子樹換位x原來的右子樹 T3 即可達到平衡, 同時還保持了二叉樹搜索樹的性質
相當於z不動, 將y節點以x爲軸順時針旋轉, 也稱爲右旋轉, 效果如下
Node rightRotate(Node y){
Node x = y.left;
Node T3 = x.left;
// 向右旋轉
x.right = y;
y.left = T3;
// 更新height, 先更新y, 再更新x, 因爲y在下面
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
RR
還有一種情況我們稱爲 Right Right, 意思是新添了一個節點 z 到根節點 y 右子樹的右子樹
這種情況用代碼表示爲getBalanceFactor(y) < -1 && getBalanceFactor(x) < 0
只需要把x的左子樹換成y, y的右子樹換位x原來的左子樹 T3 即可達到平衡, 同時還保持了二叉樹搜索樹的性質
相當於z不動, 將y節點以x爲軸逆時針旋轉, 也稱爲左旋轉, 效果如下
Node leftRotate(Node y){
Node x = y.right;
Node T3 = x.left;
// 向左旋轉
x.left = y;
y.right = T3;
// 更新height, 先更新y, 再更新x, 因爲y在下面
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
LR
另一種情況稱爲 Left Right, 意思是新添了一個節點 z 到根節點 y 左子樹的右子樹
這種情況用代碼表示爲getBalanceFactor(y) > 1 && getBalanceFactor(x) < 0
這種情況需要兩次旋轉 (要注意得保持二叉搜索樹的性質),
首先把節點x以節點z爲軸逆時針旋轉, 節點y不動 (左旋轉)
再把節點y以z爲軸順時針旋轉, 得到效果如下 (右旋轉)
RL
另一種情況稱爲 Right Left, 意思是新添了一個節點 z 到根節點 y 右子樹的左子樹
這種情況用代碼表示爲getBalanceFactor(y) < -1 && getBalanceFactor(x) > 0
這種情況也需要兩次旋轉
先節點x繞節點z順時針旋轉 (右旋轉)
節點y再繞z逆時針旋轉 (左旋轉)
刪除操作
刪除一個元素的話, 就得拿一個新的元素來代替自己放在原來的位置, 我們可以拿比它大的最小元素, 或者比它小的最大元素來代替它, 這樣可以同時保持二叉搜索樹的性質
待刪除的節點分爲四種情況
- 待刪除節點左子樹爲空
可以拿它的右子樹代替掉自己 (用它大的最小元素代替) - 待刪除節點右子樹爲空
可以拿它的左子樹代替掉自己 (用比它小的最大元素代替) - 待刪除節點左右子樹爲空
葉子節點也可以看成是有null作爲孩子, 這樣就可以用上面的方法處理掉 - 待刪除節點左右子樹不爲空
如果用 比它大的最小元素, 那就是拿它右子樹的最小元素來代替它
如果用 比它小的最大元素, 那就是拿它左子樹的最大元素來代替它
刪除之後記得要重新維護平衡, 方法同上
實現
Map.java
public interface Map<K, V> {
void add(K key, V value);
V remove(K key);
boolean contains(K key);
V get(K key);
void set(K key, V newValue);
int getSize();
boolean isEmpty();
}
AVL.tree
import test.FileOperation; // 測試用, 可刪
import java.util.ArrayList; // 測試用, 可刪
public class AVLTree<K extends Comparable<K>, V> implements Map<K, V>{
private class Node{
public K key;
public V value;
public Node left, right;
public int height;
public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
height = 1;
}
}
private Node root;
private int size;
public AVLTree(){
root = null;
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 判斷該二叉樹是否是一棵二分搜索樹
* @return
*/
public boolean isBST(){
ArrayList<K> keys = new ArrayList<>();
inOrder(root, keys); // 中序遍歷一遍, 然後看是不是按順序的
for(int i=1; i<keys.size(); i++)
if(keys.get(i-1).compareTo(keys.get(i)) > 0)
return false;
return true;
}
/**
* 判斷一棵樹是否是平衡二叉樹
* @return
*/
public boolean isBalanced(){
return isBalanced(root);
}
/**
* 判斷以Node爲根的二叉樹是否是一棵平衡二叉樹,遞歸算法
* @param node
* @return
*/
private boolean isBalanced(Node node) {
if(node == null)
return true;
int balanceFactor = getBalanceFactor(node);
if(Math.abs(balanceFactor) > 1)
return false;
return isBalanced(node.left) && isBalanced(node.right); // 父節點的平衡因子小於1, 子節點不一定也都小於1
}
/**
* 中序遍歷, 保存到一個ArrayList中
* @param node
* @param keys
*/
private void inOrder(Node node, ArrayList<K> keys) {
if(node == null){
return;
}
inOrder(node.left, keys);
keys.add(node.key);
inOrder(node.right, keys);
}
private int getHeight(Node node){
if(node == null)
return 0;
return node.height;
}
/**
* 向二分搜索書中添加元素(key, value)
* @param key
* @param value
*/
@Override
public void add(K key, V value) {
root = add(root, key, value);
}
/**
* 獲得節點node的平衡因子
* @param node
* @return
*/
private int getBalanceFactor(Node node){
if(node == null)
return 0;
return getHeight(node.left) - getHeight(node.right);
}
// 對節點y進行向右旋轉操作,返回旋轉後新的根節點x
// y x
// / \ / \
// x T4 向右旋轉 (y) z y
// / \ - - - - - - - -> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
private Node rightRotate(Node y){
Node x = y.left;
Node T3 = x.right;
// 向右旋轉
x.right = y;
y.left = T3;
// 更新height, 先更新y, 再更新x, 因爲y在下面
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
// 對節點y進行向左旋轉操作,返回旋轉後新的根節點x
// y x
// / \ / \
// T4 x 向左旋轉 (y) y z
// / \ - - - - - - - -> / \ / \
// T3 z T4 T3 T1 T2
// / \
// T1 T2
private Node leftRotate(Node y){
Node x = y.right;
Node T3 = x.left;
// 向左旋轉
x.left = y;
y.right = T3;
// 更新height, 先更新y, 再更新x, 因爲y在下面
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
/**
* 向以node爲根的二分搜索樹中插入元素(key, value),遞歸算法
* 如果key已存在, 則更新value
* @param node
* @param key
* @param value
* @return 插入新節點後二分搜索樹的根
*/
private Node add(Node node, K key, V value) {
if(node == null){
size++;
return new Node(key, value);
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else
node.value = value;
// 1. 更新height
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
// 2. 計算平衡因子
int balanceFactor = getBalanceFactor(node);
// 3. 維護平衡
// LL
if(balanceFactor > 1 && getBalanceFactor(node.left) > 0) // 不用>=0, 因爲node.left左右一定是不相等的, 如果相等的話, 那就是有兩個節點導致了不平衡, 這是不可能的, 但是加上也可以啦
return rightRotate(node);
// RR
if(balanceFactor < -1 && getBalanceFactor(node.right) < 0)
return leftRotate(node);
// LR
if(balanceFactor > 1 && getBalanceFactor(node.left) < 0){
node.left = leftRotate(node.left); // 轉化成LL的情況
return rightRotate(node);
}
// RL
if(balanceFactor < -1 && getBalanceFactor(node.right) > 0){
node.right = rightRotate(node.right); // 轉化成RR的情況
return leftRotate(node);
}
// 平衡結束
return node;
}
/**
*
* @return 返回以node爲根的二分搜索樹的最小值所在的節點
*/
private Node minimum(Node node){
if(node.left == null)
return node;
return minimum(node.left);
}
/**
* 從二分搜索樹中刪除鍵爲key的節點
* @param key
* @return
*/
@Override
public V remove(K key) {
Node node = getNode(root, key);
if(node != null){
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key) {
if(node == null)
return null;
Node retNode;
if(key.compareTo(node.key) < 0){
node.left = remove(node.left, key);
retNode = node; // 刪除完節點後可能會打破平衡, 先不返回
}
else if(key.compareTo(node.key) > 0){
node.right = remove(node.right, key);
retNode = node;
}
else{ // key.compareTo(node.key) == 0
// 刪除節點左子樹爲空的情況
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size--;
retNode = rightNode;
}
// 刪除節點右子樹爲空的情況
else if(node.right == null){
Node leftNode = node.left;
node.left = null;
size--;
retNode = leftNode;
}
// 待刪除節點左右子樹均不爲空的情況
else{
// 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
// 用這個節點頂替待刪除節點的位置
Node successor = minimum(node.right);
successor.right = remove(node.right, successor.key);
successor.left = node.left;
node.left = null;
node.right = null;
// size--; removeMin已經改過size了
retNode = successor;
}
}
if(retNode == null){ // retNode爲空
return null;
}
// 1. 更新height
retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
// 2. 計算平衡因子
int balanceFactor = getBalanceFactor(retNode);
// 3. 維護平衡
// LL
// getBalanceFactor(retNode.left) >= 0 的等於號是必須要的, 如果刪除T4的話,
// y 就會出現z, T3兩個節點同時導致不平衡, 這在添加的時候不可能出現,
// / \ 但在刪除的時候可能出現
// x T4
// / \
// z T3
if(balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
return rightRotate(retNode);
// RR
if(balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
return leftRotate(retNode);
// LR
// getBalanceFactor(retNode.left) >= 0 的等於號何以省略, 因爲 =0 就是上面的情況, 在上面用比較簡單的方法就處理掉了
if(balanceFactor > 1 && getBalanceFactor(retNode.left) < 0){
retNode.left = leftRotate(retNode.left); // 轉化成LL的情況
return rightRotate(retNode);
}
// RL
if(balanceFactor < -1 && getBalanceFactor(retNode.right) > 0){
retNode.right = rightRotate(retNode.right); // 轉化成RR的情況
return leftRotate(retNode);
}
// 平衡結束
return retNode;
}
/**
* @return 以node爲根節點的二分搜索樹中,key所在的節點
*/
private Node getNode(Node node, K key){
if(node == null){
return null;
}
if(key.equals(node.key))
return node;
else if(key.compareTo(node.key) < 0)
return getNode(node.left, key);
else // key.compareTo(node.key) > 0
return getNode(node.right, key);
}
@Override
public boolean contains(K key) {
return getNode(root, key) != null;
}
@Override
public V get(K key) {
Node node = getNode(root, key);
return node == null ? null : node.value;
}
@Override
public void set(K key, V newValue) {
Node node = getNode(root, key);
if(node == null)
throw new IllegalArgumentException(key + " doesn't exist!");
node.value = newValue;
}
// 僅測試用
public static void main(String[] args) {
System.out.println("Pride and Prejudice");
ArrayList<String> words = new ArrayList<>();
if(FileOperation.readFile("on-the-road.txt", words)) {
System.out.println("Total words: " + words.size());
AVLTree<String, Integer> map = new AVLTree<>();
for (String word : words) {
if (map.contains(word))
map.set(word, map.get(word) + 1);
else
map.add(word, 1);
}
System.out.println("Total different words: " + map.getSize());
System.out.println("Frequency of PRIDE: " + map.get("pride"));
System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));
System.out.println("is BST: " + map.isBST());
System.out.println("is Balanced: " + map.isBalanced());
for(String word : words){
map.remove(word);
if(!map.isBST() || !map.isBalanced()){
throw new RuntimeException("ERROR");
}
}
}
System.out.println();
}
}
把ALVTree包裝一下就作爲AVLMap, 如果不使用value, 只用key的話也可以作爲AVLSet使用