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),平均時間複雜度爲 (最好情況是第一個就是,最壞情況最後一個纔是)。
方法2:二分查找(Binary Search)
假設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,即,所以結果就是x = logN。
二分查找算法具有對數的時間複雜度O(logN)
【※】11個元素的二分查找判定樹
從下標爲1的地方開始放元素,放到下標爲11的地方。二分查找某個元素的過程一定是按照這樣的層次結構來的:
- 判斷樹上每個結點需要的查找次數剛好爲該結點所在的層數; (比如位於4號位置,則比較3次)
- 查找成功時查找次數不會超過判定樹的深度
- n個結點的判斷樹的深度爲
- ASL = (4*4 + 4*3 + 2*2 + 1)/ 11 = 3 (平均成功查找次數)
2. 樹的定義
樹(Tree):n ( n≥0)個結點構成的有限集合。
當n = 0時,稱爲空樹;
對於任一棵非空樹(n>0),它具備以下性質:
- 樹中有一個稱爲“根(Root)”的特殊結點,用表示;
- 其餘結點可分爲m(m>0)個互不相交的有限集其中每個集合本身又是一棵樹,稱爲原來樹的“子樹(SubTree)”
上圖樹T的根就是A,由如下四個子樹構成:
2.1 ※樹與非樹?
(多了C-D的連線,無法切分爲不相交的集合)
(多了C-E的連線)
(多了D-G的連線)
- 子樹是不相交的;
- 除了根結點外,每個結點有且僅有一個父結點;
- 一棵N個結點的樹有N-1條邊。 (每個結點都有向上的一根連接父結點的線,除了根結點,所以是N-1)
樹是保證結點連通的最小的連接方式(即邊最少)。
2.2 ※樹的一些基本術語
- 結點的度(Degree):結點的子樹個數 --如上圖:結點A的度爲3,B爲2,C爲1,D爲3,F爲0,...
- 樹的度:樹的所有結點中最大的度數 --如上圖:A和D的度都爲3,所以樹的度爲3
- 葉結點(Leaf):度爲0的結點
- 父結點(Parent):有子樹的結點是其子樹的根結點的父結點
- 子結點(Child):若A結點是B結點的父結點,則稱B結點是A結點的子結點;子結點也稱孩子結點。
- 兄弟結點(Sibling):具有同一父結點的各結點彼此是兄弟結點。
- 路徑和路徑長度:從結點到的路徑爲一個結點序列是的父結點。路徑所包含邊的個數爲路徑的長度。
- 祖先結點(Ancestor):沿樹根到某一結點路徑上的所有結點都是這個結點的祖先結點。
- 子孫結點(Descendant):某一結點的子樹中的所有結點是這個結點的子孫。
- 結點的層次(Level):規定根結點在1層,其他任一結點的層數是其父結點的層數加1。
- 樹的深度(Depth):樹中所有結點中的最大層次是這棵樹的深度。
3. 樹的表示
如圖的一棵樹,能否用數組實現?
==>用數組實現:就是把這些結點按順序用數組存起來,難度大,因爲難以分清結點的父結點和子孫結點等。
用鏈表實現:
==>每個結點用個結構來表示
存在的問題:每個結點的結構不同,不知道結點有幾個子孫結點,爲程序實現帶來了難度。
另一種思路:每個結點的結構設計爲相同的,如都同A一樣,設計爲3個指針域,那麼假設有n個結點,就一共需要3n-1個指針域,但是n個結點實際上就只有n-1個指針域不爲空。這種思路也不行。
下面介紹一種方法:
3.1 兒子-兄弟表示法
樹上的結點結構統一。
FirstChild指針指向第一個兒子,NextSibiling指向下一個兄弟結點。
例如:
將這個表示方法旋轉45°
旋轉45°後的這棵樹每個結點都有兩個指針,每個結點最多兩個兒子,這種樹叫做二叉樹。
4. 二叉樹
4.1 二叉樹的定義
二叉樹T:一個有窮的結點集合。
這個集合可以爲空
若不爲空,則它是由根結點和稱爲其左子樹和右子樹的兩個不相交的二叉樹組成。
□二叉樹具體五種基本形態
□ 二叉樹的子樹有左右順序之分
(這也是與度爲2的樹的區別)
4.2 特殊二叉樹
- 斜二叉樹(Skewed Binary Tree)
- 完美二叉樹(Perfect Binary Tree) / 滿二叉樹(Full Binary Tree)
- 完全二叉樹(Complete Binary Tree)
有n個結點的二叉樹,對樹中結點按從上至下、從左到右順序進行編號,編號爲( 1 ≤ ≤ n)結點與滿二叉樹中編號爲結點在二叉樹中位置相同
(這是一棵完全二叉樹)
所以,不是完全二叉樹。
4.3 二叉樹的幾個重要性質
- 一個二叉樹第 i 層的最大結點數爲
- 深度爲k的二叉樹有最大結點總數爲: ()---完美二叉樹可以達到
- 對任何非空二叉樹T,若表示葉結點的個數、是度爲2的非葉結點個數,那麼兩者滿足關係
(表示只有一個兒子的結點)
該結論的證明:
結點總個數:
總的邊數:(每個結點有向上的邊一條除了根結點) (不同類型結點向下的邊的條數)
4.4 二叉樹的抽象數據類型定義
類型名稱:二叉樹
數據對象集:一個有窮的結點集合 若不爲空,則由根結點和其左、右二叉子樹組成。
操作集:BT∈BinTree,Item∈ElementType,重要操作有:
|
常用的遍歷方法有:
- void PreOrderTraversal(BinTree BT): 先序----根、左子樹、右子樹
- void InOrderTraversal(BinTree BT): 中序---左子樹、根、右子樹
- void PostOrderTraversal(BinTree BT):後序---左子樹、右子樹、根
- void LevelOrderTraversal(BinTree BT):層次遍歷,從上到下,從左到右
4.4 二叉樹的存儲結構
1. 順序存儲結構
完全二叉樹:按從上至下、從左到右順序存儲n個結點的完全二叉樹的結點父子關係:
- 非根結點(序號i > 1)的父結點的序號是; ------如C結點,它的父結點是4/2 = 2 ,即B;如S結點,它的父結點5/2 = 2,即B
- 結點(序號爲)的左孩子結點的序號是,(若,否則沒有左孩子); -----如S結點,5*2 = 10大於9,就不存在
- 結點(序號爲)的右孩子結點的序號是,(若,否則沒有右孩子);
一般二叉樹也可以採用這種結構,但會造成空間浪費......
補全爲完全二叉樹--->
補全爲完全二叉樹後可以用數組存儲,但是有很多都是空的,會造成空間浪費。
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 求解思路
- 二叉樹表示
- 建二叉樹
- 通過判別
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 結束輸入。
運行結果: