1. 程式人生 > >資料結構之:AVL樹詳解及C++模板實現

資料結構之:AVL樹詳解及C++模板實現

AVL樹簡介

AVL樹的名字來源於它的發明作者G.M. Adelson-Velsky 和 E.M. Landis。AVL樹是最先發明的自平衡二叉查詢樹(Self-Balancing Binary Search Tree,簡稱平衡二叉樹)。

一棵AVL樹有如下必要條件:

  1. 條件一:它必須是二叉查詢樹。
  2. 條件二:每個節點的左子樹和右子樹的高度差至多為1。

圖一中左邊二叉樹的節點45的左孩子46比45大,不滿足二叉搜尋樹的條件,因此它也不是一棵平衡二叉樹。
右邊二叉樹滿足二叉搜尋樹的條件,同時它滿足條件二,因此它是一棵平衡二叉樹。

左邊二叉樹的節點45左子樹高度2,右子樹高度0,左右子樹高度差為2-0=2,不滿足條件二;
右邊二叉樹的節點均滿足左右子樹高度差至多為1,同時它滿足二叉搜尋樹的要求,因此它是一棵平衡二叉樹。

AVL樹的查詢、插入、刪除操作在平均和最壞的情況下都是O(logn),這得益於它時刻維護著二叉樹的平衡。如果我們需要查詢的集合本身沒有順序,在頻繁查詢的同時也經常的插入和刪除,AVL樹是不錯的選擇。不平衡的二叉查詢樹在查詢時的效率是很低的,因此,AVL如何維護二叉樹的平衡是我們的學習重點。

AVL樹相關概念

  1. 平衡因子:將二叉樹上節點的左子樹高度減去右子樹高度的值稱為該節點的平衡因子BF(Balance Factor)。
    在圖二右邊的AVL樹上:
    節點50的左子樹高度為3,右子樹高度為2,BF= 3-2 = 1;
    節點45的左子樹高度為2,右子樹高度為1,BF= 2-1 = 1;
    節點46的左子樹高度為0,右子樹高度為0,BF= 0-0 = 0;
    節點65的左子樹高度為0,右子樹高度為1,BF= 0-1 = -1;
    對於平衡二叉樹,BF的取值範圍為[-1,1]。如果發現某個節點的BF值不在此範圍,則需要對樹進行調整。

  2. 最小不平衡子樹:距離插入節點最近的,且平衡因子的絕對值大於1的節點為根的子樹。

在圖三中,左邊二叉樹的節點45的BF = 1,插入節點43後,節點45的BF = 2。節點45是距離插入點43最近的BF不在[-1,1]範圍內的節點,因此以節點45為根的子樹為最小不平衡子樹。

AVL樹的實現詳解

1. 節點結構

struct AVLTreeNode
{
    AVLTreeNode(T value, AVLTreeNode<T>*l, AVLTreeNode<T>*r)
    :key(value), lchild(l), rchild(r){}
 
    T key;
    int
height; AVLTreeNode<T>* lchild; AVLTreeNode<T>* rchild; };

AVL的節點結構為AVLTreeNode,它包括:

  1. key:節點的值
  2. height: 節點的高度,用於計算父節點的平衡因子
  3. lchild : 若節點有左子樹,則lchild指向節點的左孩子,否則指向nullptr
  4. rchild : 若節點有右子樹,則rchild指向節點的右孩子,否則指向nullptr

在另外一些AVL實現的節點設計方案中,會把BF作為結點的一個屬性儲存起來,而在這裡我們儲存的是節點的高度,通過節點的高度我們也可以間接計算出節點的BF。例如節點A的左孩子的height = 2,右孩子的height = 1,那麼節點A的平衡因子為2 - 1 = 1.

2. AVL樹的抽象資料結構(ADT)

template<typename T>
class AVLTree
{
public:
    AVLTree();            //建構函式
    ~AVLTree();            //解構函式
 
    void preOrder();    //前序遍歷AVL樹
    void InOrder();        //中序遍歷AVL樹   
    void postOrder();    //後序遍歷AVL樹
 
    void print();        //列印AVL樹
    void destory();        //銷燬AVL樹
 
    void insert(T key);    //插入指定值的節點
    void remove(T key);    //移除指定值的節點
 
    AVLTreeNode<T>* search_recurse(T key);    //利用遞迴演算法進行指定值的查詢
    AVLTreeNode<T>* search_iterator(T key);    //利用迭代演算法進行指定值的查詢
    T minimum();        //返回AVL中的最小值
    T maximum();        //返回AVL中的最大值
 
    int height();        //返回樹的高度
 
private:
    AVLTreeNode<T>* root;    //AVL樹的根節點
 
private:
    void preOrder(AVLTreeNode<T>* pnode) const;
    void inOrder(AVLTreeNode<T>* pnode) const;
    void postOrder(AVLTreeNode<T>* pnode) const;
 
    void print(AVLTreeNode<T>* pnode,T key, int direction) const;
    void destory(AVLTreeNode<T>* & pnode);
 
    AVLTreeNode<T>* insert(AVLTreeNode<T>* &pnode, T key);       
    AVLTreeNode<T>* remove(AVLTreeNode<T>* & pnode, AVLTreeNode<T>* pdel); //刪除AVL樹中節點pdel,並返回被刪除的節點
 
    AVLTreeNode<T>* minimum(AVLTreeNode<T>*pnode)const;
    AVLTreeNode<T>* maximum(AVLTreeNode<T>*pnode)const;
 
    AVLTreeNode<T>* search_recurse(AVLTreeNode<T>* pnode, T key) const;
    AVLTreeNode<T>* search_iterator(AVLTreeNode<T>* pnode, T key) const;
 
    AVLTreeNode<T>* leftRotation(AVLTreeNode<T>* pnode);        //單旋:左旋操作
    AVLTreeNode<T>* rightRotation(AVLTreeNode<T>* pnode);        //單旋:右旋操作
    AVLTreeNode<T>* leftRightRotation(AVLTreeNode<T>* pnode);    //雙旋:先左旋後右旋操作
    AVLTreeNode<T>* rightLeftRotation(AVLTreeNode<T>* pnode);    //雙旋:先右旋後左旋操作
 
};

這裡我們定義了AVL樹這個型別AVLTree。它包含了:

  1. AVL樹的根節點root,這是唯一的資料成員
  2. 操作的外部介面與內部實現介面。例如 preOrder()為提供給使用者使用的介面,介面宣告為public;而preOrder(AVLTreeNode* pnode)是類內部為了遞迴操作所使用的介面,介面宣告為private。
  3. 旋轉操作(rotation)用來調整失去平衡的二叉樹,四個內部介面針對四種失衡情況進行調整,後面詳細解釋。

3. AVL樹的高度

如前所說,我們的節點結構中並不儲存結點的BF,取而代之的是節點的高度。一個節點的BF可由其左右子樹的高度計算出來。我們提供返回一個節點高度的操作

/*返回一棵樹的高度*/
template <typename T>
int AVLTree<T>::height(AVLTreeNode<T>* pnode)
{
    if (pnode != nullptr)
    {
        return pnode->height;
    }
    return 0;                                                                //如果是空樹,高度為0
};
 
template <typename T>
int AVLTree<T>::height()
{
    return height(root);
};

4. AVL樹失衡調整

節點的插入或刪除都有可能導致AVL樹失去平衡,因此,失衡調整是插入與刪除操作的基礎。
AVL樹的失衡調整可以分為四種情況,我們逐一分析。
假設我們要為陣列a[]={4,5,6,3,2,8,7,0,1}構建一棵AVL樹。

情況一:左單旋轉

首先插入{4,5,6},在插入元素6後出現不平衡的情況:

當我們在右子樹插入右孩子導致AVL失衡時,我們需要進行單左旋調整。旋轉圍繞最小失衡子樹的根節點進行。
在刪除新節點時也有可能會出現需要單左旋的情況。
左旋程式碼如下:

/*左旋轉操作*/
/*pnode為最小失衡子樹的根節點*/
/*返回旋轉後的根節點*/
template<typename T>
AVLTreeNode<T>* AVLTree<T>::leftRotation(AVLTreeNode<T>* proot)
{
    AVLTreeNode<T>* prchild = proot->rchild;
    proot->rchild = prchild->lchild;
    prchild->lchild = proot;
 
    proot->height = max(height(proot->lchild),height(proot->rchild))+1;     //更新節點的高度值
    prchild->height = max(height(prchild->lchild), height(prchild->rchild)) + 1; //更新節點的高度值
 
    return prchild;                    
};

結合例子進行分析:

  1. 引數proot為最小失衡子樹的根節點,在圖四中為節點4
  2. 若節點5有左子樹,則該左子樹成為節點4的右子樹
  3. 節點4成為節點5的左子樹
  4. 最後更新節點的高度值

情況二:右單旋轉

我們繼續插入元素{3,2},此時二叉樹為:

插入3、2後出現了不平衡的情況。此時的插入情況是“在左子樹上插入左孩子導致AVL樹失衡”,我們需要進行單右旋調整。
單右旋程式碼為:

/*右旋轉操作*/
/*pnode為最小失衡子樹的根節點*/
/*返回旋轉後的根節點*/
template <typename  T>
AVLTreeNode<T>* AVLTree<T>::rightRotation(AVLTreeNode<T>*proot)
{
    AVLTreeNode<T>* plchild = proot->lchild;
    proot->lchild = plchild->rchild;
    plchild->rchild = proot;
 
    proot->height = max(height(proot->lchild), height(proot->rchild)) + 1;     //更新節點的高度值
    plchild->height = max(height(plchild->lchild), height(plchild->rchild)) + 1; //更新節點的高度值
 
    return plchild;
};

結合例子進行分析:

  1. 引數proot為最小失衡子樹的根節點,在圖四中為節點4
  2. 若節點3有右子樹,則該右子樹成為節點4的左子樹
  3. 節點4成為節點3的左子樹
  4. 調整節點的高度值

情況三:先左旋後右旋

需要進行兩次旋轉的原因是第一次旋轉後,AVL樹仍舊處於不平衡的狀態,第二次旋轉再次進行調整。
我們繼續插入元素{8,7}

這種情況,總結起來就是“在右子樹上插入左孩子導致AVL樹失衡",此時我們需要進行先右旋後左旋的調整。
調整的程式碼為:

/*先右旋再左旋*/
/*引數proot為最小失衡子樹的根節點*/
/*返回旋轉後的根節點*/
template<typename T>
AVLTreeNode<T>* AVLTree<T>::rightLeftRotation(AVLTreeNode<T>* proot)
{
    proot->rchild = rightRotation(proot->rchild);
    return leftRotation(proot);
};

結合例子進行分析:

  1. 首先對最小不平衡子樹的根節點(也就是節點6)的右孩子(也就是8)進行右旋操作
  2. 再對節點6進行一次左旋操作

情況四:先左旋後右旋

根據對稱性原理,當我們“在左子樹上插入右孩子導致AVL樹失衡",此時我們需要進行先左旋後右旋的調整。如果你不理解接著看圖。
我們接著插入節點{0,1}

調整的程式碼:

/*先左後右做旋轉*/
/*引數proot為最小失衡子樹的根節點*/
/*返回旋轉後的根節點*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::leftRightRotation(AVLTreeNode<T>* proot)
{
    proot->lchild= leftRotation(proot->lchild);
    return rightRotation(proot);
};

結合例子進行分析:

  1. 首先對最小不平衡子樹的根節點(也就是節點2)的左孩子(也就是0)進行左旋操作
  2. 再對節點2進行一次右旋操作

總結:四種失衡調整

型別使用情形
單左旋在左子樹插入左孩子節點,使得平衡因子絕對值由1增至2
單右旋在右子樹插入右孩子節點,使得平衡因子絕對值由1增至2
先左旋後右旋在左子樹插入右孩子節點,使得平衡因子絕對值由1增至2
先右旋後左旋在右子樹插入左孩子節點,使得平衡因子絕對值由1增至2

5. 插入新節點

其實上面已經展示了一個完整的插入過程,結合例子很好理解下面這段程式碼。

/*插入操作*/
/*遞迴地進行插入*/
/*返回插入後的根節點*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::insert(AVLTreeNode<T>* &pnode, T key)
{
    if (pnode == nullptr)    //尋找到插入的位置
    {
        pnode = new AVLTreeNode<T>(key, nullptr, nullptr);
    }
    else if (key > pnode->key)    //插入值比當前結點值大,插入到當前結點的右子樹上
    {
        pnode->rchild = insert(pnode->rchild, key);
        if (height(pnode->rchild) - height(pnode->lchild) == 2) //插入後出現失衡
        {
            if (key > pnode->rchild->key) //情況一:插入右子樹的右節點,進行左旋
                pnode=leftRotation(pnode);
            else if (key < pnode->rchild->key)  //情況三:插入右子樹的左節點,進行先右再左旋轉
                pnode=rightLeftRotation(pnode);
        }
    }
    else if (key < pnode->key) //插入值比當前節點值小,插入到當前結點的左子樹上
    {
        pnode->lchild = insert(pnode->lchild, key);
        if (height(pnode->lchild) - height(pnode->rchild) == 2) //如果插入導致失衡
        {
            if (key < pnode->lchild->key)        //情況二:插入到左子樹的左孩子節點上,進行右旋
                pnode = rightRotation(pnode);
            else if (key>pnode->lchild->key)
                pnode = leftRightRotation(pnode);//情況四:插入到左子樹的右孩子節點上,進行先左後右旋轉
        }
    }
    pnode->height = max(height(pnode->lchild), height(pnode->rchild)) + 1;
    return pnode;
};

6.刪除節點

刪除節點也可能導致AVL樹的失衡,實際上刪除節點和插入節點是一種互逆的操作:

  1. 刪除右子樹的節點導致AVL樹失衡時,相當於在左子樹插入節點導致AVL樹失衡,即情況情況二或情況四。
  2. 刪除左子樹的節點導致AVL樹失衡時,相當於在右子樹插入節點導致AVL樹失衡,即情況情況一或情況三。

另外,AVL樹也是一棵二叉排序樹,因此在刪除節點時也要維護二叉排序樹的性質。

刪除的程式碼實現:

/*刪除指定元素*/
template<typename T>
AVLTreeNode<T>* AVLTree<T>::remove(AVLTreeNode<T>* & pnode, T key)
{
    if (pnode != nullptr)
    {
        if (key == pnode->key)            //找到刪除的節點
        {
            //因AVL也是二叉排序樹,刪除節點要維護其二叉排序樹的條件
            if (pnode->lchild != nullptr&&pnode->rchild != nullptr)        //若左右都不為空
            {
                // 左子樹比右子樹高,在左子樹上選擇節點進行替換
                if (height(pnode->lchild) > height(pnode->rchild))
                {
                    //使用左子樹最大節點來代替被刪節點,而刪除該最大節點
                    AVLTreeNode<T>* ppre = maximum(pnode->lchild);        //左子樹最大節點
                    pnode->key = ppre->key;                                //將最大節點的值覆蓋當前結點
                    pnode->lchild = remove(pnode->lchild, ppre->key);    //遞迴地刪除最大節點
                }
                else  //在右子樹上選擇節點進行替換
                {
                    //使用最小節點來代替被刪節點,而刪除該最小節點
                    AVLTreeNode<T>* psuc = minimum(pnode->rchild);        //右子樹的最小節點
                    pnode->key = psuc->key;                                //將最小節點值覆蓋當前結點
                    pnode->rchild = remove(pnode->rchild, psuc->key);    //遞迴地刪除最小節點
                }
 
            }
            else
            {
                AVLTreeNode<T> * ptemp = pnode;
                if (pnode->lchild != nullptr)
                    pnode = pnode->lchild;
                else if (pnode->rchild != nullptr)
                    pnode = pnode->rchild;
                delete ptemp;
                return nullptr;
            }
 
        }
        else if (key > pnode->key)//要刪除的節點比當前節點大,則在右子樹進行刪除
        {
            pnode->rchild =  remove(pnode->rchild, key);
            //刪除右子樹節點導致不平衡:相當於情況二或情況四
            if (height(pnode->lchild) - height(pnode->rchild) == 2) 
            {
                //相當於在左子樹上插入右節點造成的失衡(情況四)
                if (height(pnode->lchild->rchild)>height(pnode->lchild->lchild))
                    pnode = leftRightRotation(pnode);
                else//相當於在左子樹上插入左節點造成的失衡(情況二)
                    pnode = rightRotation(pnode); 
            }
        }
        else if (key < pnode->key)//要刪除的節點比當前節點小,則在左子樹進行刪除
        {
            pnode->lchild= remove(pnode->lchild, key);
             //刪除左子樹節點導致不平衡:相當於情況三或情況一
            if (height(pnode->rchild) - height(pnode->lchild) == 2)
            {
                
            
           

相關推薦

資料結構AVLC++模板實現

AVL樹簡介AVL樹的名字來源於它的發明作者G.M. Adelson-Velsky 和 E.M. Landis。AVL樹是最先發明的自平衡二叉查詢樹(Self-Balancing Binary Search Tree,簡稱平衡二叉樹)。一棵AVL樹有如下必要條件:條件一:它必

資料結構圖文解析哈夫曼與哈夫曼編碼C++模板實現

0. 資料結構圖文解析系列 1. 哈夫曼編碼簡介 哈夫曼編碼(Huffman Coding)是一種編碼方式,也稱為“赫夫曼編碼”,是David A. Huffman1952年發明的一種構建極小多餘編碼的方法。 在計算機資料處理中,霍夫曼編碼使用變長編碼表對源符號進行編碼,出現頻率較高的源符號採用較短的編碼,

資料結構圖文解析二叉堆C++模板實現

0. 資料結構圖文解析系列 1. 二叉堆的定義 二叉堆是一種特殊的堆,二叉堆是完全二叉樹或近似完全二叉樹。二叉堆滿足堆特性:父節點的鍵值總是保持固定的序關係於任何一個子節點的鍵值,且每個節點的左子樹和右子樹都是一個二叉堆。 當父節點的鍵值總是大於或等於任何一個子節點的鍵值時為最大堆。 當父節點的鍵值總是小於

資料結構圖文解析佇列C++模板實現

正文 回到頂部 0. 資料結構圖文解析系列 回到頂部 1. 佇列簡介 回到頂部 1.1 佇列的特點 佇列(Queue)與棧一樣,是一種線性儲存結構,它具有如下特點: 佇列中的資料元素遵循“先進先出”(First In First Out)的原則,簡稱FI

二叉之一BSTAVLB和紅黑原理分析

BST樹,AVL樹詳解及B樹和紅黑樹原理分析 網際網路面試中樹尤其是BST,AVL是提問的重點也是難點,甚至B樹乃至高階資料結構紅黑樹都是提問的重點,像阿里雲面試就曾經問過map實現機制(紅黑樹)及其原理,這裡我們要做到對BST/AVL完全熟悉能給出全部程式碼實現,紅黑樹、

RSA演算法C語言實現

1、什麼是RSA RSA公鑰加密演算法是1977年由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)一起提出的。1987年首次公佈,當時他們三人都在麻省理工學院工作。RSA就是他們

java資料結構ArrayList與LinkedList

前篇博文講到了Java底層怎麼知道ArrayList和LinkedList哪個是隨機訪問哪個是順序訪問的,本片博文主要介紹各自的資料特點。 ArrayList: 從他的這個建構函式我們可以知道他的底層實現就是一個Object物件陣列。 第二個建構函式構造一個空的

資料結構AVL

1.什麼是AVL樹 AVL樹又稱平衡二叉搜尋樹,它能保證二叉樹高度相對平衡,儘量降低二叉樹的高度,提高搜尋效率。單純的二叉搜尋樹在最壞的情況下插入查詢刪除等操作時間複雜度會是O(N), 例如: 所以,AVL樹就能避免這種情況,使得增刪查改的時間複雜度為O(lgN). (p

資料結構二叉的遍歷

一.二叉樹—特殊樹(基礎知識) (一)概念: 一個二叉樹要麼為空,要麼包含一個根節點,外加一個左子樹和右子樹,每一個字數都是二叉樹 (二)二叉樹的形狀: 不平衡二叉樹、完美平衡二叉樹、完全二叉樹、滿二叉樹; 一般來說二叉樹越平衡,訪問、插入、刪除的效能也越高; (三)二叉樹的遍歷; 1,前序

資料結構-時間複雜度計算--向李紅老師的資料結構低頭

今天早上突然想總結一下資料結構的時間複雜度的知識。 之前學了很多遍,但是一直沒有總結,所以之前參考了Algorithm還有清華大學出版的那個資料結構書,今天早上花了幾個小時好好的總結一下,也送給三班的同學們。 演算法的時間複雜度定義為: 在

資料結構紅黑個人筆記

   作者:Sky Wang    於 2013-08-08                              

資料結構查詢-基於的查詢法

目錄 基於樹的查詢法 二叉排序樹 二叉排序樹的定義與描述 二叉排序樹的插入與建立 平衡二叉排序樹 失衡二叉樹分類及調整方法 基於樹的查詢法 基於樹的查詢法是將待查表組織成特定樹的形式並

C Primer Plus--高階資料結構二叉

目錄 二叉搜尋樹 Binary Search Tree 用C構建二叉樹ADT 樹結構的定義 C Primer Plus--高階資料結構表示之二叉樹 二叉搜尋樹 Binary Search Tree 二叉樹是一種高階資料結構。樹中的每個節點都包含一個專案和兩個指向其他

設計模式(建立型)Java常用23種設計模式單例模式以及Java程式碼實現

可以說單例模式是所有設計模式中最簡單的一種。 單例模式就是說系統中對於某類的只能有一個物件,不可能出來第二個。 單例模式也是23中設計模式中在面試時少數幾個會要求寫程式碼的模式之一。主要考察的是多執行緒下面單例模式的執行緒安全性問題。 1.多執行緒安全單例模式例項一(不使用同步鎖)

python資料結構二叉

這裡用python 實現了二叉樹 # Definition for a binary tree node. class TreeNode: def __init__(self, x): self.val = x self.left =

資料結構二叉

二叉樹的性質 在二叉樹的第iii層上至多有2i−12^{i-1}2i−1個結點(i≥1)(i \geq 1)(i≥1)。 深度為kkk的二叉樹至多有2k−12^{k}-12k−1個結點(k≥1)(k\geq 1)(k≥1)。 對任何一棵二叉樹TTT,如果其終端

Python3&資料結構二叉

實現二叉樹以及遍歷 在電腦科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查詢樹和二叉堆。   二叉樹是遞迴定義的,其結點有左右子樹之分,邏輯上二叉樹有五種基本

資料結構二叉基本功能的實現

二叉樹的各種性質在這裡不再重複,本文實現二叉樹的基本操作,包括建立、前序輸出、中序輸出、後序輸出、刪除二叉樹、葉子結點個數、葉子節點的值、交換左右子樹 1.首先建立結構體: typedef struct Tree_Node{ char ch; struct

Ikaros的資料結構二叉(基礎概念部分)

二叉樹(Binary Tree) 在瞭解二叉樹之前你需要了解如下內容: 1.樹(Tree):是一種非線性資料結構(非線性資料結構包含樹和圖) ①樹的資料結構: 相關術語 a.根節點(root):樹中沒有前驅的結點 注:一棵樹中只有一個根節點 b.葉子結點(le

資料結構"二叉的三種遍歷方法"

1、什麼是二叉樹 定義:有且僅有一個根節點,每個節點只有一個父節點,最多含有兩個子節點,子節點有左右之分。 2、二叉樹的遍歷 二叉樹是一種樹形結構,遍歷就是要讓樹中的節點被且僅被訪問一次,即按一定規律排列成一個線性佇列。二叉樹是一種遞迴定義的結構,包含三個部分:根節點(