二叉樹的存儲方式以及遞歸和非遞歸的三種遍歷方式


樹的定義和基本術語

樹(Tree)是n(n>=0)個結點的有限集T,T為空時稱為空樹,否則它滿足如下兩個條件:

  (1)有且僅有一個特定的稱為根(Root)的結點;

  (2)其余的結點可分為m(m>=0)個互不相交的子集T1,T2,T3…Tm,其中每個子集又是一棵樹,並稱其為子樹(Subtree)。

樹形結構應用實例

1、日常生活:家族譜、行政組織結構;書的目錄

2、計算機:資源管理器的文件夾;

    編譯程序:用樹表示源程序的語法結構;

    數據庫系統:用樹組織信息;

    分析算法:用樹來描述其執行過程;

3、表達式表示 ( 如 a * b + (c – d / e) * f )

專業術語

1、結點的度(degree):某結點的子樹的分支個數

葉子(leaf)(終端結點),分支結點(非終端結點),內部結點(B、C、D、E、H),樹的度(3)

2、結點的孩子(child)

雙親(parent)(D為H、I、J的雙親)

兄弟(sibling)(H、I、J互為兄弟)

祖先,子孫(B的子孫為E、K、L、F)

3、結點的層次

根結點為第一層。某結點在第 i 層,其孩子在第 i+1 層。
樹的深度(depth)就是從跟開始往下數
堂兄弟:雙親在同一層的結點,互為堂兄弟

4、有序樹和無序樹

有序樹:   無序樹:

5、森林(forest)是 m (m≥0) 棵互不相交的樹的集合。

對比樹型結構和線性結構的結構特點

線性結構:第一個元素無前驅,最后一個元素無后繼,其它數據元素一個前驅、一個后繼。(唯一頭結點,唯一尾節點;中間結點有唯一前驅,唯一后繼)
樹形結構:根節點無前驅,多個葉子節點無后繼,其它元素一個前驅,多個后繼。(唯一根結點;多個葉結點;中間結點有唯一前驅,多個后繼)

二叉樹

把滿足以下兩個條件的樹型結構叫做二叉樹(Binary Tree):

   (1)每個結點的度都不大於2;

   (2)每個結點的孩子結點次序不能任意顛倒。即使只有一棵子樹也要進行區分,說明它是左子樹,還是右子樹。這是二叉樹與樹的最主要的差別。

二叉樹一共有5種形態

二叉樹的性質

性質1: 在二叉樹的第i層上至多有2^(i-1)個結點(i>=1)。

采用歸納法證明此性質。

  (1)當i=1時,2^( i-1)=2^0 =1,命題成立。

  (2)假定i=k時命題成立,即第k層最多有2^(k-1)個結點;

  (3)由歸納假設可知,由於二叉樹每個結點的度最大為2,故在第k+1層上最大結點數為第k層上最大結點數的2倍,

           即2×2^(k-1)=2^k=2^(k+1)-1

命題得到證明。

性質2 :深度為 k 的二叉樹至多有 2^k-1個結點(k≥1)。

證明:由性質1可見,深度為k的二叉樹的最大結點數為  

性質3: 對任何一棵二叉樹,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。

證明:設二叉樹上結點總數 n = n0 + n1 + n2   (1)

         又二叉樹上分支總數 b = n1+2n2           (2)

而除根結點外,其余結點都有分支進入,即 b = n-1

將(1)(2)式代入,得 n0 = n2 + 1 。

兩類特殊的二叉樹:滿二叉樹和完全二叉樹

滿二叉樹:一棵深度為k且有2^k-1個結點的二叉樹。

完全二叉樹:樹中所含的 n 個結點和滿二叉樹中編號為 1 至 n 的結點一一對應。(編號的規則為,由上到下,從左到右。)

性質4:具有n個結點的完全二叉樹的深度為[log2 n]+1。

證明:假設此二叉樹的深度為k,則根據性質2及完全二叉樹的定義得到:

     2^(k-1)-1<n<=2^k-1  或  2^(k-1)<=n<2^k

取對數得到:k-1 <= log2 n < k  因為k是整數。所以有:k=【log2n】+1。

性質5: 如果對一棵有n個結點的完全二叉樹的結點按層序編號(從第1層到第【log2n】+1層,每層從左到右),則對任一結點i(1<=i<=n),有:

1)如果i=1,則結點i無雙親,是二叉樹的根;如果i>1,則其雙親是結點【i/2】。

2)如果2i>n,則結點i為葉子結點,無左孩子;否則,其左孩子是結點2i。

3)如果2i+1>n,則結點i無右孩子;否則,其右孩子是結點2i+1。

所示為完全二叉樹上結點及其左右孩子結點之間的關系。

二叉樹的存儲結構

1)順序存儲結構

完全二叉樹:用一組連續的存儲單元依次自上而下、自左至右存儲各結點元素。即將完全二叉樹上編號為i  的結點的值存儲在下標為 i-1 的數組元素中。結點間的關系可由公式計算得到。

一般情形的二叉樹:增添一些空結點使變成完全二叉樹形態,再按上述方法存儲。

如圖完全二叉樹的存儲

  

單只二叉樹的存儲

總結:

1、完全二叉樹用順序存儲既節約空間,存取也方便;

2、一般二叉樹用順序存儲,空間較浪費,最壞情況為右單支二叉樹。(一個深度為K且只有K個節點的單支樹卻需要長度為2^k-1的一維數組)

2)二叉樹的鏈式存儲方式

常用的有二叉鏈表和三叉鏈表存儲結構結點的左右孩子或雙親靠指針來指示

有時也可用數組的下標來模擬指針,即開辟三個一維數組Data ,lchild,rchild 分別存儲結點的元素及其左,右指針域;下面是鏈式存儲的二叉樹表示:

typedef struct BiNode{
    int data;//數據域
    BiNode *lchild, *rchild;//左右孩子指針
} BiNode, *BiTree;

二叉樹鏈表表示的示例:

遍歷二叉樹和線索二叉樹

任何一個非空的二叉樹都由三部分構成
樹的遍歷是訪問樹中每個結點僅一次的過程。遍歷可以被認為是把所有的結點放在一條線上,或者將一棵樹進行線性化的處理。

先序遍歷

DLR根左右:訪問根結點、先序遍歷左子樹、先序遍歷右子樹

若二叉樹非空

    (1)訪問根結點;

    (2)先序遍歷左子樹;

    (3)先序遍歷右子樹;

若二叉樹為空,結束——基本項(也叫終止項)

若二叉樹非空——遞歸項

    (1)訪問根結點;

    (2)先序遍歷左子樹;

    (3)先序遍歷右子樹;

主要過程就是遞歸調用,也可以用棧來實現。

對於先序遍歷來說,藍色剪頭第一次經過的結點,就是遍歷的序列,以后再次經歷就不算進去了。

typedef struct BiNode{
    int data;//數據域
    BiNode *lchild, *rchild;//左右孩子指針
} BiNode, *BiTree;

void preorder(BiNode *root){
    if (root != NULL) {
        //訪問根節點
        cout << "先序遍歷" << root->data;
        preorder(root->lchild);
        preorder(root->rchild);
    }// end of if
}

非遞歸的先序遍歷

根據前序遍歷訪問的順序,優先訪問根結點,然后再分別訪問左孩子和右孩子。即對於任一結點,其可看做是根結點,因此可以直接訪問,訪問完之后,若其左孩子不為空,按相同規則訪問它的左子樹;當訪問其左子樹時,再訪問它的右子樹。因此其處理過程如下:

對於任一結點P:

     1)訪問結點P,並將結點P入棧;

     2)判斷結點P的左孩子是否為空,若為空,則取棧頂結點並進行出棧操作,並將棧頂結點的右孩子置為當前的結點P,循環至1);若不為空,則將P的左孩子置為當前的結點P;

     3)直到P為NULL並且棧為空,則遍歷結束。

//關鍵在於何時訪問的語句的位置
void preorder(BiTree root){
    //初始化棧
    stack<BiTree> nodes;
    BiNode *p = root;
    while (p != NULL || !nodes.empty()) {
        while (p != NULL) {
            //根左右的順序遍歷
            cout << p->data;
            //進棧
            nodes.push(p);
            //繼續移動
            p = p->lchild;
        }
        //p == null
        if (!nodes.empty()) {
            //對 p 重新指向
            p = nodes.top();
            //出棧
            nodes.pop();
            //轉到右子樹
            p = p->rchild;
        }
    }
}

中序遍歷、后序遍歷和先序遍歷思想基本類似,對於中序遍歷來說,藍色剪頭第二次經過的結點,就是遍歷的序列,之前的和以后的再次經歷就不算進序列里去了。對於后序遍歷來說,藍色剪頭第三次經過的結點,就是遍歷的序列,之前經歷的就不算進去了。

LDR左跟右:中序遍歷左子樹、訪問根結點、中序遍歷右子樹

若二叉樹非空

  (1)中序遍歷左子樹;

  (2)訪問根結點;

  (3)中序遍歷右子樹;

若二叉樹為空,結束——基本項(也叫終止項)

若二叉樹非空——遞歸項

    (1)中序遍歷左子樹;

    (2)訪問根結點;

    (3)中序遍歷右子樹;

中序遞歸遍歷算法

void inOrder(BiNode *root){
    if (root != NULL) {
        inOrder(root->lchild);
        cout << "先序遍歷" << root->data;
        inOrder(root->rchild);
    }// end of if
}

中序的非遞歸遍歷,使用棧

//非遞歸的中序遍歷二叉樹
void inOrder(BiTree root){
    //非遞歸中序遍歷(左跟右)
    stack<BiTree> nodes;//初始化棧
    //指示指針
    BiNode *p = root;
    //遍歷二叉樹的循環語句
    while (p != NULL || !nodes.empty()) {
        while (p != NULL) {
            //不為空就入棧
            nodes.push(p);
            //一直向做走,直到為 kong
            p = p->lchild;
        }
        // 需要判斷空否,因為需要出棧操作
        if (!nodes.empty()) {
            //令 p 重新指向 棧頂結點
            p = nodes.top();
            //訪問根節點(棧頂結點)
            cout << p->data << " ";
            //使用完畢,彈出
            nodes.pop();
            //向右遍歷
            p = p->rchild;
        }
    }// end of while
}
LRD左右跟:后序遍歷左子樹、后序遍歷右子樹、訪問根結點

后序遍歷序列:BDFGECA

//遞歸后續遍歷二叉樹
void lastOrder(BiTree root){
    if (root != NULL) {
        lastOrder(root->lchild);
        lastOrder(root->rchild);
        cout << root->data;
    }
}

同理有非遞歸的后續遍歷二叉樹

在后序遍歷中,要保證左孩子和右孩子都已被訪問,並且左孩子在右孩子訪問之后才能訪問根結點。因此對於任一結點P,先將其入棧。如果P不存在左孩子和右孩子,則可以直接訪問它;或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被訪問過了,則同樣可以直接訪問該結點。若非上述兩種情況,則將P的右孩子和左孩子依次入棧,這樣就保證了每次取棧頂元素的時候,左孩子在右孩子前面被訪問,左孩子和右孩子都在根結點前面被訪問。

void postOrder3(BiTree root)     //非遞歸后序遍歷
{
    stack<BiTree> nodes;
    //當前結點
    BiNode *cur;
    //前一次訪問的結點
    BiNode *pre = NULL;
    //根節點入棧
    nodes.push(root);
    //依次遍歷左右子樹
    while(!nodes.empty())
    {
        cur = nodes.top();
        //判斷 cur 結點的左右孩子子樹的情況
        if((cur->lchild == NULL && cur->rchild == NULL) ||
           (pre != NULL && (pre == cur->lchild || pre == cur->rchild)))
        {
            //如果當前結點沒有孩子結點或者孩子節點都已被訪問過
            cout << cur->data;
            //出棧
            nodes.pop();
            //前一次訪問的結點, pre標記已經訪問的結點
            pre = cur;
        }
        else
        {
            //左右跟的訪問順序,關鍵還是訪問語句的位置!!!一定是先寫右子樹,再寫左子樹,順序不能錯
            //如果當前結點的右子樹不為空
            if(cur->rchild != NULL){
                nodes.push(cur->rchild);
            }
            //如果當前結點的左子樹不為空
            if(cur->lchild != NULL){
                nodes.push(cur->lchild);
            }
        }
    }
}

二叉樹遍歷的總結:

無論先序、中序、后序遍歷二叉樹,遍歷時的搜索路線是相同的:從根節點出發,逆時針延二叉樹外緣移動,對每個節點均途經三次。

先序遍歷:第一次經過節點時訪問。(ABCD)

中序遍歷:第二次經過節點時訪問。(BADC)

后序遍歷:第三次經過節點時訪問。(BDCA)

 

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM