1. 程式人生 > 實用技巧 >《趣學演算法》學習打卡Day 2

《趣學演算法》學習打卡Day 2

《趣學演算法》學習打卡:Day2

神祕電報密碼——哈夫曼編碼(哈夫曼樹)

哈夫曼編碼 :

(一):編碼儘可能的短:根據字元使用頻率不同(權值不同),當進行編碼儲存時,頻率高的編碼短,頻率低的編碼長(不定長編碼),從而節省儲存空間

注: 如果字元使用的頻率是相同的,固定長度編碼時空間效率最高的辦法

(二):消除二義性(字首碼特性);一個高頻字元(編碼短的)不能是其他低頻字元的字首。

例如: a=0,b=1,c=01,d=11 ,a是c的字首,b是d的字首,用來儲存時就會產生二義性

哈夫曼編碼的思路是以字元的使用頻率作為權來構建一棵哈夫曼樹,利用哈夫曼樹對字元進行編碼。

哈夫曼演算法採取的貪心策略是每次從樹的集合中取出沒有雙親且權值最小的兩棵樹作為左右子樹,構造一個新樹,新樹根結點的權值為其左右孩子結點的權值之和,將新樹插入到樹的集合中。

先來捋一捋思路再看程式碼吧,下面都是書上給的偽碼,一下子看有點串

首先我們清楚完成哈夫曼編碼是有兩個步驟的:

  1. 構建哈夫曼樹

關於資料:假設我們有n個字元,那麼我們最後的到的哈夫曼樹一共有多少個結點呢?答案是2*n-1個,恰好是每個字元中間插入一個結點來組成樹插入的結點數(n-1);

那麼怎麼去表示這些結點呢?這時候就需要設計一個結點結構體了,(見下文:節點結構體)

在設計好結構體以後,就只要簡單的初始化結點的資料和錄入n個字元及其權值就okk了;

在資料都準備好了的現在;按照哈夫曼貪心策略,選擇結點(有權值的)中權值最小的兩個結點,把他們的權值之和賦給下一個權值為空的結點的權值,把他們的ID按照左小右大計入左右子樹中(下標)。

重複上面的步驟到2*(n-1)中的權值不為0;

這裡應該要配圖的,不然太抽象了,ppt演示這種需要動態變化的一定會更直觀

假設n=6={a=5,b=32,c=18,d=7,e=25,f=13};則

哈夫曼結點陣列

i weight parent lchild rchild value
0 5 -1->6 -1 -1 a
1 32 -1 -1 -1 b
2 18 -1 -1 -1 c
3 7 -1->6 -1 -1 d
4 25 -1 -1 -1 e
5 13 -1 -1 -1 f
6 0—>12 -1->7 -1->0 -1->3
7 0->25 -1 -1->6 -1->5
8 0 -1 -1 -1
9 0 -1 -1 -1
10 0 -1 -1 -1

好了,這個畫圖有點浪費時間,下面的自己腦補一下吧

  1. 輸出哈夫曼編碼

ok,到這裡你應該知道,我們是怎樣構建哈夫曼樹的了,如果你還不會,那麼請仔細閱讀上面的部分。或者直接參考課本的相應章節。

重點在於怎麼藉助哈夫曼樹構建編碼

對於字母符的結點,他是一定有雙親的,我們從當前(字元)葉子結點出發,去找到雙親,判斷自己是雙親的左子樹還是右子樹,左子樹記為0,右子樹記為1,寫在編碼結構體的HCodeType.bit[]中(從後面往前面記,輸出的時候正著輸出就是這個字元的編碼);然後找雙親的雙親(祖父母??),判斷……,直到結點沒雙親。

  1. 資料結構:

結點結構體:

typedef struct
{
    double weight;//權值
    int parent;   //雙親
    int lchild;   //左孩子
    int rchild;   //右孩子
    char value;   //代表的字元
}HNodeType
weight parent lchild rchild value
0 -1 -1 -1

注:-1表示沒有**

編碼結構體:

typedef struct
{
    int bit[MAXBIT];//儲存編碼的陣列
    int start;      //編碼開始的下標
}HcodeType

2.初始化

結點初始化

for(int i=0;i<2*n-1;i++)
{
	HuffNode[i].weight=0;
	HuffNode[i].parent=-1;
	HuffNode[i].lchild=-1;
	HuffNode[i].rchild=-1;
}
/*輸入n個葉子結點的字元及權值*/
for(i=0;i<n;i++)
{
    cout<<"please input value and weight of leaf node"<<i+1<<endl;
    cin>>HuffNode[i].value>>HuffNode[i].weight;
}

輸入n個葉子結點的字元及權值

​ 3.迴圈構造Huffman樹

所有的結點都放在一個集合T中表示

s從樹集合中取出兩個權值最小的樹ti,tj,把他們合成一棵新樹zk,新樹的左兒子為ti,右孩子為tj,Zk的權值為ti和tj的權值之和。

int i,j,x1,x2;//x1、x2為最小權值結點的序號。
double m1,m2;//m1、m2為最小權值結點的權值。
for(i=0;i<n-1;i++)
{
    m1=m2=MAXVALUE;
    x1=x2=-1;
    for(j=0;j<n+i;j++)//為什麼不對字元的權值進行排序?效率那個更好?
    {
        if(HuffNode[j].weight<m1&&HuffNode[j].parent=-1){
			m2=m1;
            x2=x1;
            m1=HuffNode[j].weight;
            x1=j;
        }
        else if(HuffNode[j].weight<m2&&HuffNode[j].parent==-1){
			m2=HuffNode[j].weight;
            X2=j;
        }
   	}
    /*更新樹的資訊*/
    HuffNode[x1].parent=n+i;
    HuffNode[x2].parent=n+i;
    HuffNode[n+i].weight=m1+m2;
    HuffNOde[n+i].lchild=x1;
    HuffNode[n+i].rchild=x2;
}

​ 4.輸出哈夫曼編碼

void HuffmanCode(HCodeType Huffcode[MAXLEAF],int n)
{
    /*定義一個臨時變數來存放求解編碼時的資訊*/
	HCodeType cd;  
    int i,j,c,p;
    for(i=0;i<n;i++)  //恰好時字元數
    {
        cd.start=n-1;  //最後一個字元?
        c=i;       		//i為葉子結點的編號
        p=HuffNode[c].parent;//p為葉子結點雙親的編號
        /*while迴圈:從當前結點出發,一直遍歷到樹根,得到編碼陣列bit[]*/
        while(p!=-1)
        {
            /*如果i為雙親的左孩子***賦值0,為右孩子***賦值1*/
            if(HuffNode[p].lchild==c){
                cd.bit[cd.strat]=0;
            }
            else
                cd.bit[cd.strat]=1;
            /*哈夫曼編碼陣列向前移動,*/
            cd.strat--;
            /*c,p變數上移,準備下一迴圈*/
            c=p;
            p=HuffNode[c].parent;
		}
        /*把葉子結點的編碼資訊從臨時編碼cd中複製出來,放入編碼結構體陣列*/
        for(j=cd.strat+1,j<n;j++)
            HuffCode[i].bit[j]=cd.bit[j];
        HuffCode[i].strat=cd.strat;
	}
}
//畢竟這是別人寫的,對陣列的控制有沒有打註釋,想看仔細看清內部邏輯還是比較難的,也就是了解功能就ok了,不要太過於糾結細節,這一點我浪費了好多時間!

總結:

​ 還是犯二了,不該去糾結人家的程式碼的,像那種一段段的程式碼,知道其功能以後就該跳走的,浪費時間去研究人家for()迴圈的陣列下標控制,真的就是naocan。那些東西應該在自己寫程式碼的時候再去考慮的,那樣才會有整個程式碼全域性資料控制的視角,那樣才能迅速反應過來。

​ 可以明顯感覺到難度了,昨天是圖,今天是樹,明天是圖&樹;等明天的“溝通無限校園——最小生成樹”完了以後,後天就直接把最短路徑、哈夫曼、最小生成樹給實現了(可能要兩天)!1+1+1+2=5*20=100;剛剛好5天100頁;可以用兩天的時間來實現程式碼。

明天 後天 大後天
2020/8/29 2020/8/30 2020/9/1
完成最小生成樹的演算法思路理解 完成最短路徑和哈夫曼的實戰演練 完成最小生成樹的程式碼實現

我還是覺得,把書中的思路捋清楚就該跳過的,程式碼實現也許很重要,但那真的影響閱讀的積極性!