貪心算法——Huffman 壓縮編碼的實現

1. 如何理解 「貪心算法」

假設咱們有一個能夠容納 100 Kg 物品的揹包,能夠裝各類物品。咱們有如下 5 種豆子,每種豆子的總量和總價值都各不相同。怎樣裝才能讓揹包裏豆子的總價值最大呢?算法

這個問題其實很簡單,咱們只須要計算出每種豆子的單價,而後按照單價從高到低依次來裝就行了。單價從高到低排列爲:黑豆、綠豆、紅豆、青豆和黃豆,所以咱們往揹包裏裝 20 Kg 黑豆、30 Kg 綠豆和 50 Kg 紅豆。數據結構

實質上,這就是貪心算法的思想,用貪心算法解決問題的步驟通常是這樣的。編碼

第一步,當咱們看到這類問題的時候,首先要聯想到貪心算法。針對一組數據,咱們定義了限制值和指望值,但願選出幾個數據,在知足限制值的狀況下,指望值最大。在剛纔的問題中,限制值就是重量不超過 100 Kg,指望值就是豆子總價值。翻譯

第二步,咱們嘗試看下這個問題是否能夠用貪心算法解決。每次選擇當前狀況下,在對限制值同等貢獻的狀況下,對指望值貢獻最大的數據。上例中,就是選取單價最高的豆子,也就是重量相等狀況下對總價值貢獻最大的豆子。3d

第三步,咱們舉幾個例子看下貪心算法產生的結果是不是最優的。大部分狀況下,舉幾個例子驗證一下就能夠了,嚴格證實貪心算法的正確性,須要涉及比較多的數學推理,很是複雜。從時間角度來看,大部分能用貪心算法解決的問題,其正確性都是顯而易見的,也不須要嚴格的證實。cdn

實際上,貪心算法解決問題的思路,並不老是能給出最優解。blog

在下面的有權圖中,咱們須要找到一條從頂點 S 出發到頂點 T 的最短路徑,使得路徑中邊的權重和最小。貪心算法的思路是每次選擇一條和當前頂點鏈接的權重最小的邊,最終答案是 S->A->E->T,權重和爲 9。排序

可是,最優解其實是 S->B->D->T,權重和爲 6。在這個問題上,貪心算法不工做的緣由主要是,前面的選擇會影響後面的選擇。一旦第一步選擇了頂點 S 到頂點 A,第二步咱們就和頂點 B、C 無關了。即便第一步最優,可是若由於這個選擇後面的選擇都很糟糕,那整體上也就不會取得最優解了。隊列

2. 貪心算法實戰分析

2.1. 分糖果

假設咱們有 m 個糖果要分給 n 個孩子,由於糖果少孩子多(m<n),因此糖果只能分給一部分孩子。每一個糖果的大小不等,每一個孩子對糖果的需求也不一樣,只有糖果的大小大於等於孩子對糖果的需求時,孩子才能獲得知足。如何分配糖果,才能儘量地知足最多數量的孩子呢?get

對於一個孩子來講,若是小的糖果能夠知足,咱們就不必用更大的糖果,這樣更大的糖果就能夠用來知足需求更大的孩子。另外一方面,對糖果需求小的孩子更容易知足,所以咱們能夠從需求小的孩子開始分配糖果,由於知足一個需求小的孩子和知足一個需求大的孩子,對咱們結果的貢獻是同樣的。

因此,咱們就能夠從剩下的孩子中,找出一個需求最小的,而後發給他剩餘糖果中能知足他需求的最小的糖果。這樣的分配方案,最後就能知足最多數量的孩子。

2.2. 錢幣找零

假設咱們有面值分別爲 1 元、2 元、5 元、10 元、20 元、50 元、100 元的鈔票若干,如今要用這些錢來支付 K 元,最少要用多少張紙幣呢?

在生活中,咱們確定首先用面值最大的來支付,若是不夠,咱們繼續用更小一點面值的,以此類推,直到最後知足爲止。在貢獻相同指望值(紙幣數量)的狀況下,咱們確定但願多貢獻點金額,這樣就可讓紙幣數更少,這就是一種貪心的思想。

2.3. 區間覆蓋

假設咱們有 n 個區間,區間的起始端點分別爲 [l1, r1],[l2, r2],[l3, r3],……,[ln, rn]。咱們從這 n 個區間中選出一部分區間,這部分區間知足兩兩不相交(端點相交不算),最多能選出多少個區間呢。

這個問題的解決思路是這樣的,假設這 n 個區間的最左端點是 lmin,最右端點是 rmax。那麼這個問題就至關於,咱們選擇幾個不相交的區間,從左到右將 [lmin, rmax] 覆蓋上。咱們按照起始端點從小到大的順序對這 n 個區間進行排序,每次選擇的時候,左端點和前面已經覆蓋的區間不重合而右端點又儘可能小的區間,就能讓剩下的未覆蓋區間儘可能大,從而就能夠放置更多的區間。

這實際上就是一種貪心的選擇方法,並且這種處理思想在任務調度、教師排課等問題中都有用到。

3. Huffman 壓縮編碼

假設有一個包含 1000 個字符的文件,每一個字符佔 1 個字節 8 位,那麼存儲這個文件就須要 8000 bits。若是經過統計分析咱們發現這 1000 個字符只包含 6 個不一樣的字符,假定它們爲 a, b, c, d, e, f。那咱們只用 3 個二進制位就能夠表示 8 個不一樣的字符,因此存儲 1000 個字符就只須要 3000 bits 了,比原來省了不少空間。那還有沒有更加節省空間的存儲方式呢?

霍夫曼編碼就要登場了。霍夫曼編碼是一種很是有效的編碼方式,普遍用於數據壓縮中,其壓縮率一般在 20% - 90% 之間。霍夫曼編碼不只會考察文本中有多少個不一樣字符,還會考察每一個字符出現的頻率,根據頻率的不一樣,選擇不一樣長度的編碼。根據貪心的思想,咱們能夠把出現頻率比較多的字符,用稍微短一點的編碼,而對出現頻率比較少的字符用稍微長一些的編碼。

對於等長的編碼來講,咱們解壓縮起來很簡單,每次從文本中讀取固定長度的二進制碼,而後翻譯成對應字符便可。可是,霍夫曼編碼是不等長的,咱們每次應該讀取多少位的二進制來進行解碼呢?爲了不解碼過程出現歧義,霍夫曼編碼要求各個字符的編碼之間,不會出現某個編碼是另外一個編碼前綴的狀況。

假設這 6 個字符出現的頻率從高到低依次是:a、b、c、d、e、f,咱們就把它編碼成下面這個樣子,任何一個字符的編碼都不是另外一個的前綴。在解壓縮的時候,咱們每次會讀取儘量長的可解碼的二進制串,因此也不會出現歧義。這種編碼方式,存儲 1000 個字符就只須要 2100 bits 了.

那霍夫曼編碼是如何根據字符出現頻率的不一樣,給不一樣的字符進行不一樣長度的編碼的呢?

咱們把每一個字符看做一個節點,而且附帶着把頻率放到優先級隊列中。而後,從隊列中取出頻率最小的兩個子節點 A、B,新建一個節點 C,使其頻率爲 A、B 兩個節點的頻率之和,並把這個新節點 C 做爲 A、B 節點的父節點。最後,再把 C 節點放入到優先級隊列中,重複這個過程,直到隊列中沒有數據爲止。

如今,咱們給每一條邊畫一個權值,指向左子節點的邊通通標記爲 0,指向右子節點的邊通通標記爲 1,那麼從根節點到葉節點的路徑就是葉節點對應字符的霍夫曼編碼。

參考資料-極客時間專欄《數據結構與算法之美》

獲取更多精彩,請關注「seniusen」!

相關文章
相關標籤/搜索