MOOC 數據結構 | 3. 樹(上)

1.什麼是樹

客觀世界中許多事物存在層次關係

  • 人類社會家譜

    

  • 社會組織架構
  • 圖書信息管理

爲什麼數據結構中要採用樹?社會管理等要採用層次結構?

分層次組織在管理上具有更高的效率!

舉例分析:

數據管理的基本操作之一:查找

如何實現有效率的查找?

查找(Searching)

查找:根據某個給定關鍵字K,集合R中找出關鍵字與K相同的記錄

靜態查找:集合中記錄是固定

  • 沒有插入和刪除操作,只有查找    (如查字典)

動態查找:集合中記錄是動態變化

  • 除查找,還可能發生插入和刪除

靜態查找

在數組中查找元素。

方法1:順序查找

將元素放在數組中,在數組外用一個結構來指向該數組:

該結構有兩個分量:指針指向數組的頭,元素的個數:

可以看到,數組元素的存放是從1開始的:

之所以這樣設計是爲了介紹一種技巧:哨兵。

哨兵的作用是不用每次都去判斷下標是否到達了邊界。

如下是無哨兵和有哨兵的區別:

typedef struct LNode *List;
struct LNode{
    ElementType Element[MAXSIZE];
    int Length;
};

//順序查找的一種實現(無“哨兵”)
int SequentialSearch(List Tbl, ElementType K)
{
    /*在Element[1]~Element[n]中查找關鍵字爲K的數據元素*/
    int i;
    
    for(i = Tbl->Length; i>0 && Tbl->Element[i] != K; i--);
    return i;  /*查找成功返回所在單元下標;不成功返回0*/
}

typedef struct LNode *List;
struct LNode{
    ElementType Element[MAXSIZE];
    int Length;
};


int SequentialSearch(List Tbl, ElementType K)
{
    /*在Element[1]~Element[n]中查找關鍵字爲K的數據元素*/
    int i;
    Tbl->Element[0] = K;  /*建立哨兵*/
    for(i = Tbl->Length; Tbl->Element[i] != K; i--);
    return i;  /*查找成功返回所在單元下標;不成功返回0*/
}

上述例子中,可以看到將關鍵字K放到了下標0位置處。

順序查找時間複雜度爲O(n),平均時間複雜度爲\frac{n}{2} (最好情況是第一個就是,最壞情況最後一個纔是)。

方法2:二分查找(Binary Search)

假設n個數據元素的關鍵字滿足有序(比如:小到大) k_1< k_2< ...< k_n並且是連續存放(數組),那麼可以進行二分查找。

【例】假設有13個元素,按關鍵字由小到大順序存放。二分查找關鍵字爲444的數據元素過程如下:

1、left = 1,right = 13;mid = (1+13)/2 = 7:   100 < 444;


縮小查找範圍:

2、left = mid + 1 = 8,right = 13;mid = (8+13)/2=10:  321 < 444;


又縮小範圍:

3、left = mid + 1 = 11,right = 13;mid = (11+13)/2 = 12:    查找結束;


【例】仍然以上面13個數據元素構成的有序線性表爲例,二分查找關鍵字爲43的數據元素如下:

1、left =1, right =13;mid = (1+13)/2 = 7:           100 > 43;


縮小範圍:

2、left = 1,right = mid-1 = 6;mid = (1+6)/2 = 3:     39 < 43;


所以要挪動left的位置:

3、left = mid +1 = 4,right = 6;mid = (4+6)/2 = 5:   51 > 43;


說明要尋找的值落在51前面,修改right值:

4、left = 4,right = mid - 1 = 4;mid = (4+4)/2 = 4:     45 > 43;


5、left = 4,right = mid -1 = 3;left > right?   查找失敗,結束;


二分查找算法

typedef struct LNode *List;
struct LNode{
    ElementType Element[MAXSIZE];
    int Length;
};

int BinarySearch(List Tbl, ElementType K)
{
    /*在表Tbl中查找關鍵字爲K的數據元素*/
    int left, right, mid, NoFound = -1;
    left = 1;    /*初始左邊界,數組中是從下標1開始存放數據的*/
    right = Tbl->Length;     /*初始右邊界*/
    while(left <= right)
    {
        mid = (right - left)/2 + left;       /*防止溢出,計算中間元素座標*/
        if (K < Tbl->Element[mid])   
            right = mid-1;     /*調整右邊界*/
        else if (K > Tbl->Element[mid])   
            left = mid+1;      /*調整左邊界*/
        else 
            return mid;         /*查找成功,返回數據元素的下標*/
    }
    return NoFound;            /*查找不成功,返回-1*/
}

查找過程中每次都是除以2, 除以2,.... 除以多少次等於1,即n/2^{x} = 1,所以結果就是x = logN。

二分查找算法具有對數的時間複雜度O(logN)


【※】11個元素的二分查找判定樹

從下標爲1的地方開始放元素,放到下標爲11的地方。二分查找某個元素的過程一定是按照這樣的層次結構來的:

       

  • 判斷樹上每個結點需要的查找次數剛好爲該結點所在的層數;  (比如位於4號位置,則比較3次)
  • 查找成功時查找次數不會超過判定樹的深度
  • n個結點的判斷樹的深度爲{\color{Red} \left [ log_{2}n \right ] +1}
  • ASL = (4*4 + 4*3 + 2*2 + 1)/ 11 = 3  (平均成功查找次數)

2. 樹的定義

樹(Tree):n ( n≥0)個結點構成的有限集合。

      當n = 0時,稱爲空樹

      對於任一棵非空樹(n>0),它具備以下性質:

  • 樹中有一個稱爲“根(Root)”的特殊結點,用{\color{Blue} r}表示;
  • 其餘結點可分爲m(m>0)個互不相交的有限集T_1,T_2,...,T_m,其中每個集合本身又是一棵樹,稱爲原來樹的“子樹(SubTree)”

上圖樹T的根就是A,由如下四個子樹構成:

2.1 ※樹與非樹?

(多了C-D的連線,無法切分爲不相交的集合)

(多了C-E的連線)

(多了D-G的連線)

  • 子樹是不相交的;
  • 除了根結點外,每個結點有且僅有一個父結點
  • 一棵N個結點的樹有N-1條邊。 (每個結點都有向上的一根連接父結點的線,除了根結點,所以是N-1)

樹是保證結點連通的最小的連接方式(即邊最少)。

2.2 ※樹的一些基本術語

  1. 結點的度(Degree):結點的子樹個數       --如上圖:結點A的度爲3,B爲2,C爲1,D爲3,F爲0,...
  2. 樹的度:樹的所有結點中最大的度數       --如上圖:A和D的度都爲3,所以樹的度爲3
  3. 葉結點(Leaf):度爲0的結點
  4. 父結點(Parent):有子樹的結點是其子樹的根結點的父結點
  5. 子結點(Child):若A結點是B結點的父結點,則稱B結點是A結點的子結點;子結點也稱孩子結點
  6. 兄弟結點(Sibling):具有同一父結點的各結點彼此是兄弟結點。
  7. 路徑和路徑長度:從結點n_1n_k路徑爲一個結點序列n_1,n_2,...,n_k,n_in_{i+1}的父結點。路徑所包含邊的個數爲路徑的長度
  8. 祖先結點(Ancestor):沿樹根到某一結點路徑上的所有結點都是這個結點的祖先結點。
  9. 子孫結點(Descendant):某一結點的子樹中的所有結點是這個結點的子孫。
  10. 結點的層次(Level):規定根結點在1層,其他任一結點的層數是其父結點的層數加1。
  11. 樹的深度(Depth):樹中所有結點中的最大層次是這棵樹的深度。

3. 樹的表示

如圖的一棵樹,能否用數組實現?

==>用數組實現:就是把這些結點按順序用數組存起來,難度大,因爲難以分清結點的父結點和子孫結點等。

用鏈表實現:

==>每個結點用個結構來表示

存在的問題:每個結點的結構不同,不知道結點有幾個子孫結點,爲程序實現帶來了難度。

另一種思路:每個結點的結構設計爲相同的,如都同A一樣,設計爲3個指針域,那麼假設有n個結點,就一共需要3n-1個指針域,但是n個結點實際上就只有n-1個指針域不爲空。這種思路也不行。

下面介紹一種方法:

3.1 兒子-兄弟表示法

樹上的結點結構統一。

FirstChild指針指向第一個兒子,NextSibiling指向下一個兄弟結點。

例如:

將這個表示方法旋轉45°

 

旋轉45°後的這棵樹每個結點都有兩個指針,每個結點最多兩個兒子,這種樹叫做二叉樹。

4. 二叉樹

4.1 二叉樹的定義

二叉樹T:一個有窮的結點集合。

        這個集合可以爲空

        若不爲空,則它是由根結點和稱爲其左子樹{\color{Blue} T_L}右子樹{\color{Blue} T_R}的兩個不相交的二叉樹組成。

□二叉樹具體五種基本形態

□ 二叉樹的子樹有左右順序之分

 (這也是與度爲2的樹的區別)

4.2 特殊二叉樹

  • 斜二叉樹(Skewed Binary Tree)

                                              

  • 完美二叉樹(Perfect Binary Tree) / 滿二叉樹(Full Binary Tree)

                                 

  • 完全二叉樹(Complete Binary Tree)

      有n個結點的二叉樹,對樹中結點按從上至下、從左到右順序進行編號,編號爲i( 1 ≤ i≤ n)結點與滿二叉樹中編號爲i結點在二叉樹中位置相同

(這是一棵完全二叉樹)

所以,不是完全二叉樹。

4.3 二叉樹的幾個重要性質

  • 一個二叉樹第 i 層的最大結點數爲{\color{Blue} 2^{i-1}, i \geq 1}
  • 深度爲k的二叉樹有最大結點總數爲:{\color{Blue} 2^{k}-1,k\geq 1}   (1 + 2^{1} + 2^2+...+2^{k-1} = 2^{k} -1)---完美二叉樹可以達到
  • 對任何非空二叉樹T,若{\color{Blue} n_0}表示葉結點的個數、{\color{Blue} n_2}是度爲2的非葉結點個數,那麼兩者滿足關係{\color{Blue} n_0 = n_2+1}

                           (n_1表示只有一個兒子的結點)

該結論的證明:

         結點總個數:n_0+n_1+n_2

         總的邊數:(每個結點有向上的邊一條除了根結點){\color{Blue} n_0+n_1+n_2 -1 = 0\ast n_0 + 1\ast n_1+2\ast n_2} (不同類型結點向下的邊的條數)

4.4 二叉樹的抽象數據類型定義

類型名稱:二叉樹

 

 

數據對象集:一個有窮的結點集合

        若不爲空,則由根結點和其左、右二叉子樹組成。

 

操作集:BT∈BinTree,Item∈ElementType,重要操作有:

  1. Boolean IsEmpty(BinTree BT): 判別BT是否爲空
  2. void Traversal(BinTree BT): 遍歷,按某順序訪問每個結點
  3. BinTree  CreatBinTree(): 創建一棵二叉樹

常用的遍歷方法有:

  • void PreOrderTraversal(BinTree BT):  先序----根、左子樹、右子樹
  • void InOrderTraversal(BinTree BT)中序---左子樹、根、右子樹
  • void PostOrderTraversal(BinTree BT)後序---左子樹、右子樹、根
  • void LevelOrderTraversal(BinTree BT)層次遍歷,從上到下,從左到右

4.4 二叉樹的存儲結構

1. 順序存儲結構

完全二叉樹:按從上至下、從左到右順序存儲n個結點的完全二叉樹的結點父子關係

        

  • 非根結點(序號i > 1)的父結點的序號是{\color{Blue} \left \lfloor i/2 \right \rfloor};    ------如C結點,它的父結點是4/2 = 2 ,即B;如S結點,它的父結點5/2 = 2,即B
  • 結點(序號爲i)的左孩子結點的序號是2i,(若2i<=n,否則沒有左孩子);  -----如S結點,5*2 = 10大於9,就不存在
  • 結點(序號爲i)的右孩子結點的序號是2i+1,(若2i+1 <=n,否則沒有右孩子);

一般二叉樹也可以採用這種結構,但會造成空間浪費......

     補全爲完全二叉樹--->

補全爲完全二叉樹後可以用數組存儲,但是有很多都是空的,會造成空間浪費。

2.鏈表存儲

數據結構定義:

typedef struct TreeNode *BinTree;
typedef BinTree Position;
struct TreeNode
{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};

4.5 二叉樹的遍歷

4.5.1  二叉樹的遞歸遍歷

(1)先序遍歷

     遍歷過程爲:

     ①訪問根結點

     ②先序遍歷其左子樹;        ---遞歸地遍歷左子樹

     ③先序遍歷其右子樹。        ---遞歸地遍歷右子樹

void PreOrderTraversal(BinTree BT)
{
    if(BT)
    {
        printf("%d", BT->Data);
        PreOrderTraversal(BT->Left);
        PreOrderTraversal(BT->Right);
    }
}

(左邊部分的順序是:ABDFE,右邊是CGHI)

先序遍歷===>   A B D F E C G H I

A  (B D F E)  (C G H I)

(2) 中序遍歷

遍歷過程爲:

中序遍歷其左子樹

②訪問根結點

中序遍歷其右子樹

void InOrderTraversal(BinTree BT)
{
    if(BT)
    {
        InOrderTraversal(BT->Left);
        printf("%d",BT->Data);
        InOrderTraversal(BT->Right);
    }
}

(左邊部分的順序:D B E F,右邊順序:G H C I)

中序遍歷==> D B E F A G H C I

(D B E F)  A  (G H C I)

(3)後序遍歷

遍歷過程爲:

後序遍歷其左子樹

後序遍歷其右子樹;

③訪問根結點。

void PostOrderTraversal(BinTree BT)
{
    if(BT)
    {
        PostOrderTraversal(BT->Left);
        PostOrderTraversal(BT->Right);
        printf("%d", BT->Data);
    }
}

(根結點左邊部分順序:D E F B  右邊部分:H G I C)

後序遍歷==> D E F B H G I C A

(D E F B)  (H G I C)   A

歸納總結

每個結點都會被碰到三次,第一次碰到就輸出的叫做先序,第二次碰到輸出的叫做中序,第三次碰到輸出的叫做後序。

4.5.2  二叉樹的非遞歸遍歷

中序遍歷非遞歸遍歷算法

一開始碰到A的時候不能輸出,那麼遍歷完了怎麼知道又回到這裏來了呢?所以用堆棧。

碰到A,因爲是中序,所以A入棧;因爲是中序,先遍歷左子樹,所以B入棧;繼續往左,D入棧;再往左沒有了,所以就要往回走,往回走就是pop一個元素,也就是D被pop出來被打印。D無右孩子,又往回走,所以pop B,B打印出來;B因爲有右孩子,所以碰到了F,此時還不能print F,所以F 入棧;再往左走,碰到E,E入棧;E無左子樹,所以堆棧拋出E,E無右子樹,所以往回走,拋出F, F無右孩子,所以繼續拋出堆棧中的A。

  • 遇到一個結點,就把它壓棧,並去遍歷它的左子樹
  • 左子樹遍歷結束後,從棧頂彈出這個結點並訪問它
  • 然後按其右指針再去中序遍歷該結點的右子樹
void InOrderTraversal(BinTree BT)
{
    BinTree T = BT;
    Stack S = CreatStack(MaxSize);  /*創建並初始化堆棧S*/
    while( T || !IsEmpty(S) )
    {
        while(T)       /*一直向左並將沿途結點壓入堆棧*/
        {
            Push(S,T);   //-->第一次碰到該結點
            T = T->Left;
        }
        if (!IsEmpty(S))
        {
            T = Pop(S);  /*結點彈出堆棧*/     //---->第二次碰到該結點
            printf("%5d", T->Data);     /*訪問(打印)結點*/
            T = T->Right;     /*轉向右子樹*/
        }
    }
}

先序遍歷的非遞歸遍歷算法?

void PreOrderTraversal(BinTree BT)
{
    BinTree T = BT;
    Stack S = CreatStack(MaxSize);  /*創建並初始化堆棧S*/
    while( T || !IsEmpty(S) )
    {
        while(T)       /*一直向左並將沿途結點壓入堆棧*/
        {
            printf("%5d", T->Data);     /*訪問(打印)結點*/
            Push(S,T);
            T = T->Left;
        }
        if (!IsEmpty(S))
        {
            T = Pop(S);  /*結點彈出堆棧*/
            T = T->Right;     /*轉向右子樹*/
        }
    }
}

後序遍歷的非遞歸算法   (自己編寫)

void PostOrderTraversal(BinTree BT)
{
    BinTree T = BT;
    Stack S = CreatStack(MaxSize);
    BinTree PrePop;  //記錄上次出棧的結點
    while( T || !isEmpty(S))  /*若樹的結點未訪問完或堆棧不空*/
    {
        while(T)  /*先遍歷左子樹*/
        {
            Push(S,T);   
            T = T->Left;
        }
        T = Pop(S);   /*while結束,說明左子樹已經遍歷完畢,就要往回走,就彈出棧頂結點*/
        /*棧頂結點是否能輸出,取決於該結點是否有右孩子,如果沒有,就可以直接輸出*/
        //此處分兩種情況:1是該結點右結點爲空;2是該結點的右結點上次已經訪問(輸出)過,即是下面的返回結束往回走了 
        if( !T->Right || T->Right == PrePop) 
        {
            printf("%5d", T->Data);
            PrePop = T;
            T = NULL;          //將結點置爲空,以便可以繼續從堆棧中彈出結點
        } 
        else      /*如果有右孩子且該右孩子未被訪問過,則該結點重新入棧,並轉向右子樹*/
        {
            T = Push(S,T);  
            T = T->Right;     /*轉向右子樹*/
        }
    }
}

4.5.3 層序遍歷

二叉樹遍歷的核心問題:二維結構的線性化

  • 從結點訪問其左、右兒子結點
  • 訪問左兒子後,右兒子結點怎麼辦?
    • 需要一個存儲結構保存暫時不訪問的結點
    • 存儲結構:堆棧(保存自己)、隊列(保存右孩子)

隊列實現:遍歷從根結點開始,首先將根結點入隊,然後開始執行循環:結點出隊、訪問該結點、其左右兒子入隊

視頻描述:層序遍歷二叉樹的過程

步驟如下:

1、初始狀態

2、從根結點開始,把A放到隊列中

3、接下來開始做循環:隊列中拋出一個元素(A),把左右兒子放進去(B C)---遍歷的結果就爲A了

4、又從隊列中拋出第一個元素B,輸出B,然後把B的左右兒子(D F)放入隊列

5、再從中拋出元素C,將其左右兒子(G  I)放入隊列

6、拋出D,D沒有左右兒子就沒有元素要放到隊列中

7、進一步循環,拋出F,將F的左兒子E放入隊列

8、.....

層序遍歷=> A B C D F G I E H

訪問順序:

層序基本過程:先根結點入隊,然後:

①從隊列中取出一個元素;

訪問該元素所指結點;

③若該元素所指結點的左、右孩子結點非空,則將其左、右孩子的指針順序入隊

void LevelOrderTraversal(BinTree BT)
{
    Queue Q;
    BinTree T;
    if (!BT) return;  /*若是空樹,則直接返回*/
    Q = CreatQueue(MaxSize);  /*創建並初始化隊列Q*/
    AddQ(Q, BT);  //根結點放入隊列中
    while(!IsEmpty(Q))
    {
        T = DeleteQ(Q);
        printf("%d\n",T->Data);  /*訪問取出隊列的結點*/
        if(T->Left) AddQ(Q, T->Left);
        if(T->Right) AddQ(Q, T->Right);
    }
}

【例】遍歷二叉樹的應用:輸出二叉樹中的葉子結點

  • 在二叉樹的遍歷算法中增加檢測結點的“左右子樹是否都爲空”。
void PreOrderPrintLeaf(BinTree BT)
{
    if (BT)
    {
        if(!BT->Left && !BT->Right)
            printf("%d", BT->Data);
        PreOrderPrintLeaf(BT->Left);
        PreOrderPrintLeaf(BT->Right);
    }
}

中序遍歷和後序遍歷也類似,在printf前加入判斷語句即可。


【例】求二叉樹的高度。

(利用後序遍歷來實現)

int PostOrderGetHeight(BinTree BT)
{
    int HL, HR, MaxH;
    if(BT)
    {
        HL = PostOrderGetHeight(BT->Left);   /*求左子樹的深度*/
        HR = PostOrderGetHeight(BT->Right);  /*求右子樹的深度*/
        MaxH = (HL > HR)? HL : HR;      /*取左右子樹較大的深度*/
        return (MaxH + 1);   /*返回樹的深度*/
    }
    else 
        return 0;
}

【例】二元運算表達式樹及其遍歷

葉結點代表運算數,非葉結點是運算符號。

※三種遍歷可以得到三種不同的訪問結果:

  • 先序遍歷得到前綴表達式:++a*bc*+*defg
  • 中序遍歷得到中綴表達式:a+b*c+d*e+f*g        ----->!!!!中綴表達式會受到運算符優先級的影響!!!!如果給定一個表達式樹,要求輸出正確的中綴表達示,可通過加括號的方式解決該問題(輸出左子樹的時候加左括號,左子樹輸出完畢加右括號)
  • 後序遍歷得到後序表達式:abc*+de*f+g*+

【例】由兩種遍歷序列確定二叉樹

已知三種遍歷中的任意兩種遍歷序列,能夠唯一確定一個二叉樹呢?

答案是:必須要有中序遍歷才行!

沒有中序的困擾:

  • 先序遍歷序列:A  B
  • 後序遍歷序列:B  A

這樣確定的二叉樹不是唯一的。比如:  兩棵樹都是滿足條件的。

因爲先序是:根、左、右;後序是:左、右、根。難區分左和右分別是哪些,左、右的邊界在哪裏也不知道。

※  先序和中序遍歷序列來確定一棵二叉樹

【分析】

  • 根據先序遍歷序列第一個結點確定根結點
  • 根據根結點在中序遍歷序列分割出兩個子序列
  • 左子樹和右子樹分別遞歸使用相同的方法繼續分解。

中序遍歷序列中根據根結點就找到左子樹的結點個數,在先序序列中就可以從根結點往後數,得到左子樹的邊界。再根據先序序列中左子樹的第一個結點,得到左子樹的根結點,在中序序列中就得到了左子樹的根結點,從而可以得到左子樹的左子樹和右子樹。依此類推。

【例】先序序列:a     b c d e      f  g h i j

           中序序列:   c b e d   a  h g  i  j  f

 ==>根據先序序列可知,a是整棵樹的根,然後到中序序列中找到a,所以可以知道左子樹爲cbde,到先序序列中從根結點往後數4個,可以知道是bcde。所以知道了左子樹的先序序列和中序序列。同理右子樹也相同。

※ 類似地,後序和中序遍歷序列也可以確定一棵二叉樹。

5. 小白專場:樹的同構

5.1 題目

03-樹1 樹的同構 (25 分)

給定兩棵樹T1和T2。如果T1可以通過若干次左右孩子互換就變成T2,則我們稱兩棵樹是“同構”的。例如圖1給出的兩棵樹就是同構的,因爲我們把其中一棵樹的結點A、B、G的左右孩子互換後,就得到另外一棵樹。而圖2就不是同構的。

                                          圖1

                                          圖2

現給定兩棵樹,請你判斷它們是否是同構的。

輸入格式:

輸入給出2棵二叉樹樹的信息。對於每棵樹,首先在一行中給出一個非負整數N (≤10),即該樹的結點數(此時假設結點從0到N−1編號);隨後N行,第i行對應編號第i個結點,給出該結點中存儲的1個英文大寫字母、其左孩子結點的編號、右孩子結點的編號。如果孩子結點爲空,則在相應位置上給出“-”。給出的數據間用一個空格分隔。注意:題目保證每個結點中存儲的字母是不同的。

輸出格式:

如果兩棵樹是同構的,輸出“Yes”,否則輸出“No”。

輸入樣例1(對應圖1):

8
A 1 2
B 3 4
C 5 -
D - -
E 6 -
G 7 -
F - -
H - -
8
G - 4
B 7 6
F - -
A 5 1
H - -
C 0 -
D - -
E 2 -

輸出樣例1:

Yes

輸入樣例2(對應圖2):

8
B 5 7
F - -
A 0 3
C 6 -
H - -
D - -
G 4 -
E 1 -
8
D 6 -
B 5 -
E - -
H - -
C 0 2
G - 3
F - -
A 1 4

輸出樣例2:

No

限制:

時間限制: 400 ms

內存限制: 64 MB

代碼長度限制: 16 KB

5.2 題意理解

給定兩棵樹T1和T2。如果T1可以通過若干次左右孩子互換就變成T2,那我們稱兩棵樹是“同構”的。現給定兩棵樹,請你判斷它們是否是同構的。

輸入格式:輸入給出2棵二叉樹的信息:

  • 先在一行中給出該樹的結點數,隨後N行
  • 第i行對應編號第i個結點,給出該結點中存儲的字母、其左孩子結點的編號、右孩子結點的編號。
  • 如果孩子結點爲空,則在相應位置上給出“-”。

###輸入樣例:

8  (第一棵樹)
A 1 2
B 3 4
C 5 -
D - -
E 6 -                              
G 7 -
F - -
H - -

=======>輸入數據每一行對應一個結點,編號依次是:對應的二叉樹爲:
8  (第二棵樹)
G - 4
B 7 6
F - -
A 5 1
H - -
C 0 -
D - -
E 2 -

=======>同理,輸入數據每一行的編號依次:,對應的二叉樹爲:

可見,不要求根結點作爲第一個結點輸入。

5.3 求解思路

  1. 二叉樹表示
  2. 建二叉樹
  3. 通過判別

5.3.1 二叉樹表示

(1)最常見的表示方法(鏈表):

(2)用數組表示(補全成完全二叉樹):

(3)用結構數組表示二叉樹:靜態鏈表  (物理上的存儲是數組,思想上是鏈表的思想)

每一列是數組的一個分量,包含了三個信息:結點本身的信息保存的字母,Left和Right指向左右兒子的位置的下標。用-1表示指向空的結點。

數據結構定義:

#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1    //爲了區分關鍵字NULL(0),自定義的代表的是-1

struct TreeNode
{
    ElementType Element;
    Tree Left;
    Tree Right;
}T1[MaxTree], T2[MaxTree];

Left和Right是下標,不是指針,所以沒有左右孩子時,Left和Right都爲-1,而不是NULL。

數組中ABCD的順序不一定,可以隨意變換。如上面的那棵樹,還可以表示成:

同樣一棵樹在結構數組中的靜態鏈表表示方法不唯一,這就是靈活性。

如何通過靜態鏈表確定根結點呢?

上面的四個結點分別放在0、1、3、4下標對應的位置上,哪些在結構體數組中出現,哪個沒出現。B的左右孩子時4和3下標對應的結點,A的右孩子是0對應的結點,也就是0、3和4被用到了,只有1沒有被用到。所以1對應的結點就是根結點。

5.3.2 程序框架搭建

                               

int main()
{
    Tree R1,R2;
    
    R1 = BuildTree(T1);  //T1和T2是此前定義的結構數組,全局變量
    R2 = BuildTree(T2);
    if(Isomorphic(R1,R2))
        printf("Yes\n");
    else 
        printf("No\n");
    return 0;
}

5.3.3 如何建二叉樹

按照題目意思以及輸入樣例:

8
A 1 2
B 3 4
C 5 -
D - -
E 6 -                              
G 7 -
F - -
H - -

先輸入結點的個數,然後依次輸入結點存儲的字母,結點的左右孩子結點的編號,所以代碼如下:

Tree BuildTree(struct TreeNode T[])
{
    ...
    scanf("%d\n", &N);  //輸入結點的個數
    if(N)
    {
        ......
        for(i = 0; i < N; i++)
        {
            scanf("%c %c %c\n", &T[i].Element, &cl, &cr);  //將左右孩子編號以字符形式輸入,之後再處理成整型
            ......
        }
        ......
        
        Root = ???  //如何確定根結點是哪個?T[i]中沒有任何結點的left(cl)和right(cr)指向它。只有一個。
    }
    return Root;
}

BuildTree函數的目的是創建一棵樹,返回樹的根結點。那麼這個根結點是什麼呢?可以按照之前說的,掃描一遍這個結構數組,看哪個下標對應的結點沒有任何結點指向它。

Tree BuildTree(struct TreeNode T[])
{
    ...
    scanf("%d\n", &N);  //輸入結點的個數
    if(N)
    {
        for(i = 0; i < N; i++) 
            check[i] = 0;  //數組check對應於n個結點
        for(i = 0; i < N; i++)
        {
            scanf("%c %c %c\n", &T[i].Element, &cl, &cr);  //將左右孩子編號以字符形式輸入,之後再處理成整型
            if(cl != "-")   //左兒子不爲空
            {
                T[i].Left = cl-'0';
                check[T[i].Left] = 1; //如果某個結點的left指向了某個位置,就將該位置的check設置爲1.
            }
            else
                T[i].Left = Null;
            if(cr != '-')  //右兒子對應的編號
            {
                T[i].Right = cr-'0';
                check[T[i].Right] = 1;
            }
            else
                T[i].Right = Null;
        }
        //循環結束後,check數組中對應的值還是爲0的就是根結點
        for(i = 0; i < N; i++)
            if(!check[i]) break;   
        Root = i  
    }
    return Root;
}

5.3.4 如何判別兩二叉樹同構

int Isomorphic(Tree R1, Tree R2)
{
    if(R1 == Null) && (R2 == Null)   //兩棵樹都是空的
        return 1;
    if((R1 == Null) && (R2 != Null)) || ((R1 != Null) && (R2 == Null))) //其中一棵樹爲空,另一棵樹不爲空
        return 0;    
    if(T1[R1].Element != T2[R2].Element)  //根結點不同
        return 0;      
    if((T1[R1].Left == Null) && (T2[R2].Left == Null))   //都沒有左孩子
        return Isomorphic(T1[R1].Left, T2[R2].Left);
    if(((T1[R1].Left != Null) && (T2[R2].Left != Null)) 
        && (T1[T1[R1].Left].Element == T2[T2[R2].Left].Element))  //如果左孩子同時不爲空,且Element都相同
        return (Isomorphic(T1[R1].Left, T2[R2].Left)  && Isomorphic(T1[R1].Right, T2[R2].Right));  //判斷左邊同構,右邊是否同構
    else 
        //這個else包含的情況:
        //1、兩棵根結點的左子樹的Element不同,則判斷左邊和右邊同構,右邊和左邊同構。
        //2、一棵樹的左子樹爲空,另一棵樹的右子樹爲空,也要這樣判斷
        return (Isomorphic(T1[R1].Left, T2[R2].Right) && Isomorphic(T1[R1].Right, T2[R2].Left));
}

5.3.5  完整代碼

#include <stdio.h>
#include <stdlib.h>

#define MaxTree 10
#define ElementType char
#define Tree int

struct TreeNode
{
    ElementType element;
    Tree left;
    Tree right;
}T1[MaxTree], T2[MaxTree];

Tree buildTree(struct TreeNode T[]);
int isomorphic(Tree t1, Tree t2);

int  main()
{
    Tree r1,r2;
    r1 = buildTree(T1);
    r2 = buildTree(T2);
    if (isomorphic(r1, r2))
        printf("Yes\n");
    else
        printf("No\n");
    return 0;
}

Tree buildTree(struct TreeNode T[])
{
    int n;
    scanf("%d\n", &n);
    Tree root = -1;
    if(n) {
        Tree check[MaxTree];
        int i;
        char cl,cr;
        for(i = 0; i < n; i++)
            check[i] = 0;
        for(i = 0; i < n; i++) {
            scanf("%c %c %c\n", &T[i].element, &cl, &cr);
            if(cl != '-') {
                T[i].left = cl - '0';
                check[T[i].left] = 1;
            } else {
                T[i].left = -1;
            }
            if(cr != '-') {
                T[i].right = cr - '0';
                check[T[i].right] = 1;
            } else {
                T[i].right = -1;
            }
        }
        for (i = 0; i < n; i++){
            if(!check[i])
                break;
        }
        root = i;
    }
    return root;
}

int isomorphic(Tree r1, Tree r2)
{
    if(r1 == -1 && r2 == -1)
        return 1;
    if((r1 == -1 && r2 != -1) || (r1 != -1 && r2 == -1))
        return 0;
    if(T1[r1].element != T2[r2].element)
        return 0;
    if(T1[r1].left == -1 && T2[r2].left == -1)
        return isomorphic(T1[r1].right, T2[r2].right);
    if((T1[r1].left != -1) && (T2[r2].left != -1)
       && T1[T1[r1].left].element == T2[T2[r2].left].element)
        return isomorphic(T1[r1].left, T2[r2].left) && isomorphic(T1[r1].right, T2[r2].right);
    else
        return isomorphic(T1[r1].left, T2[r2].right) && isomorphic(T1[r1].right, T2[r2].left);

}

ctrl+z 結束輸入。

運行結果:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章