1. 程式人生 > >資料結構(C語言版 嚴蔚敏著)——樹

資料結構(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;
}