github 地址:https://github.com/duchenlong/Cpp/tree/master/avl_tree/avl_tree
什麼是AVL樹
對於爲什麼會有AVL樹這個概念,名字來源於他的發明者G. M. Adelson-Velsky和E. M. Landis
兩位大佬。
AVL樹是最先發明的平衡二叉查找樹,在AVL樹中任何節點的兩個子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹
。
平衡二叉樹的搜索效率確實也挺高的,但是由於他的根節點是由第一次插入的元素的決定的,一旦根節點確定,那麼以後這個節點就是整棵樹的根了,是不會變化的,這也就出現了一些意外情況:
- 當我們按照升序進行插入的時候,整個樹就會變成一個只有右子樹的結構
- 另一種就是我們以降序進行插入的時候,又會出現只有左子樹的情況
對於這樣的情況,當我們進行查找的時候,其實總的效率還是O(n)
的,但是由於樹中的節點都是不一定連續的地址空間,然後用指針在邏輯上連接起來的,所以當時間複雜度爲O(n)
的時候,可能還不如直接在數組中遍歷找數據。
而AVL樹的出現,很好的解決了這樣的情況。既然樹中節點的地址都是不連續的空間,那麼就可以通過移動指針變量,來對樹的節點進行旋轉,從而讓整棵樹趨於平衡的狀態,讓查找的效率真正達到O(log n)
AVL樹的概念
AVL 樹相比於平衡二叉樹,除了多了一個旋轉的操作,還在成員變量中多了一個平衡因子的概念。
平衡因子
當樹的一個節點是根節點的時候,他的平衡因子 = 右子樹的最大深度 - 左子樹的最大深度
而一顆AVL樹,他一定需要具備以下的幾個特徵:
- 他的左右子樹都是AVL樹
- 左右子樹的高度之差的絕對值不超過1
(1,0,-1)
,也就是每一個根節點的平衡因子都是(1,0,-1)
中的一個。
那麼,當一顆樹是AVL樹的時候,就可以保證他是高度平衡的。
如果它有n個結點,其高度可保持在O(log n)
,搜索時間複雜度也爲O(log n)
AVL樹節點的定義
在AVL樹的節點的結構中,爲了調節平衡因子,所有就需要利用孩子節點找到父親節點,所以說爲了方便,就增加了一個父親節點的指針,用來指向他的父親節點。
而樹中節點所保存的數據類型是一個pair
的結構體,他是C++中的一個只有兩個元素的結構體模板,可以通過first,second
兩個指針來訪問兩個結構體的元素。
template<class Key,class Val>
struct tree_node
{
typedef tree_node<Key, Val> node;
typedef pair<Key, Val> data_type;
node* _parent; //雙親節點
node* _left; //左孩子節點
node* _right; //右孩子節點
data_type _data;//節點的值
int _bf; //平衡因子
tree_node(const data_type& data)
:_data(data), _bf(0)
{
_parent = _left = _right = nullptr;
}
};
對於整個AVL樹的結構,可以這樣來
template<class key, class val>
class avl_tree
{
public:
typedef key key_type;
typedef val val_type;
typedef tree_node<key_type,val_type> node;
avl_tree()
:root(nullptr)
{}
private:
node* root;//根節點
};
AVL數的插入
首先,我們需要明確一點,就是AVL樹在插入之前,他一定是一顆平衡的二叉樹。只是在插入這個新的節點之後,纔可能會變得不平衡,所以說平衡因子的取值只會是(-2,-1,0,1,2)
。所以說,只有出現(-2,2)
中的任何一個數,就說明以該節點爲根節點的子樹不平衡,需要調整。
對於前兩步而言,程序部分都是和平衡二叉樹一樣的,只是多了一個更新父親節點的操作
//如果樹中沒有節點
if (root == nullptr)
{
root = new node(data);
return true;
}
node* parent = nullptr;
node* cur = root;
//找到要插入的位置
while (cur)
{
if (cur->_data.first == data.first) return false;
parent = cur;
cur = (cur->_data.first < data.first) ? cur->_right : cur->_left;
}
cur = new node(data);
//插入 cur 到指定位置
if (parent->_data.first < data.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
接下來就是判斷是否需要旋轉,也就是插入節點後,這棵樹是否平衡?
這個時候,我們維護了兩個指針變量,一個是當前新插入的節點(cur),另一個是該節點的父親節點(parent)。所以說,我們可以循環的取更新平衡因子,直到父親節點(parent)是空的時候,就說明更新完畢了
//更新平衡因子
while (parent)
{
}
更新平衡因子 並 判斷是否旋轉
如何更新平衡因子?
對於插入一個節點,非三種情況:
- 新插入的節點就是AVL樹 的跟節點,這個時候只需要更新跟節點就可以了
- 新插入的節點在右子樹中,根節點的平衡因子 + 1
- 新插入的節點在左子樹中,根節點的平衡因子 - 1
//新插入的節點在右子樹
if (cur == parent->_right)
{
parent->_bf++;
}
else if (cur == parent->_left)//新插入的節點在左子樹
{
parent->_bf--;
}
在更新平衡因子的時候,我們選擇從下向上更新,而不是在找插入節點位置時候同時更新平衡因子。
我們需要考慮這樣一種情況,對於某一個根節點,我們插入新的節點後,並沒有使得這個根節點的左右子樹的高度發生變化。那麼我們是不需要更新根節點(以及根節點再向上的父親節點)的平衡因子的
所以說,對於更新完畢的平衡因子,我們做以下的處理
- 當更新後,父親節點的平衡因子爲
0
。說明對於父親節點向上的節點來說,他們的高度沒有發生變化,更新完畢了 - 父親節點的平衡因子爲
(1,-1)
。說明這個父親節點的高度發生了變化,會引起上面節點的高度發生變化,但是沒有失去平衡,所以需要向上迭代 - 父親節點的平衡因子爲
(-2,2)
。說明當前的父親節點已經不平衡了,我們需要進行旋轉操作。
if (parent->_bf == 0)
{
break;
}
else if (abs(parent->_bf) == 1)
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_bf) == 2)
{
//此時parent的子樹已經不平衡,需要調整
}
旋轉
對於一顆AVL樹,當需要進行旋轉的時候,他大體上一定是下面這四種類型:
- 當父親節點的平衡因子爲
-2
的時候,說明左子樹高度比較高
第二種情況經過化簡,其實就是這個模式
- 當父親節點的平衡因子爲
2
的時候,說明右子樹的高度比較高
同樣的,第三種情況化簡後可以是這樣
那麼對於第一種和第四種情況,他們都是隻有一條單邊,那麼我們只需要旋轉一次就可以了,此爲單旋
單旋
對於單旋而言,又根據旋轉方向的不同,分爲左單旋和右單旋兩種
- 左單旋,即爲向左進行旋轉
程序的實現
//右,右,左旋
void Right_Right(node* parent)
{
//保留父親的父親節點
node* pParent = parent->_parent;
//父親的右節點
node* pRight = parent->_right;
//右節點的左節點
node* pRLeft = pRight->_left;
//先把 pRLeft 和 parent 節點連接起來
parent->_right = pRLeft;
//更新 pRLeft 的父親節點爲 parent
if (pRLeft) pRLeft->_parent = parent;
//開始左旋
pRight->_left = parent;
parent->_parent = pRight;
//pRight 此時作爲新的父親節點,需要判斷是否是新的根節點
//並連接 pRight 和 pParent
if (pParent == nullptr)
root = pRight;
else
(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
pRight->_parent = pParent;
pRight->_bf = parent->_bf = 0;
}
- 右單旋,即爲向右進行旋轉。
程序實現
//左,左,右旋
void Left_Left(node* parent)
{
// 保留父親的父親節點
node* pParent = parent->_parent;
//父親的左節點
node* pLeft = parent->_left;
//左節點的右節點
node* pLRight = pLeft->_right;
//先把 pLRight 和 parent 節點連接起來
parent->_left = pLRight;
//更新 pLRight 的父親節點爲 parent
if (pLRight) pLRight->_parent = parent;
//開始右旋
pLeft->_right = parent;
parent->_parent = pLeft;
//pLeft 此時作爲新的父親節點,需要判斷是否是新的根節點
//並連接 pLeft 和 pParent
if (pParent == nullptr)
root = pLeft;
else
(parent == pParent->_left) ? pParent->_left = pLeft : pParent->_right = pLeft;
pLeft->_parent = pParent;
pLeft->_bf = parent->_bf = 0;
}
最後,因爲如果只進行單旋,那麼旋轉後的圖形的根節點的平衡因子一定是0,並且原本不平衡的那個節點由於變成了一個葉子節點,所以說平衡因子也爲0。
而最底下的那個節點,因爲在單旋的過程中,高度沒有發生變化,所以說他的平衡因子不變。
雙旋
對於雙旋的情況,我們重新用程序實現也是挺麻煩的,肯定還不簡潔。所以可以嘗試着把雙旋變成單旋來試試。
- 我們上面所示的第二種情況,當失衡節點的平衡因子是-2時
程序實現
//左,右,雙旋
void Left_Right(node* parent)
{
node* pLeft = parent->_left;
node* pLRight = pLeft->_right;
int pLRbf = pLRight->_bf;
//右,右,單旋
Right_Right(pLeft);
//左,左,單旋
Left_Left(parent);
//更新平衡因子
}
對於雙旋而言,我們需要再次更新平衡因子,因爲我們第一次單旋的分支並不一定是一個模板的分支,他可能只是單旋分支的前兩部分
其實這裏又有三種平衡因子變化的情況,就是當根節點的左節點的右節點,的平衡因子是 (0,-1,1)這三種情況的時候,對於雙旋後的平衡因子都有不同的結果
- 當該節點的平衡因子爲0時:
2. 當該點的平衡因子爲-1時:
- 當該點的平衡因子爲1時
所以說,在修改平衡因子的時候,可以判斷一下這個節點的情況,在進行賦值就可以了
if (pLRbf == 1)
{
parent->_bf = 0;
pLeft->_bf = -1;
pLRight->_bf = 0;
}
else if (pLRbf == -1)
{
parent->_bf = 1;
pLeft->_bf = 0;
pLRight->_bf = 0;
}
else if (pLRbf == 0)
{
parent->_bf = 0;
pLeft->_bf = 0;
pLRight->_bf = 0;
}
這裏需要注意的是,我們在修改平衡因子的過程是在旋轉完畢後的,那麼旋轉後這個點的平衡因子是會發生變化的,所以說我們需要在進行旋轉之前就保留這個點的平衡因子的值,然後用這個變量在最後去修改。(親測一下午)
爲什麼說這個點的取值一定是(0,-1,1)?
因爲我們這是一顆AVL樹,他在插入一個元素之前一定是一顆平衡的樹,我們進行雙旋的時候,這個點一定是一個平衡的節點,所以他的取值是(0,-1,1)
- 當失衡節點的平衡因子是2的時候,也就是第三種情況
那麼,他的旋轉過程我們可以再進行一次封裝,並且他調節平衡因子的過程和上面是差不多的,也是那三種情況。
void Right_Left(node* parent)
{
node* pRight = parent->_right;
node* pRLeft = pRight->_left;
int pRLbf = pRLeft->_bf;
//左,左,單旋
Left_Left(pRight);
//右,右,單旋
Right_Right(parent);
if (pRLbf == 1)
{
parent->_bf = -1;
pRight->_bf = 0;
pRLeft->_bf = 0;
}
else if (pRLbf == -1)
{
parent->_bf = 0;
pRight->_bf = 1;
pRLeft->_bf = 0;
}
else if (pRLbf == 0)
{
parent->_bf = 0;
pRight->_bf = 0;
pRLeft->_bf = 0;
}
}
AVL樹的驗證
對於驗證,我們只需要對數的結構進行遍歷(前中後序都可以),然後依次判斷每個節點的左右孩子節點的深度可以了。
另外,和插入操作相同,我們在遍歷的時候,需要傳入一個參數,也就是當前節點的指針。而這個樹的節點是被封裝好的私有類型,所以我們需要在類內部調用另一個接口。
public:
//判斷是否是平衡二叉樹
bool IsBF()
{
return _IsBF(root);
}
private:
//判斷是否是平衡
bool _IsBF(node* root)
{
if (root == nullptr) return true;
int leH = _Height(root->_left);//左子樹高度
int riH = _Height(root->_right);//右子樹高度
if (riH - leH != root->_bf)
{
cout << root->_data.first << " 平衡因子異常" << endl;
return false;
}
return _IsBF(root->_left) && _IsBF(root->_right);
}
//由根節點確定子樹高度
int _Height(node* root)
{
if (root == nullptr)
return 0;
return 1 + max(_Height(root->_left), _Height(root->_right));
}
最後在測試的時候,我使用了大量的隨機數來進行測試,程序一切正常