定義:
樹(Tree)是n(n≥0)個結點的有限集,它或爲空樹(n = 0);或爲非空樹,對於非空樹T:
(1)有且僅有一個稱之爲根的結點;
(2)除根結點以外的其餘結點可分爲m(m>0)個互不相交的有限集T1, T2, …, Tm, 其中每一個集合本身又是一棵樹,並且稱爲根的子樹(SubTree)。
對於樹,我們要明白一些最基本的術語:根節點(root),葉子(終端節點),森林(多顆不相交的數構成的集合),有序樹(對於一個節點的子樹,從左到右爲其順序,不可交換,比如二叉樹),無序樹,前驅結點,後繼節點(孩子),兄弟,堂兄弟,組先(從根節點下來的所有分支節點的集合),子孫,節點的度,樹的度(子樹的個數,分支的個數),節點的層次,樹的深度,分支節點。
而一般的樹並不是我們研究的重點,我們研究的主要還是二叉樹Binary Tree。爲什麼呢?普通樹(多叉樹)若不轉化爲二叉樹,則運算很難實現。那二叉樹有那些特徵呢?
1-節點的度,一定小於等於2
2-他是有序樹,左右子樹不可顛倒。
二叉樹可以實現很多功能,比如霍夫曼樹實現數據壓縮,二叉樹來求表達式的值,
二叉樹的一些有趣的性質:
1-對於任何一顆二叉樹,我們根據度數分爲n0,n1,n2.那麼一定有N=n0+n1+n2,並且如果二度的節點有n2個,葉子(0度節點)必定爲n2+1.那麼我們再看,n2+n0=2n2+1了,它必定是個奇數。
對於二叉樹也有分類:
到此,我們可以出一道思考題了。
一棵完全二叉樹有5000個結點,可以計算出其葉結點的個數是( )。
【Tip:完全二叉樹,其中的度爲1的結點個數最多是1個】
2-對完全二叉樹,若從上至下、從左至右編號,則編號爲i 的結點,其左孩子編號必爲2i,其右孩子編號必爲2i+1;其雙親的編號必爲i/2。
3- 具有n個結點的完全二叉樹的深度必爲[log2n]+1,因爲完全二叉樹是有序的,而且前面都不缺省,用對數的思想,或者說,你可以數學歸納來想這個簡單的問題。
4-任何樹都可以轉化爲二叉樹,根據左子指孩子,右子指本節點的親兄弟的方法。
現在讓我們言歸正傳,二叉樹這個數據結構的存儲的實現:
順序存儲:(非完全二叉樹,缺省部分置一個特殊缺省值,比如0)
另外可以用鏈式存儲結構:
比如二叉鏈表:
於是我們有可以出一道思考題:在n個結點的二叉鏈表中,有 _____個空指針域。
這種方式只能從上往下,於是我們可以引入一個父代節點指針,這樣就美名爲三叉鏈表了。
遍歷二叉樹和線索二叉樹:
遍歷的定義:指按某條搜索路線遍訪每個結點並且不重複。
遍歷的目的:對樹結構進行 插入、刪除、修改、查找、排序的前提,是二叉樹運算的核心
遍歷的規則:DLR先序遍歷 LDR中序遍歷 LRD後序遍歷 RDL RLD,L代表左子樹,R代表右子樹,D代表節點。三種順序遍歷的過程都一樣(Routine),只是訪問各個結點的時機不同。
對於先序遍歷算法(遞歸方法):
//In a recursive way:DLR
Status PreOrderTraverse(BiTree T)
{
if(T==NULL) return OK; //若爲空,則執行空操作
else
{
printf("%d ",T->data); //訪問節點
PreOrderTraverse(T->lchild); //遞歸遍歷左子樹
PreOrderTraverse(T->rchild);
}
}
層次圖:
同理,中序遍歷和後序遍歷不過是內部遞歸調用和訪問根節點的順序不一樣而已。
//二叉樹的建立:
void CreateBiTree(BiTree &T){
scanf(“%c”,&ch);
if (ch==’#’) T=NULL; //遞歸結束,建空樹
else{
T=new BiTNode; T->data=ch; //生成根結點
CreateBiTree(T->lchild); //遞歸創建左子樹
CreateBiTree(T->rchild); //遞歸創建右子樹
}
}
//二叉樹節點的計數:
int NodeCount(BiTree T)
{
if(T == NULL ) return 0;
else return (NodeCount(T->lchild)+NodeCount(T->rchild)+1);
}
//count leafnode:
int LeadCount(BiTree T)
{
if(T==NULL) //如果是空樹返回0
return 0;
if (T->lchild == NULL && T->rchild == NULL)
return 1; //如果是葉子結點返回1
else return LeafCount(T->lchild) + LeafCount(T->rchild);
}
/*題外話:以中序遍歷爲例子,我們介紹一下非遞歸遍歷方法:
遇到一個結點,就把它壓棧,並去遍歷它的左子樹;
當左子樹遍歷結束後,從棧頂彈出這個結點並訪問它;
然後按其右指針再去中序遍歷該結點的右子樹。
void InOrderTraversal( BiTree T )
{
BinTree T=BT;
Stack S = CreatStack( MAXMIZE );
/*創建並初始化堆棧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; /*轉向右子樹*/
}
}
}
*/
從根節點到葉子結點一次經過的結點形成樹的一條路徑,最長路徑的長度爲樹的深度。根節點的深度爲1。
哈夫曼樹:
在一般的數據結構的書中,樹的那章後面,著者一般都會介紹一下哈夫曼(HUFFMAN)樹和哈夫曼編碼。哈夫曼編碼是哈夫曼樹的一個應用。哈夫曼編碼應用廣泛,如JPEG中就應用了哈夫曼編碼。 首先介紹什麼是哈夫曼樹。哈夫曼樹又稱最優二叉樹,是一種帶權路徑長度最短的二叉樹。所謂樹的帶權路徑長度,就是樹中所有的葉結點的權值乘上其到根結點的 路徑長度(若根結點爲0層,葉結點到根結點的路徑長度爲葉結點的層數)。樹的帶權路徑長度記爲WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),N個權值Wi(i=1,2,...n)構成一棵有N個葉結點的二叉樹,相應的葉結點的路徑長度爲Li(i=1,2,...n)。可以證明哈夫曼樹的WPL是最小的。
應用實例:哈夫曼的編碼:
關鍵:要設計長度不等的編碼,則必須使任一字符的編碼都不是另一個字符的編碼的前綴-前綴編碼
哈夫曼樹的構造過程:
根據給定的n個權值{w1,w2,……wn},構造n棵只有根結點的二叉樹。 在森林中選取兩棵根結點權值最小的樹作左右子樹,構造一棵新的二叉樹,置新二叉樹根結點權值爲其左右子樹根結點權值之和。 在森林中刪除這兩棵樹,同時將新得到的二叉樹加入森林中。 重複上述兩步,直到只含一棵樹爲止,這棵樹即哈夫曼樹。
編碼化:
一棵有n個葉子結點的Huffman樹有 2n-1 個結點,採用順序存儲結構——一維結構數組。至於構造哈夫曼樹:
1)初始化HT[1..2n-1]:lch=rch=parent=0
2)輸入初始n個葉子結點:置HT[1..n]的weight值
3)進行以下n-1次合併,依次產生HT[i],i=n+1..2n-1:
3.1)在HT[1..i-1]中選兩個未被選過的weight最小的兩個結點HT[s1]和HT[s2] (從parent = 0 的結點中選)
3.2)修改HT[s1]和HT[s2]的parent值: parent=i
3.3)置HT[i]:weight=HT[s1].weight + HT[s2].weight , lch=s1, rch=s2
算法思路:
* 實現過程:着先通過 HuffmanTree() 函數構造哈夫曼樹,然後在主函數 main()中 自底向上開始(也就是從數組序號爲零的結點開始)向上層層判斷,若在 父結點左側,則置碼爲 0,若在右側,則置碼爲 1。最後輸出生成的編碼。
藉助最小堆的實現:(考完半期我再補一節關於堆、堆棧的水文吧,最近太忙)
typedef struct TreeNode *HuffmanTree;
struct TreeNode
{
int Weight;
HuffmanTree Left, Right;
}
HuffmanTree Huffman( MinHeap H )
{
/* 假設H->Size個權值已經存在H->Elements[]->Weight裏 */
int i;
HuffmanTree T;
BuildMinHeap(H); /*將H->Elements[]按權值調整爲最小堆*/
for (i = 1; i < H->Size; i++)
{
/*做H->Size-1次合併*/
T = malloc( sizeof( struct TreeNode) );
/*建立新結點*/
T->Left = DeleteMin(H);
/*從最小堆中刪除一個結點,作爲新T的左子結點*/
T->Right = DeleteMin(H);
/*從最小堆中刪除一個結點,作爲新T的右子結點*/
T->Weight = T->Left->Weight+T->Right->Weight;
/*計算新權值*/
Insert( H, T ); /*將新T插入最小堆*/
}
T = DeleteMin(H);
return T;
嚴蔚敏版教材:
typedef struct
{
int weight;//表徵節點的權值
int parent,lchild,rchild;//節點的雙親,左子,右子的下標
}HTnode,*HuffmanTree;//動態分配數組內存存儲哈夫曼樹
//構造哈夫曼樹
void CreateHuffmanTree(HuffmanTree &HT,int n)
{
if(n<=1) return;
int m=2*n-1; //n個有權的葉子節點,就一共有m=2n-1個節點
HT =new HTnode[m+1]; //動態分配內存,0號單元未引用
//Initialize
for(i=1;i<=m;++i)
{
HT[i].parent=0;HT[i].lchild=0;HT[i].rchild=0;
}
//Input the leafnode's weight in sequence
for(i=1;i<=n;i++)
cin>>HT[i].weight;
//Then we create a huffman tree
for(i=n+1;i<=m;++i)//通過n-1次的選擇、刪除、合併來生成一個哈夫曼樹
{
Select(HT,i-1,s1,s2);
//在HT[k],1<=k<=i-1中選擇一個雙親爲0,權最小的節點,並且返回他們在HT中的序號s1和s2
}
}