簡介
平衡二叉搜尋樹是一種特殊的二叉搜尋樹。為什麼會有平衡二叉搜尋樹呢?
考慮一下二叉搜尋樹的特殊情況,如果一個二叉搜尋樹所有的節點都是右節點,那麼這個二叉搜尋樹將會退化成為連結串列。從而導致搜尋的時間複雜度變為O(n),其中n是二叉搜尋樹的節點個數。
而平衡二叉搜尋樹正是為了解決這個問題而產生的,它通過限制樹的高度,從而將時間複雜度降低為O(logn)。
AVL的特性
在討論AVL的特性之前,我們先介紹一個概念叫做平衡因子,平衡因子表示的是左子樹和右子樹的高度差。
如果平衡因子=0,表示這是一個完全平衡二叉樹。
如果平衡因子=1,那麼這棵樹就是平衡二叉樹AVL。
也就是是說AVL的平衡因子不能夠大於1。
先看一個AVL的例子:
總結一下,AVL首先是一個二叉搜尋樹,然後又是一個二叉平衡樹。
AVL的構建
有了AVL的特性之後,我們看下AVL是怎麼構建的。
public class AVLTree {
//根節點
Node root;
class Node {
int data; //節點的資料
int height; //節點的高度
Node left;
Node right;
public Node(int data) {
this.data = data;
left = right = null;
}
}
同樣的,AVL也是由各個節點構成的,每個節點擁有data,left和right幾個屬性。
因為是二叉平衡樹,節點是否平衡還跟節點的高度有關,所以我們還需要定義一個height作為節點的高度。
在來兩個輔助的方法,一個是獲取給定的節點高度:
//獲取給定節點的高度
int height(Node node) {
if (node == null)
return 0;
return node.height;
}
和獲取平衡因子:
//獲取平衡因子
int getBalance(Node node) {
if (node == null)
return 0;
return height(node.left) - height(node.right);
}
AVL的搜尋
AVL的搜尋和二叉搜尋樹的搜尋方式是一致的。
先看一個直觀的例子,怎麼在AVL中搜尋到7這個節點:
搜尋的基本步驟是:
- 從根節點15出發,比較根節點和搜尋值的大小
- 如果搜尋值小於節點值,那麼遞迴搜尋左側樹
- 如果搜尋值大於節點值,那麼遞迴搜尋右側樹
- 如果節點匹配,則直接返回即可。
相應的java程式碼如下:
//搜尋方法,預設從根節點搜尋
public Node search(int data){
return search(root,data);
}
//遞迴搜尋節點
private Node search(Node node, int data)
{
// 如果節點匹配,則返回節點
if (node==null || node.data==data)
return node;
// 節點資料大於要搜尋的資料,則繼續搜尋左邊節點
if (node.data > data)
return search(node.left, data);
// 如果節點資料小於要搜素的資料,則繼續搜尋右邊節點
return search(node.right, data);
}
AVL的插入
AVL的插入和BST的插入是一樣的,不過插入之後有可能會導致樹不再平衡,所以我們需要做一個再平衡的步驟。
看一個直觀的動畫:
插入的邏輯是這樣的:
- 從根節點出發,比較節點資料和要插入的資料
- 如果要插入的資料小於節點資料,則遞迴左子樹插入
- 如果要插入的資料大於節點資料,則遞迴右子樹插入
- 如果根節點為空,則插入當前資料作為根節點
插入資料之後,我們需要做再平衡。
再平衡的邏輯是這樣的:
- 從插入的節點向上找出第一個未平衡的節點,這個節點我們記為z
- 對z為根節點的子樹進行旋轉,得到一個平衡樹。
根據以z為根節點的樹的不同,我們有四種旋轉方式:
- left-left:
如果是left left的樹,那麼進行一次右旋就夠了。
右旋的步驟是怎麼樣的呢?
- 找到z節點的左節點y
- 將y作為旋轉後的根節點
- z作為y的右節點
- y的右節點作為z的左節點
- 更新z的高度
相應的程式碼如下:
Node rightRotate(Node node) {
Node x = node.left;
Node y = x.right;
// 右旋
x.right = node;
node.left = y;
// 更新node和x的高度
node.height = max(height(node.left), height(node.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;
// 返回新的x節點
return x;
}
- right-right:
如果是right-right形式的樹,需要經過一次左旋:
左旋的步驟正好和右旋的步驟相反:
- 找到z節點的右節點y
- 將y作為旋轉後的根節點
- z作為y的左節點
- y的左節點作為z的右節點
- 更新z的高度
相應的程式碼如下:
//左旋
Node leftRotate(Node node) {
Node x = node.right;
Node y = x.left;
//左旋操作
x.left = node;
node.right = y;
// 更新node和x的高度
node.height = max(height(node.left), height(node.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;
// 返回新的x節點
return x;
}
- left-right:
如果是left right的情況,需要先進行一次左旋將樹轉變成left left格式,然後再進行一次右旋,得到最終結果。
- right-left:
如果是right left格式,需要先進行一次右旋,轉換成為right right格式,然後再進行一次左旋即可。
現在問題來了,怎麼判斷一個樹到底是哪種格式呢?我們可以通過獲取平衡因子和新插入的資料比較來判斷:
如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較新插入的data和node.left.data的大小
如果data < node.left.data,表示是left left的情況,只需要一次右旋即可
如果data > node.left.data,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋
如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較新插入的data和node.right.data的大小
如果data > node.right.data,表示是Right Right的情況,只需要一次左旋即可如果data < node.left.data,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋
插入節點的最終程式碼如下:
//插入新節點,從root開始
public void insert(int data){
root=insert(root, data);
}
//遍歷插入新節點
Node insert(Node node, int data) {
//先按照普通的BST方法插入節點
if (node == null)
return (new Node(data));
if (data < node.data)
node.left = insert(node.left, data);
else if (data > node.data)
node.right = insert(node.right, data);
else
return node;
//更新節點的高度
node.height = max(height(node.left), height(node.right)) + 1;
//判斷節點是否平衡
int balance = getBalance(node);
//節點不平衡有四種情況
//1.如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較新插入的data和node.left.data的大小
//如果data < node.left.data,表示是left left的情況,只需要一次右旋即可
//如果data > node.left.data,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋
//2.如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較新插入的data和node.right.data的大小
//如果data > node.right.data,表示是Right Right的情況,只需要一次左旋即可
//如果data < node.left.data,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋
//left left
if (balance > 1 && data < node.left.data)
return rightRotate(node);
// Right Right
if (balance < -1 && data > node.right.data)
return leftRotate(node);
// Left Right
if (balance > 1 && data > node.left.data) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// Right Left
if (balance < -1 && data < node.right.data) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
//返回插入後的節點
return node;
}
AVL的刪除
AVL的刪除和插入類似。
首先按照普通的BST刪除,然後也需要做再平衡。
看一個直觀的動畫:
刪除之後,節點再平衡也有4種情況:
如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較左節點的平衡因子
如果左節點的平衡因子>=0,表示是left left的情況,只需要一次右旋即可
如果左節點的平衡因<0,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋
如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較右節點的平衡因子
如果右節點的平衡因子<=0,表示是Right Right的情況,只需要一次左旋即可
如果右節點的平衡因子>0,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋
相應的程式碼如下:
Node delete(Node node, int data)
{
//Step 1. 普通BST節點刪除
// 如果節點為空,直接返回
if (node == null)
return node;
// 如果值小於當前節點,那麼繼續左節點刪除
if (data < node.data)
node.left = delete(node.left, data);
//如果值大於當前節點,那麼繼續右節點刪除
else if (data > node.data)
node.right = delete(node.right, data);
//如果值相同,那麼就是要刪除的節點
else
{
// 如果是單邊節點的情況
if ((node.left == null) || (node.right == null))
{
Node temp = null;
if (temp == node.left)
temp = node.right;
else
temp = node.left;
//沒有子節點的情況
if (temp == null)
{
node = null;
}
else // 單邊節點的情況
node = temp;
}
else
{ //非單邊節點的情況
//拿到右側節點的最小值
Node temp = minValueNode(node.right);
//將最小值作為當前的節點值
node.data = temp.data;
// 將該值從右側節點刪除
node.right = delete(node.right, temp.data);
}
}
// 如果節點為空,直接返回
if (node == null)
return node;
// step 2: 更新當前節點的高度
node.height = max(height(node.left), height(node.right)) + 1;
// step 3: 獲取當前節點的平衡因子
int balance = getBalance(node);
// 如果節點不再平衡,那麼有4種情況
//1.如果balance>1,那麼我們在Left Left或者left Right的情況,這時候我們需要比較左節點的平衡因子
//如果左節點的平衡因子>=0,表示是left left的情況,只需要一次右旋即可
//如果左節點的平衡因<0,表示是left right的情況,則需要將node.left進行一次左旋,然後將node進行一次右旋
//2.如果balance<-1,那麼我們在Right Right或者Right Left的情況,這時候我們需要比較右節點的平衡因子
//如果右節點的平衡因子<=0,表示是Right Right的情況,只需要一次左旋即可
//如果右節點的平衡因子>0,表示是Right left的情況,則需要將node.right進行一次右旋,然後將node進行一次左旋
// Left Left Case
if (balance > 1 && getBalance(node.left) >= 0)
return rightRotate(node);
// Left Right Case
if (balance > 1 && getBalance(node.left) < 0)
{
node.left = leftRotate(node.left);
return rightRotate(node);
}
// Right Right Case
if (balance < -1 && getBalance(node.right) <= 0)
return leftRotate(node);
// Right Left Case
if (balance < -1 && getBalance(node.right) > 0)
{
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
本文的程式碼地址:
本文收錄於 http://www.flydean.com/11-algorithm-avl-tree/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!