[top]
樹和二叉樹
樹:是\(N(N\geq0)\)個結點的有限集合,\(N=0\)時,稱為空樹,這是一種特殊情況。在任意一棵非空樹中應滿足:
- 有且僅有一個特定的稱為根的結點。
- 當\(N>1\)時,其余結點可分為\(m(m>0)\)個互不相交的有限集合\(T_1,T_2,\ldots,T_m\),其中每一個集合本身又是一棵樹,並且稱為根結點的子樹。
顯然樹的定義是遞歸的,是一種遞歸的數據結構。樹作為一種邏輯結構,同時也是一種分層結構,具有以下兩個特點:
- 樹的根結點沒有前驅結點,除根結點之外的所有結點有且僅有一個前驅結點。
- 樹中所有結點可以有零個或者多個后繼結點。
樹適合於表示具有層次結構的數據。樹中的某個結點(除了根結點之外)最多之和上一層的一個結點(其父結點)有直接關系,根結點沒有直接上層結點,因此在n個結點的樹中最多只有n-1條邊。而樹中每個結點與其下一層的零個或者多個結點(即其子女結點)有直接關系。
>樹具有如下最基本的性質。
1. 樹中結點數等於所有節點的度數+1.
2. 度為m的樹中第i層上之多有$m^{i-1}$個結點$(i\geq1)$。
3. 高度為h的m叉樹至多有$\frac{m^h-1}{m-1}$個結點。
4. 具有n個結點的m叉樹的最小高度為$\log_m(n(m-1)+1)$。
二叉樹的概念
二叉樹和度為2的有序樹的區別:
- 度為2的樹至少有3個結點,而二叉樹則可以為空;
- 度為2的有序樹的孩子結點的左右次序是相對於另一個孩子結點而言的,如果某個結點只有一個孩子結點,這個孩子結點就無需區別其左右次序,但是二叉樹無論孩子數是否為2,均需要確定其左右次序,也就是說二叉樹結點次數不是相對於另一個結點而言,而是確定的。
單解釋一下完全二叉樹:設一個高度為h,有n個結點的二叉樹,當且僅當其每一個結點都與高度為h的滿二叉樹中編號一一對應是稱為完全二叉樹。
二叉樹的遍歷
先序遍歷
void PreOrder(BitTree T)
{
if(T!=NULL)
{
printf("%d\n",T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍歷
void InOrder(BitTree T)
{
if(T!=NULL)
{
InOrder(T->lchild);
printf("%d\n",T->data);
InOrder(T->rchild)
}
}
后序遍歷
void PostOrder(BitTree T)
{
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
printf("%d\n",T->data);
}
}
三種遍歷算法中遞歸遍歷左子樹和右子樹的順序都是固定的,只是訪問根節點的順序不同。不管采用何種遍歷方法,每個結點都是訪問一次,所以時間復雜度就是\(O(n)\)。
在遞歸遍歷中,遞歸工作棧的深度恰巧是樹的深度,所以在最壞的情況下,二叉樹是有n個結點且深度為n的單支樹,遞歸遍歷算法的時間復雜度是\(O(n)\)。
@[中序遍歷的非遞歸算法如下]
typedef struct BiTNode
{
int data;
struct BiTNode *lchild,*rchild;
}*BitTree;
typedef struct
{
char data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S)
{
S.top=-1;
}
void InOrder2(BitTree T)
{
InitStack(S);
BitTree p=T;
while(p||IsEmpty(s))
{
if(p)
{
Push(S,p);
p=p->lchild;
}
else
{
Pop(s,p);
printf("%d\n",p->data);
p=p->rchild;
}
}
}
線索二叉樹
遍歷二叉樹就是以一定的規則將二叉樹中的結點排列為一個線性序列,從而得到二叉樹中結點的各種遍歷序列。其實質就是對一個非線性結構進行線性化操作,使在這個訪問序列中的每一個結點(除了最后一個和第一個)都有一個直接前驅結點或者后繼結點。
傳統的鏈式儲存能夠體現出一種父子關系,不能直接得到結點在遍歷中的前驅或者后繼。通過觀察,我們發現在二叉鏈表表示的二叉樹中存在大量的空指針,若是利用這些空鏈域存放指向其直接的前驅或者后繼的指針,則可以更加方便的運用某些二叉樹的操作算法。引入線索二叉樹是為了加快查找節點的前驅和后繼的速度。
前面提到,在N個節點的二叉樹中,有N+1個空指針。這是因為每個葉節點都有兩個空指針,而每一個度為1的節點有一個空指針。總的空指針數目為\(2N_0+N_1\),又有\(N_0=N_2+1\)。意思是二倍的葉子節點加上1被的一個孩子的節點的數目。
線索二叉樹的構造。
線索二叉樹的存儲結構描述如下:
typedef struct ThreadNode
{
int data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
\(ltag=0\)表示lchild指向的是結點的左孩子
\(ltag=1\)表示lchild指向的是結點的前驅
\(rtag=0\)表示rchild指向的是結點的右孩子
\(rtag=1\)表示rchild指向的是結點的后繼
這種結點結構構成的二叉鏈表作為二叉樹的存儲結構,叫做線索鏈表,其中指向結點前驅和后繼的指針,稱為線索。加上線索的二叉樹稱為線索二叉樹。對二叉樹進行以某種次序遍歷使其變為線索二叉樹的過程叫做線索化。
@[線索化二叉樹的構造]
對二叉樹的線索化,實質上就是遍歷一次二叉樹,只是在遍歷的過程中檢查當前節點的左右指針是否為空,若為空,將他們改為指向前驅節點或者后繼節點的線索。
@[P109]
度為2的有序樹不是是二叉樹:二叉樹中如果某個節點只有一個孩子節點,那么這個孩子節點的左右次數是確定的,但是在有序樹中如果某個節點只有一個孩子節點則這個節點無需區分其左右次序,所以度為2的樹不是二叉樹。
完全二叉樹的節點數目和高度的關系是\([\log_2N]+1\)。
完全二叉樹的節點排列是從左到右從上到下,所以如果一個節點沒有左孩子,則它必定是葉節點。
二叉排序樹 后面補上。
設層次遍歷的結果為A,B,C。
則先序遍歷的結果為A,B,C。
則中序遍歷的結果為B,A,C。
則后續遍歷的結果為B,C,A。
二叉樹的中序遍歷的最后一個節點一定是從根開始沿右子女指針鏈走到最低的結點。
樹的存儲結構
> 雙親表示法
這種存儲方式采用一組連續空間來存儲每個節點,同時在每個節點中增設一個尾指針指示雙親結點在數組中的位置。根節點下標為0,其偽指針域為-1.
並查集
並查集是一種簡單的集合表示,它支持一下三種操作:
- $Union(S,root1,root2); $把集合S中的子集合Root2,並入Root1中。要求Root1和Root2互不相交,否則不執行合並。
- \(Find(S,x);\)查找集合S中單元素x所在的子集合,並返回該子集合的名字。
- \(Initial(S);\)將集合S中每一個元素都初始化為只有一個單元素的子集合。
通常用樹(森林)的雙琴表示作為並查集的存儲結構,每個子集合以一棵樹表示。所有表示子集合的樹,構成表示全集合的森林,存放在雙親表示數組中。
並查集的結構定義如下:
#define Size 100
int UFSets[Size];
void Initial(int S[])
{
for(int i=0;i<Size;i++)
S[i]=-1;
}
int Find(int S[],int x)
{
while(S[x]>=0)
x=S[x];
return x;
}
void Union(int S[],int Root1,int Root1)
{
S[Root1]=Root2;
}
森林
由於二叉樹和樹都可以用二叉鏈表作為存儲結構,則以二叉鏈表作為媒介可以導出樹與二叉樹的一個對應關系,即給定一棵樹,可以找出唯一的一棵二叉樹與之對應。從物理結構上看,樹的孩子兄弟表示法和二叉樹的二叉鏈表表示法相同,即每個節點共有兩個指針,分別指向結點的第一個孩子節點和結點的下一個兄弟節點,而二叉鏈表可以使用雙指針。因此,就可以用同意存儲結構的不同解釋將一棵樹轉換為二叉樹。
樹轉換為二叉樹的規則:每個節點的左指針指向她的第一個孩子節點,右指針指向它在書中的相鄰兄弟結點,可表示為左孩子有兄弟。由於根節點沒有兄弟,所以由樹轉換而得的二叉樹沒有右子樹。
二叉排序樹的查找
BSTNode *BST_Search(BitTree T,int key,BSTNode *&p)
{ // 返回指向關鍵字為key的結點指針,若不存在則返回NULL
p=NULL; // p指向被查找結點的雙親,用於插入和刪除操作中。
while(T!=NULL&&key!=T->data)
{
p=T;
if(key<T->data)
T=T->lchild;
else
T=T->rchild;
}
return T;
}
二叉排序樹的插入
int BST_Insert(BitTree &T,int k)
{
if(T==NULL) // 原樹為空,新插入的記錄為根節點。
{
T=(BitTree)malloc(sizeof(BSTNode));
T->data=k;
T->lchild=T->rchild=NULL;
return 1;
}
else if(k==T->data) // 存在相同的結點。
return 0;
else if(k<T->data) // 插入到T的左子樹中
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
由此可見,插入的新節點一定是某個葉節點。在一個二叉排序樹先后依次插入結點28和58,虛線表示的邊是其查找的路徑。
二叉排序樹的構造
void Create_BST(BitTree &T,int str[],int n)
{
T=NULL;
int i=0;
while(i<n)
{
BST_Insert(T,str[i]);
i++;
}
}
二叉排序樹的刪除
在二叉排序樹中刪除一個結點時,不能把以該結點為根的子樹上的結點都刪除,必須先把被刪除結點從存儲二叉排序樹的鏈表上摘下來,將因刪除結點而斷開的二叉鏈表重新鏈接起來,同時確保二叉排序樹的性質不會丟失。
刪除操作的實現過程按照以下三種情況進行處理:
- 如果被刪除結點z是葉節點,則直接刪除,不會破壞二叉排序樹的性質。
- 如果結點z只有一顆左子樹后者右子樹,則讓z的子樹稱為z父結點的子樹,替代z的位置。
- 若結點z有左右兩棵子樹,則令z的直接后繼(或者直接前驅)替代z,然后從二叉排序樹中刪去這個直接后繼(或者直接前驅),這樣就變成了第一種或者第二種情況。
平衡二叉樹的插入:二叉排序樹保證平衡的基本思想:每當在二叉排序樹中插入(或刪除)一個節點時,首先要檢查其在插入路徑上的節點是否因此次操作導致了不平衡。如果導致了不平衡,則先找到插入路徑上距離插入節點最近的平衡因子絕對值大於1的節點A,再對以A為根的子樹,在保持二叉排序樹特性的前提下調整各節點的位置關系,使之重新達到平衡。
注意:每次調整的對象都是最小不平衡子樹,即在插入路徑上距離插入節點最近的平衡因子的絕對值大於1的結點作為根的子樹。
從上述步驟中可以看出哈夫曼樹具有如下的特點:
1. 每個初始結點最終都會稱為葉節點,並且權值越小的結點路徑長度越大。
2. 構造過程中共新建了$N-1$個結點,因此哈夫曼樹中結點的總數為$2N-1$。
3. 每次構造都選擇兩棵樹作為新節點的孩子,因此哈夫曼樹中不存在度為1的結點。
哈夫曼樹編碼
對待處理一個字符串序列,如果每個字符用同樣長度的二進制位來表示,則這種方式稱為固定長度編碼。若允許對不同字符用不等長的二進制位表示,則這種編碼方式稱為可變長度編碼。可變長度編碼比固定長度編碼好得多,其特點是對頻率高的字符賦予段編碼,而對頻率較低的字符則賦予一些較長的編碼,從而可以是字符的平均編碼長度被縮短,起到壓縮數據的效果。哈夫曼編碼是一種被廣泛應用而且非常有效的數據壓縮編碼方法。
如果沒有一個編碼是另一個編碼的前綴,則稱這樣的編碼為前綴編碼。如0,101,100是前綴編碼。對前綴編碼的解碼也是很簡單的。因為沒有一個碼是其他碼的前綴。所以可以識別出第一個編碼,將他翻譯為源碼,在對雨下的編碼文件重復同樣的操作。如\('00101100'\)可被唯一的分析為0,0,101,100。
由哈夫曼樹得到哈夫曼編碼是很自然的過程,首先將每個出現的字符當作一個獨立的結點,其權值為它出現的頻度(或者是次數),構造出對應的哈夫曼樹。顯然所有字符結點都出現在葉節點中。我們可以將字符的編碼解釋為從根至該字符的路徑上邊標記的序列,其中邊標記為0表示“轉向左孩子”,標記為1表示“轉向右孩子”。
