樹
樹的定義
樹是\(N(N>0)\)個結點的有限集合,\(N=0\)時,稱為空樹,這是一種特殊情況,在任意一顆非空樹中應滿足:
- 有且僅有一個特定的稱為根的結點
- 當\(N>1\)時,其余結點可分為\(m(m>0)\)個互不相交的有限集合\(T_1,T_2,……,T_m\),其中每個集合本身又是一棵樹,並且稱為根結點的子樹
通俗理解總結:
- 有且僅有一個根結點
- 根結點沒有前驅結點,其他結點有且只有一個前驅結點
- 所有結點可以有零個或者多個后繼結點
- 樹是一種遞歸的數據結構,適合於表示所有層次結構的數據
樹的基本術語
-
結點關系
- 祖先結點:根結點到該結點的唯一路徑的任意結點
- 子孫結點
- 雙親結點:根結點到該結點的唯一路徑上最接近該結點的結點
- 孩子結點
- 兄弟結點:具有相同雙親結點的結點
-
樹的度:樹中所有結點的度數的最大值
-
結點的層次、深度、高度
- 層次:根為第一層、它的孩子為第二層,以此類推
- 深度:根結點開始自頂向下累加
- 高度:葉子結點開始自底向上累加
-
樹的高度(深度)、路徑、路徑長度
-
樹的高度(深度):樹中結點的最大層數
-
路徑:又兩個結點之間所經過的結點序列構成的
-
路徑長度:路徑上所經過的邊的個數
由於樹種的分支是有向的,即從雙親指向孩子,所以數中的路徑是自上而下的,同一雙親的兩個孩子之間不存在路徑
-
樹的性質
- 樹中的結點數等於所有結點的度數加1。
證明:
不難想象,除根結點以外,每個結點有且僅有一個指向它的前驅結點。也就是說每個結點和指向它的分支一一對應。
假設樹中一共有\(b\)個分支,那么除了根結點,整個樹就包含有\(b\)個結點,所以整個樹的結點數就是這\(b\)個結點加上根結點,設為\(n\),則\(n=b+1\)。而分支數\(b\)也就是所有結點的度數,證畢。
- 度為\(m\)的樹中第\(i\)層上至多結點樹如下
證明:(數學歸納法)
首先考慮\(i=1\)的情況:第一層只有根結點,即一個結點,\(i=1\)帶入式子滿足。
假設第\(i-1\)層滿足這個性質,第\(i-1\)層最多有\(m^{i-2}\)個結點,又因為樹的度為\(m\),所以對於第\(i-1\)層的每個結點,最多有\(m\)個孩子結點。所以第\(i\)層的結點數最多是\(i-1\)層的\(m\)倍,所以第\(i\)層上最多有\(m ^{i-1}\)個結點。
- 高度為\(h\)的\(m\)叉樹至多的結點數如下
- 具有\(n\)個結點的\(m\)叉樹的最小高度如下
- 樹結點與度之間的關系有
樹的存儲結構
順序存儲結構
雙親表示法:用一組連續的存儲空間存儲樹的結點,同時在每個結點中,用一個變量存儲該結點的雙親結點在數組中的位置。
如圖所示:
代碼如下:
typedef char ElemType;
typedef struct TNode{
ElemType data; //結點數據
int parent; //該結點雙親在數組中的下標
}Tnode; //結點數據類型
#define MaxSize 100
typedef struct{
TNode nodes[MaxSize]; //結點數組
int n; //結點數量
}Tree; //樹的雙親表示結構
優點:可以很快得到每個結點的雙親結點
缺點:求結點的孩子需要遍歷整個結構
鏈式存儲結構
孩子表示法:
把每個結點的孩子結點排列起來存儲成一個單鏈表。所以\(n\)個結點就有\(n\)個鏈表;
如果是葉子結點,那這個結點的孩子單鏈表就是空的;
然后\(n\)個單鏈表的的頭指針又存儲在一個順序表(數組)中。
如圖所示:
代碼如下:
typedef char ElemType;
typedef struct CNode{
int child; //該孩子在表頭數組的下標
struct CNode *next; //指向該結點的下一個孩子結點
}CNode,*Child; //孩子結點的數據類型
typedef struct{
ElemType data; //結點數據域
Child firstchild; //指向該結點的第一個孩子結點
}TNode; //孩子結點的數據類型
優點:尋找子女非常直接
缺點:尋找雙親需要便利\(N\)個結點的孩子鏈表指針域所只想的\(N\)個孩子鏈表
孩子兄弟表示法:
孩子兄弟表示法:顧名思義就是要存儲孩子和孩子結點的兄弟,具體來說,就是設置兩個指針,分別指向該結點的第一個孩子結點和這個孩子結點的右兄弟結點。
如圖所示:
代碼如下:
typedef char ElemType;
typedef struct CSNode{
ElemType data; //結點數據域
struct CSNode *firstchild,*rightsib; //指向該結點的第一個孩子結點和該結點的右兄弟結點
}CSNode; //孩子兄弟點的數據類型
優點:方便實現轉換為二叉樹,易於查找結點的孩子
缺點:從當前結點查找其雙親結點比較麻煩
二叉樹
二叉樹的定義
二叉樹是\(n(n≥0)\)個結點的有限集合:
- 或者為空二叉樹,即 \(n=0\)
- 或者由一個根結點和兩個互不相交的被稱為根的左子樹和右子樹組成。左子樹和右子樹又分別是一棵二叉樹。
通俗理解:
每個結點至多有兩顆子樹,左右子樹的順序不能顛倒,二叉樹與度為2的有序樹不同,不同的原因是度為2的樹要求每個結點最多只能有兩棵子樹,並且至少有一個結點有兩棵子樹。二叉樹的要求是度不超過2,結點最多有兩個叉,可以是1或者0。
二叉樹的五種基本形態
- 空樹
- 只有一個根結點
- 根結點只有左子樹
- 根結點只有右子樹
- 根結點既有左子樹又有右子樹
擁有特殊形態的二叉樹
- 斜樹:每個結點只有左結點或者每個結點只有右結點
- 滿二叉樹:樹種每一層都含有最多的結點,對於編號\(i\)的結點,期雙親結點為\(\lfloor i/2\rfloor\)
- 完全二叉樹:每一個結點都與高度為h的滿二叉樹編號\(1-n\)相同;如果\(i≤n/2\)下,則結點\(i\)為分支結點,否則為葉子結點
- 二叉排序樹:左子樹均小於根結點,右子樹均大於根結點
- 平衡二叉樹:左右子樹的深度之差不超過1
二叉樹的性質
- 非空二叉樹上的葉子結點數等於度為2的結點數加一,即 \(n_0=n_2+1\)
- 非空二叉樹上第\(k\)層上至多有\(2^{k-1}\)個結點\((k≥1)\)
- 高度為\(h\)的二叉樹至多有\(2^k - 1\)個結點\((h≥1)\)
- 具有\(n\)個\((n>0)\)結點的完全二叉樹的高度為\(\lceil log_2{n+1}\rceil\)或\(\lfloor log_2n\rfloor+1\)
二叉樹的存儲結構
順序存儲結構
二叉樹的順序存儲結構就是用一組地址連續的存儲單元依次自上而下、自左至右存儲完全二叉樹上的結點元素。
如圖所示:
優點:適合完全二叉樹和滿二叉樹,序號可以反映出結點之間的邏輯關系,可以節省空間
缺點:適合一般二叉樹,只能添加一些空結點,空間利用率低
鏈式存儲結構
二叉樹每個結點最多兩個孩子,所以設計二叉樹的結點結構時考慮兩個指針指向該結點的兩個孩子。
如圖所示:
代碼如下:
typedef char ElemType;
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild,*rchild;
}
二叉樹的遍歷
先序遍歷(\(NLR\))
過程:
- 訪問根結點
- 先序遍歷左子樹
- 先序遍歷右子樹
代碼如下:
遞歸代碼如下:
void PreOrder(BiTree T)
{
//先序遍歷算法
if(T!=NULL){
vist(T); //訪問根結點,如:printf("%c",T->data);
PreOrder(T->lchild); //遞歸遍歷左子樹
PreOrder(T->rchild); ////遞歸遍歷右子樹
}
}
非遞歸代碼如下
void Preorder2(BiTree T){
//先序遍歷非遞歸算法
InitStack(S); //需要借助一個遞歸棧
BiTree p=T; //p是遍歷指針
while(p||!IsEmpty(S)){ //棧不空或p不空時循環
if(p){ //一路向左
visit(p); //訪問當前結點
Push(S,p); //入棧
p=p->lchild; //左孩子不空,一直向左走
}
else{ //出棧,並轉向出棧結點的右子樹,可改成if(!IsEmpty(S))
Pop(S,p); //棧頂元素出棧
p=p->rchild; //向右子樹走,p賦值為當前結點的右孩子
} // 返回while循環繼續進入if-else語句
}
}
中序遍歷(\(LNR\))
- 中序遍歷左子樹
- 訪問根結點
- 中序遍歷右子樹
遞歸代碼如下:
void InOrder(BiTree T)
{
//先序遍歷算法
if(T!=NULL){
InOrder(T->lchild); //遞歸遍歷左子樹
vist(T); //訪問根結點,如:printf("%c",T->data);
InOrder(T->rchild); ////遞歸遍歷右子樹
}
}
非遞歸代碼如下:
void Inorder2(BiTree T){
//中序遍歷非遞歸算法
InitStack(S);//需要借助一個遞歸棧
BiTree p=T;
while(p||!IsEmpty(S)){ //棧不空或者P不空時循環
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
visit(p);
p=p->rchild;
}
}
}
后序遍歷(\(LRN\))
- 后序遍歷左子樹
- 后序遍歷右子樹
- 訪問根結點
遞歸代碼如下:
void PostOrder(BiTree T)
{
//先序遍歷算法
if(T!=NULL){
PostOrder(T->lchild); //遞歸遍歷左子樹
PostOrder(T->rchild); ////遞歸遍歷右子樹
vist(T); //訪問根結點,如:printf("%c",T->data);
}
}
非遞歸代碼如下(重難點!!!):
void PostOrder(BiTree T)){
InitStack(S);
BiTree p=T; //工作指針
r=NULL;//指向最近訪問過的結點,輔助指針
while(p||!IsEmpty(S)){
if(p){
//1、從根結點到最左下角的左子樹都入棧
Push(S,p);
p=p->lchild;
}
else{ //返回棧頂的兩種情況
GetTop(S,P);//彈出棧頂元素
if(p->rchild&&p->rchild!=r){
//1、右子樹存在且未訪問過,
p=p->rchild;//轉右
push(S,p); //壓入棧
p=p->lchild;//走到最左
}
else{
//2、右子樹已經訪問或空,接下來出棧訪問結點
pop(S,p); //將結點彈出
visit(p->data); //訪問該結點
r=p; //指針訪問過的右子樹根結點
p=NULL;//訪問完之后就重置P,每次從棧中彈出一個,防止進入第一個if
}
}
}
}
難點:要保證左孩子和右孩子都已被訪問並且左孩子在右孩子前訪問才能訪問根結點