二叉樹的相關內容

二叉樹的遍歷
  1. 先序遍歷(NLR):先訪問根節點,再訪問左子樹,最後訪問右子樹。
  2. 中序遍歷(LNR):先訪問左子樹,再訪問根節點,最後訪問右子樹。
  3. 後序遍歷(LRN):先訪問左子樹,再訪問右子樹,最後訪問根節點。

       注:要進行二叉樹重建時,中序遍歷是必需要知道的,先序和後序只需知道其中一種。html

  • 遞歸方式實現三種遍歷方式
//先序遍歷--遞歸
int traverseBiTreePreOrder(BiTreeNode *ptree,int (*visit)(int))
{
    if(ptree)
    {
        if(visit(ptree->c))
            if(traverseBiTreePreOrder(ptree->left,visit))
                if(traverseBiTreePreOrder(ptree->right,visit))
                    return 1;  //正常返回
        return 0;   //錯誤返回
    }else return 1;   //正常返回
}
//中序遍歷--遞歸
int traverseBiTreeInOrder(BiTreeNode *ptree,int (*visit)(int))
{
    if(ptree)
    {
        if(traverseBiTreeInOrder(ptree->left,visit))
            if(visit(ptree->c))
                if(traverseBiTreeInOrder(ptree->right,visit))
                    return 1;
        return 0;
    }else return 1;
}
//後序遍歷--遞歸
int traverseBiTreePostOrder(BiTreeNode *ptree,int (*visit)(int))
{
    if(ptree)
    {
        if(traverseBiTreePostOrder(ptree->left,visit))
            if(traverseBiTreePostOrder(ptree->right,visit))
                if(visit(ptree->c))
                    return 1;
        return 0;
    }else return 1;
}

 

 
  • 非遞歸方式實現
先序遍歷:首先考慮非遞歸先序遍歷(NLR)。在遍歷某一個二叉(子)樹時,以一當前指針記錄當前要處理的二叉(左子)樹,以一個棧保存當前樹以後處理的右子樹。首先訪問當前樹的根結點數據,接下來應該依次遍歷其左子樹和右子樹,然而程序的控制流只能處理其一,因此考慮將右子樹的根保存在棧裏面,當前指針則指向需先處理的左子樹,爲下次循環作準備;若當前指針指向的樹爲空,說明當前樹爲空樹,不須要作任何處理,直接彈出棧頂的子樹,爲下次循環作準備。
 
//先序遍歷--非遞歸
int traverseBiTreePreOrder2(BiTreeNode *ptree,int (*visit)(int))
{
    Stack *qs=NULL;
    BiTreeNode *pt=NULL;
    qs=initStack();
    pt=ptree;
    while(pt || !isEmpty(qs))
    {
        if(pt)
        {
            //遍歷根節點
            if(!visit(pt->c)) return 0;  //錯誤返回
            push(qs,pt->right); //右子樹入棧
            pt=pt->left; //開始訪問左子樹
        }
        else pt=pop(qs); //不然依次出棧訪問右子樹
    }
    return 1;   //正常返回
}

 

中序遍歷:對於非遞歸中序遍歷,若當前樹不爲空樹,則訪問其根結點以前應先訪問其左子樹,於是先將當前根節點入棧,而後考慮其左子樹,不斷將非空的根節點入棧,直到左子樹爲一空樹;當左子樹爲空時,不須要作任何處理,彈出並訪問棧頂結點,而後指向其右子樹,爲下次循環作準備。
//中序遍歷--非遞歸
int traverseBiTreeInOrder2(BiTreeNode *ptree,int (*visit)(int))
{
    Stack *qs=NULL;
    BiTreeNode *pt=NULL;
    qs=initStack();
    pt=ptree;
    while(pt || !isEmpty(qs))
    {
        if(pt)
        {
            push(qs,pt); //根節點入棧
            pt=pt->left; //開始訪問左子樹
        }
        else
        {
            pt=pop(qs);
            if(!visit(pt->c)) return 0;
            pt=pt->right;
        }
    }
    return 1;
}

 

後序遍歷:因爲在訪問當前樹的根結點時,應先訪問其左、右子樹,於是先將根結點入棧,接着將右子樹也入棧,而後考慮左子樹,重複這一過程直到某一左子樹爲空;若是當前考慮的子樹爲空,若棧頂不爲空,說明第二棧頂對應的樹的右子樹未處理,則彈出棧頂,下次循環處理,並將一空指針入棧以表示其另外一子樹已作處理;若棧頂也爲空樹,說明第二棧頂對應的樹的左右子樹或者爲空,或者均已作處理,直接訪問第二棧頂的結點,訪問完結點後,若棧仍爲非空,說明整棵樹還沒有遍歷完,則彈出棧頂,併入棧一空指針表示第二棧頂的子樹之一已被處理。
 
//後序遍歷--非遞歸
int traverseBiTreePostOrder2(BiTreeNode *ptree,int (*visit)(int))
{
    Stack *qs=NULL;
    BiTreeNode *pt=NULL;
    qs=initStack();
    pt=ptree;
    while(1)  //循環條件恆「真」
    {
        if(pt)
        {
            push(qs,pt); //根節點先入棧
            push(qs,pt->right); //右子樹再入棧
            pt=pt->left; //開始遍歷左子樹
        }
        else if(!pt)
        {
            pt=pop(qs); //右子樹出棧
            //若是右子樹爲空,即沒有孩子
            if(!pt)
            {
                pt=pop(qs); //根節點出棧
                if(!visit(pt->c)) return 0;
                if(isEmpty(qs)) return 1;
                pt=pop(qs);
            }
            push(qs,NULL);
        }
    }
    return 1;
}

 

 

滿二叉樹:高度爲h,而且由2^h –1個結點的二叉樹,被稱爲滿二叉樹。

 

徹底二叉樹:一棵二叉樹中,只有最下面兩層結點的度能夠小於2,而且最下一層的葉結點集中在靠左的若干位置上。這樣的二叉樹稱爲徹底二叉樹。特色:葉子結點只能出如今最下層和次下層,且最下層的葉子結點集中在樹的左部。顯然,一棵滿二叉樹一定是一棵徹底二叉樹,而徹底二叉樹未必是滿二叉樹。

 


二叉查找樹(Binary Search Tree)
定義
  1. 每一個節點都不比它左子樹的任意節點小,並且不比它的右子樹的任意節點大。
  2. 任意節點,其左右子樹也分別是二叉查找樹。
  3. 沒有相等鍵值的節點。

 

 
 
 
 
 
 
 
 
 
查找
二叉查找樹能夠方便的實現查找算法。在查找元素x的時候,咱們能夠將x和根節點比較:
1. 若是x等於根節點,那麼找到x,中止查找 (終止條件)
2. 若是x小於根節點,那麼查找左子樹
3. 若是x大於根節點,那麼查找右子樹
二叉查找樹所須要進行的操做次數最多與樹的深度相等。n個節點的二叉查找樹的深度最多爲n,平均查找複雜度爲O(log(n))。
 
插入節點:
  1. 若是樹爲空,直接插入做爲根節點,而後返回。
  2. 若是樹不爲空,插入的節點小於根節點,則插入至左子樹,如此遞歸下去。
  3. 若是樹不爲空,插入的節點大於根節點,則插入至右子樹,如此遞歸下去。
注:新插入的節點必定是 葉子節點
 
刪除節點
刪除節點相對比較複雜。刪除節點後,有時須要進行必定的調整,以恢復二叉查找樹的性質(每一個節點都不比它左子樹的任意元素小,並且不比它的右子樹的任意元素大)。
  1. 葉節點能夠直接刪除。
  2. 當節點只有右子樹或者左子樹時,直接刪除這個節點而後將其右孩子或左孩子替代其位置。
  3. 刪除非葉節點時。好比下圖中的節點8,咱們能夠刪除左子樹中最大的元素(或者右子樹中最大的元素),用刪除的節點來補充元素8產生的空缺。但該元素可能也不是葉節點,因此它所產生的空缺須要其餘元素補充…… 直到最後刪除一個葉節點。上述過程能夠遞歸實現。

toios


AVL樹(自平衡二叉查找樹,Balanced Binary Tree)
爲了改善二叉查找樹的平均查找效率,從而提出了AVL樹。
定義:具備以下特性的二叉樹
  1. 是一棵二叉查找樹
  2. 任意節點的左右兩個子樹的高度差的絕對值不超過1
  3. 任意節點的左右子樹均爲AVL樹
 

 

AVL樹的查找
同二叉查找樹是一致的。
 
AVL樹的節點的平衡因子
節點的平衡因子是它的左子樹的高度減去它的右子樹的高度。帶有平衡因子 一、0 或 -1 的節點被認爲是平衡的。
 
AVL樹的節點的旋轉
樹的旋轉操做是爲了改變樹的結構,使其達到平衡。旋轉總共分爲左旋和右旋兩類。
給出記號:節點p,節點p的左孩子pL,節點p的右孩子pR;
以p爲軸右旋:p變爲pL的右孩子,pL的原右孩子變爲p的左孩子。
以p爲軸左旋:p變爲pR的左孩子,pR的原左孩子變爲p的右孩子。
插入一個節點,必定能夠經過1~2次旋轉(多是左右組合旋轉)達到平衡。

 

 
往AVL樹中插入節點:向AVL樹插入能夠經過如同它是未平衡的二叉查找樹同樣把給定的值插入樹中,接着自底向上向根節點折回,於在插入期間成爲不平衡的全部節點上進行旋轉來完成。
1、若是路徑上節點平衡因子是0,則插入後不會打破這個節點的平衡性。
2、若是路徑上的節點的平衡因子是1或-1,則可能會打破平衡性,在這種狀況下若是此節點的新的平衡因子是0,則恰好將其補的更加平衡,平衡性未打破;不然平衡因子變成2或-2,則須要進行調整。
3、咱們在對樹進行調整後恢復子樹相對於插入前的高度,不改變子子樹的平衡因子。
 
刪除AVL樹中的節點
方法1:將要刪除的節點向下旋轉成葉子節點,而後直接刪除便可,向下旋轉的過程當中樹可能會不知足二叉查找樹的性質,但刪除結束後必定仍爲AVL樹。
方法2:如同普通二叉查找樹同樣刪除節點,若刪除後未達到平衡,再經過旋轉使樹達到平衡。
 

紅黑樹(Red Black Tree)
定義:紅黑樹是每一個節點都帶有顏色屬性的二叉查找樹,顏色或紅色或黑色。
有以下性質:
性質1. 任意節點是紅色或黑色。
性質2. 根節點是黑色。
性質3 每一個葉子節點(指的是NIL節點,空節點)是黑色的。
性質4 每一個紅色節點的兩個子節點都是黑色。(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點)
性質5. 從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點。
以上性質能夠推出 關鍵性質: 從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長
特色:紅黑樹的平衡性不如AVL樹,可能只是局部平衡,但只要知足上面幾個性質便可。雖然是局部平衡,可是它的平均查找效率與AVL樹至關(O(log(n))),統計性能要好於通常的AVL樹。
 

 

往紅黑樹中插入節點
整個過程十分複雜,簡單說來是首先根據二叉查找樹同樣,將節點插入樹中,若插入後不違反紅黑樹的各個性質,那麼無需改變紅黑樹的結構;相反地,若是違反了性質,則要先經過相似於AVL樹的左旋和右旋使得紅黑樹局部平衡,而後再根據性質對節點進行着色。
 
刪除紅黑樹的節點
刪除的結點的方法與常規二叉搜索樹中刪除結點的方法是同樣的,若是它的子結點是沒有左孩子或者右孩子,那就用直接刪除它,用NIL來頂替它的位置;若是被刪除的結點只有一個左孩子或者右孩子,則直接刪除這個結點,用它的惟一子結點頂替它的位置;若是該節點即有左孩子又有右孩子,咱們就把它的直接後繼結點內容複製到它的位置,以後以一樣的方式刪除它的後繼結點,它的後繼結點不多是雙子非空,所以此傳遞過程最多隻進行一次。最後刪除結束後可能違反了紅黑樹的性質,再經過改變着色來修復該樹的紅黑樹性質。
 
插入和刪除的參考資料
 
紅黑樹的一個應用:C++中的set、map、multiset和multimap
針對set和map提出四個問題
1. 爲什麼map和set的插入刪除效率比用其餘序列容器高?
2. 爲什麼每次insert以後,之前保存的iterator不會失效?
3. 爲什麼map和set不能像vector同樣有個reserve函數來預分配數據?
4. 當數據元素增多時(10000到20000個比較),map和set的插入和搜索速度變化如何?
(從他們的數據結構、儲存方式、排序、查找、插入、刪除的特性來考慮這幾個問題)
哈希表的一個應用:C++11中的unordered_set、unordered_map、unordered_multiset和unordered_multimap
unordered容器的內部數據結構是基於hash table實現的,所以它其中儲存的鍵值(key)是 無序儲存的,可是它的查找效率確實接近常數級的!unordered容器使用「桶」來存儲元素,散列值相同的被存儲在一個桶裏。當散列容器中有大量數據時,同一個桶裏的數據也會增多,形成訪問衝突,下降性能。爲了提升散列容器的性能,unordered庫會在插入元素是自動增長桶的數量,不須要用戶指定。
來看一個示例程序:
//test map & unordered_map
#include <iostream>
#include <map>
#include <unordered_map>
#include "time.h"

using std::cout;
using std::endl;
using std::map;
using std::unordered_map;
using std::pair;

int main()
{
    //首先測試unordered_map
    unordered_map<int, int> hash;
    //測試插入效率 
    time_t first_time = time(0); //記錄當前時間 
    for(int i = 0; i < 20000000; ++i)
    {
        hash[i] = 0;
    } 
    cout << hash.size() << endl;
    time_t second_time = time(0); 
    //測試查找效率
    for(int i = 0; i < 20000001; ++i)
    {
        unordered_map<int, int>::iterator it = hash.find(i);
        if(it == hash.end())
        {
            cout << "false" << endl;
        }
    } 
    time_t third_time = time(0);
    cout << "second - first = " << second_time - first_time << endl;
    cout << "third - second = " << third_time - second_time << endl;
    
    
    //而後測試map
    map<int, int> rb_tree;
    //測試插入效率 
    first_time = time(0); //記錄當前時間 
    for(int i = 0; i < 20000000; ++i)
    {
        rb_tree[i] = 0;
    } 
    cout << rb_tree.size() << endl;
    second_time = time(0); 
    //測試查找效率
    for(int i = 0; i < 20000001; ++i)
    {
        map<int, int>::iterator it = rb_tree.find(i);
        if(it == rb_tree.end())
        {
            cout << "false" << endl;
        }
    } 
    third_time = time(0);
    cout << "second - first = " << second_time - first_time << endl;
    cout << "third - second = " << third_time - second_time << endl;
    
    
    
    return 0;
}
測試輸出:

能夠看出不管是插入仍是查找小,unordered_map的時間都比map要小。git

 
總結
無序容器時候用unordered_map,有序容器時候用map;須要頻繁查找元素用unordered_map,查詢無需很快但須要穩定查找效率則首選map。
 
參考博客
 

伸展樹(Splay Tree)
也是一種自平衡二叉查找樹,它能在O(n log n)內完成插入、查找和刪除操做,提出的緣由也是爲了提升AVL樹在最壞狀況下的查找效率。
 

還有許許多多種類的樹....
相關文章
相關標籤/搜索