實驗——樹(根據后序和中序遍歷輸出先序遍歷、哈夫曼編碼)詳細過程


實驗——樹(根據后序和中序遍歷輸出先序遍歷、哈夫曼編碼)

一、實驗目的

  1. 熟練掌握二叉樹、完全二叉樹的存儲方式,二叉樹的前序、中序、后序和層次遍歷方法,樹的性質。

  2. 練習建立二叉樹的算法,通過前中、后中順序確定二叉樹的算法。

  3. 通過二叉樹的算法,解決哈夫曼編碼等應用問題。

二、 根據后序和中序遍歷輸出先序遍歷

2.1 實驗內容和要求

問題描述

要求根據給定的一棵二叉樹的后序遍歷和中序遍歷結果,輸出該樹的先序遍歷結果。

輸入格式

第一行給出正整數N(≤30),是樹中結點的個數。隨后兩行,每行給出N個整數,分別對應后序遍歷和中序遍歷結果,數字間以空格分隔。題目保證輸入正確對應一棵二叉樹。

輸出格式

在一行中輸出Preorder: 以及該樹的先序遍歷結果。數字間有1個空格,行末不得有多余空格。

輸入樣例

7
2 3 1 5 7 6 4
1 2 3 4 5 6 7

輸出樣例

Preorder: 4 1 3 2 6 5 7

 

2.2 算法設計

  1. 主流程設計

 int main(){
   輸入樹中結點個數;
   輸入該樹的后序遍歷結果;
   輸入該樹的中序遍歷結果;
   通過兩種遍歷結果創建該樹;
   用先序遍歷方法輸出該樹;
   return 0;
 }

 

      2. 構建二叉樹流程分析       

          *設計思路:通過后序遍歷找到該樹的根結點(即后序遍歷最后一個結點),根結點將中序遍歷序列分為兩個子序列,就可以確定根結點下的左右子樹的結點個數,且后序遍歷序列可以看作根結點左子樹序列+根結點右子樹序列+根結點組成。由樹的遞歸性可以對根結點左子樹序列、根結點右子樹序列進行相同操作。

         *具體實現:設定兩序列長度均為Len,后序遍歷序列為Post,中序遍歷序列為In,在遞歸過程中后序遍歷序列區間為[k,Len-1],中序遍歷序列區間為[k,Len-1],由上述分析可以知道,在這一遞歸階段中,根結點為Post[Len-1], 接着在中序遍歷序列中尋找位置index,使In[index] = Post[Len-1],這樣這一遞歸階段中左子樹結點數量為i, 進入下一遞歸階段時,左子樹后序遍歷序列和中序遍歷序列變為[k, i-1],右子樹后序遍歷序列變為[k+i, Len-i-1],中序遍歷序列變為[k+i+1, Len-i-1](‘+1’是該樹的根結點的位置,需要跳過去)。

 

BinTree CreateBinTree(int* Post, int* In, int Len) {  //輸入后序、中序和結點的個數
    判斷是否有子樹;
    創建樹;
    將此時后序遍歷的最后一個數作為一個根結點;
    遍歷中序結果;
    若找到與根結點相同的數,將該數下標做標記,並且跳出循環;
    找根結點的左子樹;
    找根結點右子樹的值;
}

 

  1. ADT定義

typedef struct TNode* PreToTNode;
struct TNode {
    int Data;
    PreToTNode Left;
    PreToTNode Right;
};
typedef PreToTNode BinTree;
​
//根據前序和中序遍歷,構建二叉樹
BinTree CreateBinTree(int* Post, int* In, int Len);
​
//先序遍歷輸出
void PreorderTraversal(BinTree BT);

 

  1. 算法示例

     

2.3 算法分析

      通過算法過程示例發現,時間復雜度為 O(n) = O(NlogN[構建二叉樹] + NlogN[先序遍歷輸出]) = O(2*NlogN) ;

      空間復雜度為二叉樹 的構造空間使用 O(n) = O(N)。

三、哈夫曼編碼

3.1 實驗內容和要求

問題描述

給定一段文字,如果我們統計出字母出現的頻率,是可以根據哈夫曼算法給出一套編碼,使得用此編碼壓縮原文可以得到最短的編碼總長。然而哈夫曼編碼並不是唯一的。例如對字符串"aaaxuaxz",容易得到字母 'a'、'x'、'u'、'z' 的出現頻率對應為 4、2、1、1。我們可以設計編碼 {'a'=0, 'x'=10, 'u'=110, 'z'=111},也可以用另一套 {'a'=1, 'x'=01, 'u'=001, 'z'=000},還可以用 {'a'=0, 'x'=11, 'u'=100, 'z'=101},三套編碼都可以把原文壓縮到 14 個字節。但是 {'a'=0, 'x'=01, 'u'=011, 'z'=001} 就不是哈夫曼編碼,因為用這套編碼壓縮得到 00001011001001 后,解碼的結果不唯一,"aaaxuaxz" 和 "aazuaxax" 都可以對應解碼的結果。本題就請你判斷任一套編碼是否哈夫曼編碼。

輸入格式

首先第一行給出一個正整數 N(2≤N≤63),隨后第二行給出 N 個不重復的字符及其出現頻率,格式如下:

c[1] f[1] c[2] f[2] ... c[N] f[N]

其中c[i]是集合{'0' - '9', 'a' - 'z', 'A' - 'Z', '_'}中的字符;f[i]c[i]的出現頻率,為不超過 1000 的整數。再下一行給出一個正整數 M(≤1000),隨后是 M 套待檢的編碼。每套編碼占 N 行,格式為:

c[i] code[i]

其中c[i]是第i個字符;code[i]是不超過63個'0'和'1'的非空字符串。

輸出格式

對每套待檢編碼,如果是正確的哈夫曼編碼,就在一行中輸出"Yes",否則輸出"No"。 注意:最優編碼並不一定通過哈夫曼算法得到。任何能壓縮到最優長度的前綴編碼都應被判為正確。

輸入樣例

7
A 1 B 1 C 1 D 3 E 3 F 6 G 6
4
A 00000
B 00001
C 0001
D 001
E 01
F 10
G 11
A 01010
B 01011
C 0100
D 011
E 10
F 11
G 00
A 000
B 001
C 010
D 011
E 100
F 101
G 110
A 00000
B 00001
C 0001
D 001
E 00
F 10
G 11

 

輸出樣例

Yes
Yes
No
No

 

3.2 算法設計

  1. 主流程設計

 int main(){
    建一個哈夫曼樹;
    將字符存進去;
    計算最優WPL;
    讀入待檢測的編碼;
    比較編碼的wpl是否與最優WPL一致;
    檢測是否是前綴編碼;
  }

 

     2. 哈夫曼樹的構造

         *設計思路:由哈夫曼樹和帶權路徑長度的定義可知,一棵二叉樹要使其WPL最小,必須使權值越大的葉結點越靠近根結點,而權值越小的葉結點越遠離根結點。可在初始狀態下將每一個字符看成一棵獨立的樹,每一步選擇權值最小的兩顆樹進行合並。

 
        
初始化哈夫曼樹;
for(遍歷前n個){
  存葉子節點,賦給權值,其他項(parent、lchild、rchild)賦零;
}
for(遍歷后面的){
  存非葉子節點,權值賦零,其他項也賦零;
}
​
建立哈夫曼樹;
for(遍歷每套編碼方案){
  在HT[1,i-1]中找到沒有parent且權值最小的兩個元素(需要一個函數);
  將其parent、lchild、rchlid賦值,並算該樹的權值,一步一步建立哈夫曼樹;
}
​
注意根結點的位置變化;
}
 
        
 

      3. 最優帶權路徑長度(WPL)的求值

          *設計思路:計算該樹的最優帶權路徑長度,用來判斷所給編碼方案是否為最優編碼。 具體實現:運用遞歸函數,由根結點依次向下找出葉節點。

void GetWPL(HuffmanTree HT, int Deep, HTNode* p){       
    //對葉子節點進行計算
    if ((p->lchild == 0) && (p->rchild == 0)){
        WPL += (p->weight) * Deep;
    }
    if (p->lchild != 0)  //注意細節,不能用 else if
        GetWPL(HT, Deep + 1, HT + (p->lchild));
    if (p->rchild != 0)
        GetWPL(HT, Deep + 1, HT + (p->rchild));
}
 
  1. 前綴編碼的判斷                                                                                                                                                                                                   * 設計思路:前綴編碼是指任一字符的編碼都不是另一個字符編碼的前綴(等長編碼一定是前綴編碼!)。由於需要編碼及其地址,所以運用二維數組的傳遞更為方便。                                                                                                                                                                                   *具體實現:將這些編碼逐個的添加到二維數組中,對於每一個編碼字符串,字符串中的每一個字符也逐個掃描(需要注意循環開始的條件),先假定不是前綴編碼,用flag記錄,兩兩相比較,如果在循環中有不一樣的編碼位,說明是前綴編碼;如果循環結束但已掃描到某節點為葉子節點但字符串還未結束,或者字符串已掃描結束但還當前節點非空,那么就不是前綴碼。

int IsPreCoding(char temp[][64]){
    int i, j, h, len1, len2;
    for (i = 1; i <= N - 1; ++i){
        for (j = i + 1; j <= N; ++j){  //j和i為需要比較的元素下標
            int flag = 0;
            for (h = 0; (temp[i][h] != '\0') && (temp[j][h] != '\0'); h++){
                if (temp[i][h] != temp[j][h]){
                    flag = 1;
                }
            }
            if (flag == 0){
                return 0;
            }
        }
    }
    return 1;
}
 
  1. ADT定義

int m;  //哈夫曼樹節點個數
int w[64], N, M;  //權值數組W[],元素數量N,M套編碼
int WPL;  //帶權路徑長度
​
typedef struct HTNode* HuffmanTree;
struct HTNode{
    int weight;  //結點權值
    int parent, lchild, rchild;
};
HTNode* Root_pos;
typedef char** HuffmanCode;  //定義元素類型為  char數組首地址  的數組
void Select(HuffmanTree& HT, int n, int& s1, int& s2);  //比較權值,找出最小、次小權值
void HuffmanCoding(HuffmanTree& HT, HuffmanCode& HC, int* w, int n);  //構造哈夫曼樹
int IsPreCoding(char temp[][64]);  //判斷是否為前綴編碼
void GetWPL(HuffmanTree HT, int Deep, HTNode* p);  //對葉子節點進行計算,求WPL

 

2.3 算法分析

該Huffman算法的復雜度主要由以下幾部分組成:

(1)構造哈夫曼樹:O(N^2);

(2)求最優WPL: O(NlogN);

(3)前綴編碼的判斷:O(N^2);

故整體復雜度為 O(n) = (NlogN) ;

空間復雜度為結點空間的使用,即O(N)。

四、總結

根據后序和中序遍歷輸出先序遍歷:

主要不清楚的點在構建樹的左右子樹的遞歸函數中后序序列、中序序列和根結點、長度的關系,根據例子畫圖,按照還原后的二叉樹反過來一步一步帶入到函數中,最后再正着分析才明白;

哈夫曼編碼:

雖然知道如何求WPL、哈夫曼編碼怎么構建,但也只是頭腦中的動畫演示,代碼寫不出來,於是在網上找到了代碼做思考分析。這個代碼思路特別清晰,難理解的是其使用的方法,例如運用二維數組的傳遞判斷是否為前綴編碼,不知道為什么用二維數組、為什么不用一維或者直接掃描或者其他容器,經過反復研讀代碼,做了些小試驗,體會到了運用二維數組傳遞的簡潔,還有其他一些不懂的,又重新多看了幾遍慕課和書本相關內容。在整個分析中發現自己更多的問題,特別是樹、指針、地址、數組等的靈活運用,還有代碼的理解能力等,感覺還有一些沒有想到的問題與分析。這是一道經典題,需要強加記憶、反復思考,需要勤加鍛煉、多動手打代碼,需要反復修改;

通過這兩道題:

書、慕課、CSDN、畫圖或者動畫演示等都是為自己能夠記住、能夠自己打出成功代碼而服務的,不要認為理解了、能十分詳細的用圖的形式解釋明白就完事了的,還需要再看反復看,會有新的認識,對自己寫代碼就會有更多的幫助。

五、源代碼(主要)

5.1 根據后序和中序遍歷輸出先序遍歷

/*
 先序遞歸遍歷
 *訪問根結點
 *先序遍歷其左子樹
 *先序遍歷其右子樹
*/
void PreorderTraversal(BinTree BT) {
    if (BT) {
        printf(" %d", BT->Data);
        PreorderTraversal(BT->Left);
        PreorderTraversal(BT->Right);
    }
}
​
/*
 構建樹
 *輸入后序、中序和結點的個數
*/
BinTree CreateBinTree(int* Post, int* In, int Len) {
    BinTree T;
    int index = 0;
    if (Len == 0) {  //判斷是否有子樹
        return NULL;
    }
    T = (BinTree)malloc(sizeof(struct TNode));  //創建樹
    T->Data = Post[Len - 1];  //此時后序遍歷的最后一個數作為一個根結點
    for (int i = 0; i < Len; i++) {  //遍歷中序結果
        if (In[i] == Post[Len - 1]) {  //若找到與根結點相同的數,將該數下標做標記,跳出循環
            index = i;
            break;
        }
    }
    
    T->Left = CreateBinTree(Post, In, index);  //找根結點的左子樹
    T->Right = CreateBinTree(Post + index, In + index + 1, Len - index - 1);  //找根結點右子樹的值
    return T;
}
 

5.2 哈夫曼編碼

/*找最小、次小權值*/
void Select(HuffmanTree& HT, int n, int& s1, int& s2){
    int i;
    s1 = s2 = 0;
    int min1 = INT_MAX;  //最小值,INT_MAX在<limits.h>中定義的
    int min2 = INT_MAX;  //次小值

    for (i = 1; i <= n; ++i){
        if (HT[i].parent == 0){  //找沒有parent的最小和次小權值(下標)
            //只有兩種情況
            if (HT[i].weight < min1){
                min2 = min1;  //舊的最小權值賦給舊的次小權值
                s2 = s1;  //下標變動
                min1 = HT[i].weight;  //賦最小權值
                s1 = i;
            }
            else if ((HT[i].weight >= min1) && (HT[i].weight < min2)){
                min2 = HT[i].weight;  //賦次小權值
                s2 = i;
            }
        }
    }
}

void HuffmanCoding(HuffmanTree& HT, HuffmanCode& HC, int* w, int n){
    if (n <= 1) return;
    M = 2 * n - 1;
    HuffmanTree p;
    int i;
    HT = (HuffmanTree)malloc((M + 1)*sizeof(HTNode));  //初始化哈夫曼樹
    w++;
    for (p = HT + 1, i = 1; i <= n; ++i, ++p, ++w){  //前n個存葉子節點,賦權值,其他項賦零
        p->weight = *w;
        p->parent = 0;
        p->lchild = 0;
        p->rchild = 0;
    }
    for (; i <= M; ++i, ++p){  //后面存非葉子節點;
        p->weight = 0;
        p->parent = 0;
        p->lchild = 0;
        p->rchild = 0;
    }
    //建立哈夫曼樹
    int s1, s2;
    for (i = n + 1; i <= M; i++){
        Select(HT, i - 1, s1, s2);  //此函數在HT[1,i-1]中選擇父為零且w值最小的兩個元素,返回下標
        HT[s1].parent = i;  HT[s2].parent = i;
        HT[i].lchild = s1;  HT[i].rchild = s2;
        HT[i].weight = HT[s1].weight + HT[s2].weight;
    }
    Root_pos = (HT + i - 1);
}

/*前綴編碼的判斷*/
int IsPreCoding(char temp[][64]){
    int i, j, h, len1, len2;
    for (i = 1; i <= N - 1; ++i){   //循環比較N-1次
        for (j = i + 1; j <= N; ++j){  //j和i為需要比較的元素下標
            int flag = 0;  //假定不是前綴編碼
            for (h = 0; (temp[i][h] != '\0') && (temp[j][h] != '\0'); h++){
                if (temp[i][h] != temp[j][h]){  //循環中有不一樣的編碼位,說明是前綴編碼
                    flag = 1;
                }
            }
            if (flag == 0){  //如果循環結束flag還沒有變,說明當前的兩個不是前綴編碼,返回0
                return 0;
            }
        }
    }
    return 1;
}

//遞歸求WPL
void GetWPL(HuffmanTree HT, int Deep, HTNode* p){   //對葉子節點進行計算
    if ((p->lchild == 0) && (p->rchild == 0)){
        WPL += (p->weight) * Deep;
    }
    if (p->lchild != 0)
        GetWPL(HT, Deep + 1, HT + (p->lchild));
    if (p->rchild != 0)
        GetWPL(HT, Deep + 1, HT + (p->rchild));
}

int main(){
    /*處理哈夫曼樹*/
    cin >> N;
    char ch;
    int i;
    for (i = 1; i <= N; i++){
        cin >> ch >> w[i];
    }

    HuffmanTree HT;
    HuffmanCode HC, HCp;
    HuffmanCoding(HT, HC, w, N);
    /*得出權值最優路徑*/
    int Deep = 0;
    HTNode* p = Root_pos;
    WPL = 0;
    GetWPL(HT, Deep, p);
    /*處理編碼方案*/
    cin >> M;
    
    for (int j = 0; j < M; j++){  //M套編碼方案
        int wpl = 0;
        char temp[N + 2][64];  //注意開二維數組,下面每個循環用一維
        for (i = 1; i <= N; i++){  //每套編碼都是N個元素
            cin >> ch >> temp[i];
            wpl += (strlen(temp[i]) * w[i]);
        }
        if (wpl > WPL){  //判斷是否是最優編碼
            cout << "No" << endl;
        }else{
            if (IsPreCoding(temp)){  //判斷是否是前綴編碼
                cout << "Yes" << endl;
            }else{
                cout << "No" << endl;
            }
        }
    }
    
    getchar();
    return 0;
}

 

算法分析、遍歷生成樹分析圖等均為原創作品,歡迎指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM