資料結構(C語言版 嚴蔚敏著)——樹
· 樹(tree)是n(n>=0)個結點的有限集。當n=0時成為空樹,在任意一顆非空樹中:
//這裡只需掌握定義,重點在二叉樹
-有且僅有一個特定的稱為根(Root)的結點;
-當n>1時,其餘結點可分為m(m>0)個互不相交的有限集T1、T2、...、Tm,
其中集合本身又是一棵樹,並且稱為根的子樹(SubTree)。
- n>0時,根結點是唯一的,堅決不可能存在多個根結點。
- m>0時,子樹的個數是沒有限制的,但它們互相是一定不會相交的。
· 結點擁有的子樹稱為結點的度(Degree),樹的度取樹內各結點的度的最大值 。
-度為0的結點稱為葉結點(Leaf)或終端結點。
-度不為0的結點稱為分支結點或非終端結點,除根結點外 ,分支結點也稱為內部結點。
· 結點的子樹的根稱為結點的孩子(Child),相應的,該結點稱為孩子的雙親(Parent),
同一雙親的孩子之間互稱為兄弟(Sibling)。
· 結點的祖先是從根到該結點所經分支上的所有結點。
· 結點的層次(Level)從根開始定一起,根為第一層,根的孩子為第二層。
· 其雙親在同一層的結點互為堂兄弟。
· 樹中結點的最大層次稱為樹的深度(Depth)或高度。
· 如果將樹中結點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹
為有序樹,否則稱為無序樹。
二叉樹
定義
· 二叉樹是n(n>=0)個結點 的 有限集合,該集合或者為空集(空二叉樹),或者由
一個根結點和兩棵互不相交的、分別稱為根結點的左子樹和右子樹的二叉樹組成。
特點
· 每個結點 最多 有兩棵子樹,所以二叉樹中不存在度大於2的結點。
· 左子樹和右子樹是有順序的,次序不能顛倒。
· 即使樹中某節點只有一棵子樹,也要區分它是左子樹還是右子樹。
五種基本形態
· 空二叉樹
· 只有一個根結點
· 根結點只有左子樹
· 根結點只有右子樹
· 根結點既有左子樹又有右子樹
滿二叉樹
-在一棵二叉樹中,如果所有分支點都存在左子樹和右子樹,並且所有葉子都在同一層上,
這樣的二叉樹稱為滿二叉樹。
· 滿二叉樹的特點有:
-葉子只能出現在最下一層。
-非葉子結點的度一定是2。
-在同樣深度的二叉樹中,滿二叉樹的結點個數一定最多,同時葉子也是最多。
完全二叉樹
· 對一棵具有n個結點的二叉樹按層序編號,如果編號為i(1<=i<=n)的結點與同樣深度的滿二叉樹
中編號為i的結點位置完全相同,則這顆 二叉樹稱為完全二叉樹。
· 完全二叉樹的特點有:
-葉子結點只能出現在最下兩層。
-最下層的葉子一定集中在左部連續位置。
-倒數第二層,若有葉子結點,一定都在右部連續位置。
-如果結點度為1,則該結點只有左孩子。
-同樣結點數的二叉樹,完全二叉樹的深度最小。
· 注意:滿二叉樹一定是完全二叉樹,但完全二叉樹不一定是滿二叉樹。
二叉樹的性質
· 性質一:在二叉樹的第i層上至多有2^(i-1)個結點(i>=1)。
· 性質二:深度為k的二叉樹至多有2^k-1個結點(k>=1)。
· 性質三:對任何一棵二叉樹T,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。
-推導過程
-首先再假設度為1的結點數為n1,則二叉樹T的結點總數n=n0+n1+n2
-其次發現連線樹總是等於總結點數n-1,並且等於n1+2*n2
-所以n-1=n1+2*n2
-所以n0+n1+n2-1=n1+n2+n2
-最後n0=n2+1
· 性質四:具有n個結點的完全二叉樹的深度為取下整的(log2n)+1
-由滿二叉樹的定義結合性質二可得,深度為k的滿二叉樹的結點樹n一定是2^k-1。
-對於滿二叉樹可以通過n=2^k-1推得滿二叉樹的深度為k=log2(n+1)
-對於倒數第二層的滿二叉樹我們同樣很容易回推出它的結點數為n=2^(k-1)-1
-所以完全二叉樹的結點數的取值範圍是:2^(k-1)-1<n<=2^k-1
-由於n是整數,n<=2^k-1可以看成n<2^k
-同理2^(k-1)-1<n可以看成2^(k-1)<=n
-所以2^(k-1)<=n<2^k
-不等式 兩邊同時取對數,得到k-1<=log2n<k
-由於k是深度,必須取整,所以k為取下整的(log2n)+1
· 性質五:如果對一棵有n個結點的完全二叉樹(其深度為(log2n)+1)的結點
按層序編號,對任一結點i(1<=i<=n)有以下性質:
-如果i=1,則結點i是二叉樹的根,無雙親;如果i>1,則其雙親結點[i/2]取下整
-如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子結點是2i
-如果2i+1>n,則結點i無右孩子;否則其右孩子是結點2i+1
二叉連結串列
儲存結構:
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
BiTNode *lchild,*rchild;//左右孩子指標
}BiTNode,*BiTree;
建立一個二叉樹
void CreateBiTree(BiTree &T){
//按先序遍歷輸入結點,左孩子或右孩子為空,用空格代替
char c;
scanf("%c",&c);
if(c==' '){
//如果為空格,則指向的左孩子或者右孩子為空
T=NULL;
} else{
//建立結點,按照先序遍歷建立
T=(BiTree)malloc(sizeof(BiTNode));
if(!T)
exit(0);
T->data=c;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
二叉樹的遍歷
· 二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹中所有結點,使得
每個結點被訪問一次且僅被訪問一次。
· 二叉樹的遍歷次序不同於線性結構,線性結構最多也就是分為順序、迴圈、雙向等
簡單的遍歷方式。
· 樹的結點之間不存在唯一的前驅和後繼這樣的關係,在訪問一個結點後,下一個被
訪問的結點面臨著不同的選擇。
· 二叉樹的遍歷方式可以很多,主要有下面三種:
· 前序遍歷:
-若二叉樹為空,則空操作返回,否則先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹。
· 中序遍歷:
-若樹為空,則空操作返回,否則從根結點開始(注意並不是先訪問根結點),中序
遍歷根結點的左子樹,然後是訪問根結點,最後中序遍歷右子樹。
· 後序遍歷:
-若樹為空,則空操作返回,否則從左到右先葉子後結點的方式遍歷訪問左右子樹,最後根結點。
先序遍歷:ABCDEFGHK
中序遍歷:BDCAEHGKF
後序遍歷:DCBHKGFEA
遞迴遍歷演算法程式碼實現:
void PrintElement(TElemType e){
printf("%c",e);
}
void PreOrderTraverse(BiTree T){
if(T){
//先序遍歷
//三種遍歷方式只不過更換下面三句語句的順序
PrintElement(T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
void InOrderTraverse(BiTree T){
//中序遍歷
if(T){
InOrderTraverse(T->lchild);
PrintElement(T->data);
InOrderTraverse(T->rchild);
}
}
void PostOrderTraverse(BiTree T){
//後序遍歷
if(T){
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
PrintElement(T->data);
}
}
非遞迴的兩種演算法:
· 另需定義一個棧,儲存結點
#define STACK_INIT_SIZE 100 //儲存空間初始分配量
#define STACKINCREMENT 10 //儲存空間分配增量
typedef struct {
BiTree *base; //在棧構造之前和銷燬之後,base值為NULL
BiTree *top; //棧頂指標
int stacksize; //當前已分配的儲存空間,以元素為單位
} SqStack;
int InitStack(SqStack &S) {
//構造一個空棧S
S.base = (BiTree *) malloc(STACK_INIT_SIZE * sizeof(BiTree));
//儲存分配失敗
if (!S.base)
exit(0);
S.top = S.base;
S.stacksize = STACK_INIT_SIZE;
return 1;
}
int Push(SqStack &S, BiTree e) {
//插入元素e為新的棧頂元素
if (S.top - S.base >= S.stacksize) {
//棧滿,追加儲存空間
S.base = (BiTree *) realloc(S.base,
(S.stacksize + STACKINCREMENT) * sizeof(BiTree));
//出錯退出
if (!S.base)
exit(0);
//使top指標重新回到棧頂
S.top = S.base + S.stacksize;
S.stacksize += STACKINCREMENT;
}
*S.top++ = e;//賦值後,指標上移
return 1;
}
int Pop(SqStack &S, BiTree &e) {
//若棧不為空,則刪除S的棧頂元素,用e返回其值
//並返回1,否則返回0
if (S.top == S.base)
return 0;
//top指標下移,並賦值給e
e = *--S.top;
return 1;
}
int GetTop(SqStack S, BiTree &e) {
//若棧不空,則用e返回S的棧頂元素,並返回1,否則返回0
if (S.top == S.base)
return 0;
e = *(S.top - 1);
return 1;
}
int StackEmpty(SqStack S) {
//判斷棧是否為空,空則返回1,否則返回0
if (S.base == S.top)
return 1;
else
return 0;
}
void unInOrderTraverse1(BiTree T){
//採用二叉連結串列儲存結構
//中序遍歷二叉樹T的非遞迴 演算法
//方法1
SqStack S;
BiTree p;
InitStack(S);//建立棧
Push(S,T);//頭結點入棧
while (!StackEmpty(S)){//當棧非空時
while (GetTop(S,p)&&p)//把棧頂元素給p,且p存在
Push(S,p->lchild);//一直往左,直到盡頭
Pop(S,p);//空指標出棧
if(!StackEmpty(S)){//判斷是否空棧
Pop(S,p);//最左邊的一個結點出棧
if(!p->data)//訪問結點
exit(0);
else
PrintElement(p->data);
Push(S,p->rchild);//該結點的右子樹進棧
}
}
}
void unInOrderTraverse2(BiTree T){
//採用二叉連結串列儲存結構
//中序遍歷二叉樹T的非遞迴 演算法
//方法2
SqStack S;
BiTree p;
//建立棧
InitStack(S);
p=T;
while (p||!StackEmpty(S)){
if(p){
//根指標進棧,遍歷左子樹
Push(S,p);
p=p->lchild;
} else{
//根指標退棧,訪問根結點 ,遍歷右子樹
Pop(S,p);
if(!p->data)
exit(0);
else
PrintElement(p->data);
p=p->rchild;
}
}
}
線索二叉樹
普通二叉樹在葉子結點中存在空指標,造成了空間浪費,線索二叉樹把這些利用起來
並且能提高遍歷的效率。就像連結串列一樣,直接指示下一個結點的位置。
需要增加兩個標識域
· LTage 為0 lchild域指示結點的左孩子
· LTage 為1 lchild域指示結點的前驅
· RTage 為0 rchild域指示結點的右孩子
· RTage 為1 rchild域指示結點的後繼
結構體程式碼:
typedef char TElemType;
//Link為0表示左右孩子的指標
//Thread為1表示前驅後繼的線索
enum PointerTag {
Link, Thread
};
typedef struct BiThrNode {
TElemType data;
struct BiThrNode *lchild, *rchild;
PointerTag LTag, RTag;
} BiThrNode, *BiThrTree;
中序遍歷線索化以及中序遍歷二叉線索樹T的非遞迴演算法
//全域性變數,始終指向剛剛訪問過的結點
BiThrTree pre;
void CreateBiThrTree(BiThrTree &T) {
//遵循前序遍歷約定輸入
char c;
scanf("%c", &c);
if (c == ' ')
T = NULL;
else {
T = (BiThrTree) malloc(sizeof(BiThrNode));
if (!T)
exit(0);
T->data = c;
printf("%c", c);
//先預設它有左右子樹
T->LTag = Link;
T->RTag = Link;
CreateBiThrTree(T->lchild);
CreateBiThrTree(T->rchild);
}
}
//中序遍歷線索化
void InTreading(BiThrTree p) {
if (p) {
//遞迴左孩子線索化
InTreading(p->lchild);
//如果該結點沒有左孩子,設定LTag為Thread,
// 並把lchild指向剛剛訪問的結點,設為前驅
if (!p->lchild) {
p->LTag = Thread;
p->lchild = pre;
}
//如果該結點沒有右孩子,設定RTag為Thread,
// 並把剛剛訪問過結點的rchild指向當前結點,設為後繼
if (!pre->rchild) {
pre->RTag = Thread;
pre->rchild = p;
}
pre = p;
InTreading(p->rchild);
}
}
void InOrderThreading(BiThrTree &Thrt, BiThrTree T) {
//中序遍歷二叉樹T,並將其中序線索化,Thrt指向頭結點
if (!(Thrt = (BiThrTree) malloc(sizeof(BiThrNode))))
exit(0);
//建立頭結點
Thrt->LTag = Link;
Thrt->RTag = Thread;
Thrt->rchild = Thrt;//右指標回指
//若二叉樹空,則左指標回指
if (!T)
Thrt->lchild = Thrt;
else {
Thrt->lchild = T;
pre = Thrt;
InTreading(T);//中序遍歷進行中序線索化
//最後一個結點線索化
pre->rchild = Thrt;
pre->RTag = Thread;
Thrt->rchild = pre;
}
}
void PrintElement(TElemType e) {
printf("%c", e);
}
void InOrderTraverse_Thr(BiThrTree T) {
//T指向頭結點,頭結點的左鏈lchild指向根結點
//中序遍歷二叉線索樹T的非遞迴演算法
BiThrTree p;
p = T->lchild;//p指向根結點
while (p != T) {//空樹或遍歷結束時,p==T
while (p->LTag == Link)
p = p->lchild;
if (!p->data)
exit(0);
else
PrintElement(p->data);//訪問其左子樹為空的結點
while (p->RTag == Thread && p->rchild != T) {
p = p->rchild;
PrintElement(p->data);//訪問後繼結點
}
p = p->rchild;
}
}
樹、森林及二叉樹的相互轉換
· 樹轉換成二叉樹:
-加線,在所有兄弟結點之間加一條線。
-去線,對樹中每個結點,只保留它與第一孩子結點的連線,
刪除它與其他孩子結點之間的連線。
-層次調整,以樹的根結點為軸心,將整棵樹順時針旋轉
一定角度,使之結構層次分明。
1.第一步,在樹中所有兄弟結點之間加一連線
2.第二步,對每個結點,除了保留與其長子的連線外,去掉該結點與其它孩子的連線。
· 森林轉換二叉樹:
-先將森林中的每棵樹變為二叉樹。
-再將各二叉樹的根結點視為兄弟從左至右連在一起,就這樣形成一個二叉樹。
把第一棵根結點為根結點,其他根結點連起來,作為它的右子樹。
1.第一步,先將森林中的每棵樹變為二叉樹。
2.第二步,將各二叉樹的根結點視為兄弟從左至右連在一起。
· 二叉樹到樹、森林的轉換
-二叉樹轉換為普通樹是剛才的逆過程,步驟也就是反過來而已
-判斷一棵二叉樹能夠轉換成一棵樹還是森林,那就是隻要看這棵
二叉樹的根結點有沒有右子樹,有的話就是森林,沒有就是一棵樹。
樹與森林的遍歷:(理解)
· 樹的遍歷分為兩種方式:一種是先根遍歷,另一種是後根遍歷。
· 先根遍歷:先訪問樹的根結點,然後再依次先根遍歷根的每棵子樹。
· 後根遍歷:依次遍歷每棵子樹,然後再訪問根結點。
· 先根遍歷結果:ABEFCGDHIJ
· 後根遍歷結果:EFBGCHIJDA
·森林的遍歷也分為前序遍歷和後序遍歷,其實就是按照樹的先根遍歷和
後根遍歷依次訪問森林的每棵樹。
· 有個 驚人的發現:樹、森林前根(序)遍歷和二叉樹的前序遍歷結果相同,
樹、森林的後根(序)遍歷和二叉樹的中序遍歷結果相同
· 於是我們可以找到對樹和森林遍歷這種複雜問題的簡單解決方案。
赫夫曼樹
· 結點的路徑長度:
-從根結點到該結點的路徑上的連線數。
· 樹的路徑長度:
-樹中每個葉子結點的路徑長度之和。
· 結點帶權路徑長度:
-結點的路徑長度與結點權值的乘積。
· 樹的帶權路徑長度:
-WPL是樹中所有葉子結點的帶權路徑長度之和。
WPL值越小,說明構造出來的二叉樹效能越優
赫夫曼樹的構造過程
-選權值最小的兩個結點構成一個二叉樹,其雙親結點權值為兩個結點權值之和。
-然後再選取剩下結點的權值最小的,與上一步的二叉樹組合,構成新的二叉樹,
如此重複,直到沒有結點剩下,這棵二叉樹便是赫夫曼樹。
構造完成。
注意:為了使得到的哈夫曼樹的結構儘量唯一,通常規定生成的哈夫曼樹中每個結點的左子樹
根結點的權小於等於右子樹根結點的權。
赫夫曼編碼
· 赫夫曼編碼可以有效地壓縮資料(通常可以節省20%~90%的空間,具體壓縮率依賴於資料的特性)。
· 定長編碼、變長編碼、字首碼
-定長編碼:類似於ASCII編碼。
-變長編碼:單個編碼的長度不一致,可以根據整體出現頻率來調節。
-字首碼:沒有任何碼字是其他碼字的字首。
· 赫夫曼樹中沒有度為1的結點(這類樹又稱為嚴格的二叉樹),則一棵有n個葉子結點的赫夫曼樹
共有2n-1個結點,可以儲存在一個大小為2n-1的以為陣列中。
· 由於在構成赫夫曼樹之後,為求編碼需從葉子結點出發走一條從葉子到根的路徑;而為編碼需
從根到葉子的路徑。則對沒個結點而言,既需知雙親的資訊,又需知孩子結點的資訊。
程式碼實現:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int weight;
int parent, lchild, rchild;
} HTNode, *HuffmanTree; //動態分配陣列儲存赫夫曼樹
typedef char **HuffmanCode;//動態分配陣列儲存赫夫曼編碼表
//這裡可以理解成相當於許多字串組成的陣列
void Select(HuffmanTree htree, int end, int &s1, int &s2) {
//從數集中選取parent=0,weight最小的兩個結點,其下標存入s1,s2
int min1, min2;
int i = 1;
//找到第一個沒有雙親的結點
while (htree[i].parent != 0 && i <= end)
i++;
//作為一個參考的最小值,存入min1和s1
min1 = htree[i].weight;
s1 = i;
//下個結點開始
i++;
//找到第二個沒有雙親的結點,與前面那個作對比
//較小的放min1和s1,較大的放min2和s2
while (htree[i].parent != 0 && i <= end)
i++;
if (htree[i].weight < min1) {
min2 = min1;
s2 = s1;
min1 = htree[i].weight;
s1 = i;
} else {
min2 = htree[i].weight;
s2 = i;
}
//遍歷剩下無雙親的結點
for (int j = i + 1; j <= end; j++) {
if (htree[j].parent != 0)
continue;
//如果比min1小,min1,s1的資料移到min2,s2
//並把當前結點的值賦給min1,結點序號給s1
if (htree[j].weight < min1) {
min2 = min1;
min1 = htree[j].weight;
s2 = s1;
s1 = j;
} else if (htree[j].weight >= min1 && htree[j].weight < min2) {
//如果比min1大且比min2小,
//則把當前結點值賦給min2,序號給s2
min2 = htree[j].weight;
s2 = j;
}
}
}
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n) {
//w存放n個字元的權值(均>0),構造赫夫曼樹HT,並求出n個字元的赫夫曼樹編碼HC
HuffmanTree p;
int i, s1, s2, start, c, f;
if (n <= 1)
return;
int m = 2 * n - 1;
HT = (HuffmanTree) malloc((m + 1) * sizeof(HTNode));//0號單元不用
//注意,第一個結點為空
//每個結點賦初值
//n個葉子結點
for (p = HT+1, i = 1; i <= n; ++i, ++p, ++w)
*p = {*w, 0, 0, 0};
//m-n個終端結點
for (i; i <= m; ++i, ++p)
*p = {0, 0, 0, 0};
for (i = n + 1; i <= m; ++i) {
//在HT[1...i-1]選擇parent為0且weight最小的兩個結點,其序號分別為 s1和s2
//組合好後又成為一個新的可選結點,故[1...i-1]
Select(HT, i - 1, s1, s2);
//最小兩個結點的雙親序號為當前i
HT[s1].parent = i;
HT[s2].parent = i;
//當前i結點左右孩子序號分別是s1,s2
HT[i].lchild = s1;
HT[i].rchild = s2;
//權重為兩孩子之和
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
//---從葉子到根逆向求沒個字元的赫夫曼編碼---
//分配n+1個字元編碼的頭指標向量
//0號位不存放資料
HC = (HuffmanCode) malloc((n + 1) * sizeof(char *));
//分配求編碼的工作空間
char *cd = (char *) malloc(n * sizeof(char));
//最後一個字元為結束符
cd[n - 1] = '\0';
//逐個字元求赫夫曼編碼
for (i = 1; i <= n; ++i) {
start = n - 1;//編碼結束符位置
for (c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent){
//從葉子到根逆向求編碼
if (HT[f].lchild == c)
cd[--start] = '0';
else
cd[--start] = '1';
}
//為第i個字元分配空間
HC[i] = (char *) malloc((n - start) * sizeof(char));
//將遍歷得到的編碼串複製到HC[i]
strcpy(HC[i], &cd[start]);
}
free(cd);
}
void print_huffman_tree(HuffmanTree htree, int n) {
printf("Huffman tree:\n");
int m = 2 * n - 1;
for (int i = 1; i < m; ++i) {
printf("node_%d, weight = %d, parent = %d, left = %d, right = %d\n",
i, htree[i].weight, htree[i].parent, htree[i].lchild, htree[i].rchild);
}
}
void print_all_huffman_code(HuffmanCode HC, int n) {
printf("Huffman code:\n");
for (int i = 1; i <= n; ++i) {
printf("%d code = %s\n", i, HC[i]);
}
}
int main() {
int w[5] = {2, 8, 7, 6, 5};
int n = 5;
HuffmanTree HT;
HuffmanCode HC;
HuffmanCoding(HT, HC, w, n);
print_huffman_tree(HT, n);
print_all_huffman_code(HC, n);
return 0;
}