文章轉(zhuǎn)發(fā)自51CTO【ELT.ZIP】OpenHarmony啃論文俱樂(lè)部——《統(tǒng)計(jì)壓縮編碼機(jī)理分析》
1.技術(shù)DNA
2. 智慧場(chǎng)景
場(chǎng)景 | 技術(shù) | 開(kāi)源項(xiàng)目 |
自動(dòng)駕駛 / AR | 點(diǎn)云壓縮 | Draco/ 基于深度學(xué)習(xí)算法/PCL/OctNet |
語(yǔ)音信號(hào) | 稀疏快速傅里葉變換 | SFFT |
流視頻 | 有損視頻壓縮 | AV1/H.266編碼/H.266解碼/VP9 |
GPU 渲染 | 網(wǎng)格壓縮 | MeshOpt/Draco |
科學(xué)、云計(jì)算 | 動(dòng)態(tài)選擇壓縮算法框架 | Ares |
內(nèi)存縮減 | 無(wú)損壓縮 | LZ4 |
科學(xué)應(yīng)用 | 分層數(shù)據(jù)壓縮 | HCompress |
醫(yī)學(xué)圖像 | 醫(yī)學(xué)圖像壓縮 | DICOM |
數(shù)據(jù)庫(kù)服務(wù)器 | 無(wú)損通用壓縮 | Brotli |
人工智能圖像 | 人工智能圖像壓縮 | RAISR |
文本傳輸 | 短字符串壓縮 | AIMCS |
GAN媒體壓縮 | GAN 壓縮的在線多粒度蒸餾 | OMGD |
圖像壓縮 | 圖像壓縮 | OpenJPEG |
文件同步 | 文件傳輸壓縮 | rsync |
數(shù)據(jù)庫(kù)系統(tǒng) | 快速隨機(jī)訪問(wèn)字符串壓縮 | FSST |
通用數(shù)據(jù) | 高通量并行無(wú)損壓縮 | ndzip |
系統(tǒng)數(shù)據(jù)讀寫 | 增強(qiáng)只讀文件系統(tǒng) | EROFS |
3.開(kāi)篇簡(jiǎn)介
本文著重對(duì)傳統(tǒng)經(jīng)典壓縮算法的分析與理解,從認(rèn)識(shí)到實(shí)現(xiàn)的角度展開(kāi)描述,主要涉及了 Shannon-Fano、Huffman、算術(shù)編碼等編碼方案。除此之外,還附帶了對(duì)于數(shù)據(jù)壓縮初識(shí)的部分。
3.1 統(tǒng)計(jì)編碼是什么
統(tǒng)計(jì)編碼(statistical compression),也可稱為熵編碼,其出現(xiàn)是為了彌補(bǔ)傳統(tǒng)VLC可變長(zhǎng)編碼在編碼時(shí)須進(jìn)行特定方法匹配的痛點(diǎn),原因是VLC有時(shí)并非能找到最佳選擇,相較來(lái)說(shuō),統(tǒng)計(jì)編碼是一類只需依據(jù)每個(gè)字符出現(xiàn)的次數(shù) / 概率,便可自生成一套高效編碼的方案,正因如此,它們具備顯著的通用性。
統(tǒng)計(jì)編碼的首要目的是,在信息和碼之間找到明確的一一對(duì)應(yīng)關(guān)系,從而保證在解碼時(shí)準(zhǔn)確無(wú)誤地再現(xiàn)回來(lái),或極接近地找到相當(dāng)?shù)膶?duì)應(yīng)關(guān)系,同時(shí)將失真率控制在一定范圍內(nèi)。但無(wú)論借助什么途徑,核心總是要把平均碼長(zhǎng) / 碼率壓低到最低限度。
3.2統(tǒng)計(jì)編碼分類
四種常用的統(tǒng)計(jì)編碼有:香農(nóng)·范諾編碼、Huffman 編碼、算術(shù)編碼以及 ANS,其中,香農(nóng)·范諾編碼稱得上是現(xiàn)代第一個(gè)壓縮編碼,具有相當(dāng)?shù)臍v史意義。
4.香農(nóng)·范諾編碼
4.1誕生背景
早在香農(nóng)(Claude Elwood Shannon) 撰寫《通信的數(shù)學(xué)理論》一文,并試圖提出且證明一種可以按符號(hào)出現(xiàn)概率實(shí)現(xiàn)高效編碼,以最大程度減少通信傳輸所需信道容量的方法之前,時(shí)任 MIT 教授的羅伯特·范諾( Robert Mario Fano )也已對(duì)這一編碼方法展開(kāi)了相關(guān)研究。范諾不久后將其以技術(shù)報(bào)告的形式獨(dú)立進(jìn)行了發(fā)表,因而,這種編碼被并稱為香農(nóng)·范諾編碼( Shannon–Fano coding ),它是現(xiàn)代熵編碼與數(shù)據(jù)壓縮技術(shù)的雛形。即便它不是最佳的編碼方案,但在有些時(shí)候仍會(huì)使用。
4.2簡(jiǎn)單認(rèn)識(shí)
香農(nóng)·范諾編碼準(zhǔn)確的說(shuō)是一種前綴碼技術(shù),所謂前綴碼,是指編碼后的每個(gè)碼字都不會(huì)再作為其他碼字的前綴出現(xiàn),這為后續(xù)解碼操作時(shí)字符的唯一確定提供了條件。
以EBACBDBEBCDEAABEEBDDBABEBABCDBBADBCBECA這樣一串字符串為例,我們首先需要統(tǒng)計(jì)并計(jì)算其中每個(gè)字符的出現(xiàn)概率。
字符 | A | B | C | D | E |
計(jì)數(shù) | 7 | 14 | 5 | 6 | 7 |
概率 | 0.179 | 0.359 | 0.128 | 0.154 | 0.179 |
字符 | B | A | E | D | C |
計(jì)數(shù) | 14 | 7 | 7 | 6 | 5 |
概率 | 0.359 | 0.179 | 0.179 | 0.154 | 0.128 |
經(jīng)以上操作分組完畢后,五個(gè)字符已位于整棵樹(shù)的最外層葉子處,在每個(gè)分支處的左半部分樹(shù)干標(biāo)上 0,右半部分樹(shù)干標(biāo)上 1。最后,從樹(shù)根起始,沿樹(shù)干依次遍歷至最外層的葉子節(jié)點(diǎn),便得到了每個(gè)字符的香農(nóng)·范諾碼。由于每個(gè)樹(shù)干的 “0”、“1” 二進(jìn)制碼獨(dú)一無(wú)二,所以最終的編碼彼此不會(huì)重復(fù)。
符號(hào) | A | B | C | D | E |
計(jì)數(shù) | 7 | 14 | 5 | 6 | 7 |
編碼 | 1 | 0 | 111 | 110 | 10 |
出現(xiàn)概率較高的字符被編碼成兩位,概率較低的則被編碼成三位。由此,我們便可計(jì)算出每個(gè)字符平均所需的編碼位數(shù):
結(jié)果表明,每個(gè)字符平均只需約 2.28 個(gè)位即可保證在信息不丟失的情況下完美表示。當(dāng)然,實(shí)際在計(jì)算機(jī)中,是無(wú)法把位分割成小數(shù)的,2.28 需二次近似于 3。
然而迄今為止,仍沒(méi)有任何一種編碼方案能夠保證在通用情況下達(dá)到香農(nóng)熵值。香農(nóng)與范諾兩位杰出科學(xué)家為后世壓縮技術(shù)的發(fā)展開(kāi)了一個(gè)好頭。
5.哈夫曼編碼
香農(nóng)·范諾編碼固然強(qiáng)大,但它并非總是能產(chǎn)生最優(yōu)前綴碼,所以只能取得一定的壓縮效果,離真正實(shí)用的壓縮算法還相去甚遠(yuǎn)。
為此,在其基礎(chǔ)上演化出的第一個(gè)稱得上實(shí)用的壓縮編碼哈夫曼編碼( Huffman Code ),由大衛(wèi)·哈夫曼( David Albert Huffman )于 1952 年的博士論文《最小冗余度代碼的構(gòu)造方法( A Method for the Construction of Minimum Redundancy Codes )》中提出。哈夫曼編碼同樣依據(jù)字符使用的頻率來(lái)分配表示字符的碼字,不同的是,頻繁出現(xiàn)的字符被分配較短的編碼,出現(xiàn)不是那么頻繁的字符則會(huì)被分配較長(zhǎng)的編碼。
哈夫曼編碼效率高、運(yùn)算速度快、實(shí)現(xiàn)方式靈活。自 Windows10 起所支持的 CompactOS 特性,便是利用哈夫曼壓縮來(lái)減小操作系統(tǒng)體積的一項(xiàng)技術(shù)。直至今天,許多《數(shù)據(jù)結(jié)構(gòu)》教材在討論二叉樹(shù)時(shí)仍繞不開(kāi)哈夫曼這樣一個(gè)話題,不過(guò),比起算法本身,最為人們津津樂(lè)道的還是發(fā)明算法的過(guò)程。
5.1青出于藍(lán)而勝于藍(lán)
1951 年,哈夫曼作為一名 MIT 的學(xué)生,正在上一門由導(dǎo)師范諾教授的《信息學(xué)》課程。不過(guò),既然正式上了一門課,那期末考核是在所難免的。范諾出了道選擇題,給學(xué)生們兩種通過(guò)考核的方式:第一選項(xiàng)是夜以繼日地照常復(fù)習(xí),最后參與期末考試;第二選項(xiàng)是完成期末論文,也被叫做大作業(yè)。同學(xué)們普遍認(rèn)為,在 MIT 這樣一個(gè)地方,考試的難度可不是個(gè)小兒科,盡管如此,比起要求邏輯嚴(yán)謹(jǐn)、證明充分的學(xué)科論文來(lái)說(shuō),大多數(shù)同學(xué)還是更傾向于去考試。哈夫曼選擇了不隨波逐流,他認(rèn)為后者相對(duì)于他而言更簡(jiǎn)單,又能逃脫考試的浩劫,何樂(lè)而不為?
不出所料,最終只有哈夫曼一人選擇了獨(dú)自開(kāi)辟新路徑 —— 范諾限定了這樣一個(gè)課題:“給定一組字母、數(shù)字或其他各種符號(hào),設(shè)法找到其最有效的二進(jìn)制編碼”。實(shí)際上,這即是范諾與香農(nóng)等大科學(xué)家所正在研究的內(nèi)容,是信息論與數(shù)據(jù)壓縮領(lǐng)域尚未解決的難題,但他并未告訴學(xué)生們這一點(diǎn)。
結(jié)合所學(xué)知識(shí),哈夫曼知道“最有效”一詞的意思是“編碼長(zhǎng)度足夠短”。起初,哈夫曼認(rèn)為這個(gè)問(wèn)題應(yīng)該不是什么難事,漸漸地,他發(fā)現(xiàn)事情其實(shí)遠(yuǎn)并非他想得那樣。經(jīng)過(guò)幾個(gè)月的苦思冥想與文獻(xiàn)查找,哈夫曼確實(shí)設(shè)計(jì)出了許多算法,但令人沮喪的是,沒(méi)有一種算法可以被證明達(dá)到了“最有效”的條件…… 到了學(xué)期結(jié)束的前一周,仍舊沒(méi)有取得任何實(shí)質(zhì)性突破,哈夫曼開(kāi)始為之感到疲倦。迫于即將結(jié)課的壓力,他不得不撂掉手頭上這已不可能完成的任務(wù),回頭轉(zhuǎn)向?yàn)槌R?guī)考試的準(zhǔn)備。一天早餐后,就在哈夫曼隨手抓起桌上的研究筆記將其扔進(jìn)廢紙簍之時(shí),一切突然明朗了起來(lái),他說(shuō)那是他生命中最奇特的時(shí)刻。這樣一個(gè)困擾領(lǐng)域?qū)<以S久的難題,被一個(gè)年僅 25 歲的小伙子當(dāng)作課程作業(yè)給解決了。
哈夫曼后來(lái)回憶道,如果他知道他的老師和信息學(xué)之父彼時(shí)也都在努力解決這個(gè)問(wèn)題,他可能永遠(yuǎn)也不會(huì)想到去嘗試。他很慶幸自己在正確的時(shí)間做了正確的事,慶幸他的老師在那時(shí)沒(méi)有告訴他還有其他更優(yōu)秀的人也曾在這個(gè)問(wèn)題上苦苦掙扎。
5.2編碼方法
哈夫曼編碼是分組編碼、可變長(zhǎng)編碼,是依據(jù)各字符出現(xiàn)的概率構(gòu)造碼字的。制作碼表的基本原理是基于二叉樹(shù)的編碼思想:所有可能的輸入字符在哈夫曼樹(shù)上對(duì)應(yīng)為一個(gè)葉子節(jié)點(diǎn),節(jié)點(diǎn)的位置就是該字符的哈夫曼編碼。其次,基于字符串中每個(gè)字符的累計(jì)出現(xiàn)次數(shù)進(jìn)行編碼,出現(xiàn)頻率越高得到的編碼越短。特別的,為了構(gòu)造出唯一可譯碼,這些葉子節(jié)點(diǎn)都是哈夫曼樹(shù)上的終極節(jié)點(diǎn),不再延伸,不再出現(xiàn)前綴碼??梢愿惺艿?,哈夫曼編碼與香農(nóng)·范諾編碼的實(shí)現(xiàn)過(guò)程極其類似,但還是有些許不同,哈夫曼編碼的大體步驟如下:
-
將信源消息符號(hào)按其出現(xiàn)的概率大小依次排列
-
取兩個(gè)概率最小的字符分別配以 0 和 1 兩個(gè)碼元,并將這兩個(gè)概率相加作為一個(gè)新字符的概率,與未分配二進(jìn)制碼的字符一起重新排隊(duì)
-
對(duì)重排后的兩個(gè)概率最小的字符重復(fù)步驟 2 的過(guò)程
-
不斷重復(fù)上述過(guò)程,直到最后兩個(gè)字符被配以 0 和 1 為止
-
從最后一級(jí)開(kāi)始,向前返回得到各個(gè)信源符號(hào)所對(duì)應(yīng)的碼元序列,即相應(yīng)碼字
5.3舉個(gè)例子
讓我們淺試一下,現(xiàn)在有一串由 5 個(gè)不同字符 ( A, B, C, D, E ) 組成的字符串序列:
BACAB BACDA ABBBE
步驟一:根據(jù)上述字符串,統(tǒng)計(jì)各個(gè)字符出現(xiàn)的次數(shù)并排序:
字符 | B | A | C | D | E |
次數(shù) | 6 | 5 | 2 | 1 | 1 |
步驟二:把次數(shù)最少的兩者放在一起并相加,同時(shí)將結(jié)果按順序重新放入隊(duì)列。顯然,是 D: 1, E: 1, 1 + 1 = 2。
步驟三:繼續(xù)抽出兩個(gè)值最小的卡片,重復(fù)上一步并以此類推……
步驟四:現(xiàn)在,我們完成了步驟二的迭代,一棵二叉樹(shù)的模型自然形成了,下面要做的就是分別在每個(gè)分支的左樹(shù)干上標(biāo) 0、右樹(shù)干上標(biāo) 1。
步驟五:從樹(shù)根到每片葉子依次遍歷,將經(jīng)過(guò)的 0、1 記錄下來(lái),即可得到哈夫曼碼表。
字符 | B | A | C | D | E |
次數(shù) | 6 | 5 | 2 | 1 | 1 |
編碼 | 1 | 0 | 10 | 110 | 111 |
所以,原本的字符串BACABBACDAABBBE用哈夫曼碼表示為:100010001100010011000001110111,符合字符出現(xiàn)次數(shù)越多編碼長(zhǎng)度越短的標(biāo)準(zhǔn)。
5.4一些性質(zhì)
與香農(nóng)·范諾編碼相比,哈夫曼編碼的平均碼長(zhǎng)更小,編碼效率高,信息傳輸速率大。所以在壓縮信源信息率的實(shí)用設(shè)備中,哈夫曼編碼還是比較常用的。哈夫曼方法得到的碼并非唯一,不唯一的原因有兩點(diǎn):
-
每次對(duì)信源進(jìn)行壓縮時(shí),最后分配給兩個(gè)概率最小的字符以 0 和 1 可以是任意的,由此可以得到不同的哈夫曼碼,但不會(huì)影響碼字的長(zhǎng)度。
-
對(duì)信源進(jìn)行縮減時(shí),兩個(gè)概率最小的字符合并后的概率與其他信源字符的概率相等時(shí),它們?cè)趬嚎s信源中放置的前后相對(duì)次序可以是任意的,由此也會(huì)得到不同的哈夫曼碼。此時(shí)將影響碼字的長(zhǎng)度,一般將合并的概率放在上面,這樣可獲得較小的碼長(zhǎng)方差。
哈夫曼碼是用概率匹配方法進(jìn)行信源編碼。它有兩個(gè)明顯特點(diǎn):一是哈夫曼碼的編碼方法保證了概率大的符號(hào)對(duì)應(yīng)于短碼,概率小的符號(hào)對(duì)應(yīng)于長(zhǎng)碼,充分利用了短碼;二是壓縮信源的最后二個(gè)碼字總是最后一位不同,從而保證了哈夫曼碼是即時(shí)碼。
編碼平均長(zhǎng)度等式:
對(duì)于哈夫曼編碼的基本理論,我們差不多都清楚了,下面嘗試如何用代碼去實(shí)現(xiàn)它。
5.5算法實(shí)現(xiàn)
哈夫曼算法的模型基于二叉樹(shù),樹(shù)的節(jié)點(diǎn)分為終端節(jié)點(diǎn)(葉子節(jié)點(diǎn))與非終端節(jié)點(diǎn)(內(nèi)部節(jié)點(diǎn))。為了達(dá)成一個(gè)在二叉樹(shù)下更通用、標(biāo)準(zhǔn)的定義,我們將字符出現(xiàn)的頻率抽象為權(quán)重。初始第一輪迭代時(shí),每個(gè)最底層的節(jié)點(diǎn)都是葉子節(jié)點(diǎn),包含兩個(gè)字段:字符與權(quán)重;在第二輪及以后的迭代中,產(chǎn)生的每個(gè)節(jié)點(diǎn)都是內(nèi)部節(jié)點(diǎn),包含三個(gè)字段:權(quán)重、指向左子節(jié)點(diǎn)的鏈接與指向右子節(jié)點(diǎn)的鏈接。
因此,首先需要具備的兩個(gè)必要元素便是內(nèi)部節(jié)點(diǎn)與葉子節(jié)點(diǎn),同時(shí),它們又都包含權(quán)重這一相同字段,所以我們先定義基類 INode:
// C++實(shí)現(xiàn)
class INode
{
public:
const unsigned weight; // 權(quán)重
virtual ~INode() {}
protected:
INode(unsigned weight) : weight(weight) {}
};
為了避免不必要的干擾,將 INode 的構(gòu)造函數(shù)聲明為 protected。
葉子節(jié)點(diǎn)與內(nèi)部節(jié)點(diǎn)的定義即是繼承 INode 后,把剩下的另外字段補(bǔ)充上去,通過(guò)調(diào)用父類 INode 的構(gòu)造函數(shù)生成權(quán)值。
// 葉子節(jié)點(diǎn)
class LeafNode : public INode
{
public:
const char c; // 字符
LeafNode(unsigned weight, char c) : INode(weight), c(c) {}
};
內(nèi)部節(jié)點(diǎn)中指向左右子節(jié)點(diǎn)的鏈接毫無(wú)疑問(wèn)使用指針來(lái)實(shí)現(xiàn),且指向 INode 類型。自身權(quán)值則通過(guò)將左右子節(jié)點(diǎn)的權(quán)值相加得到。此外,還需顯式聲明一個(gè)析構(gòu)函數(shù),以便在后續(xù)操作中自動(dòng)釋放空間、防止野指針與內(nèi)存泄漏。
// 內(nèi)部節(jié)點(diǎn)
class InternalNode : public INode
{
public:
INode * const left; // 左指針
INode * const right; // 右指針
InternalNode(INode * leftChild, INode * rightChild) : INode(leftChild->weight + rightChild->weight), left(leftChild), right(rightChild) {}
~InternalNode()
{
delete left;
delete right;
}
};
基本元素現(xiàn)已齊全,繼續(xù)進(jìn)行下一步操作。上一小節(jié)我們說(shuō)到,靜態(tài) Huffman 算法需要對(duì)數(shù)據(jù)進(jìn)行兩次遍歷,第一次是得到概率表并構(gòu)建樹(shù),第二次才進(jìn)行字符編碼。先來(lái)看第一次,在構(gòu)建樹(shù)之前必須提供一套排好序的概率表,假設(shè)現(xiàn)在有這樣一串字符DATACOMPRESSION,我們?nèi)绾卧谟?jì)算機(jī)中用復(fù)雜度較低的算法統(tǒng)計(jì)并排序?肯定不能用眼睛盯著來(lái)數(shù)了。
因?yàn)榭傋址麛?shù)是一定的,所以用字符出現(xiàn)的次數(shù),即頻數(shù),來(lái)代替概率是等效的。統(tǒng)計(jì)頻數(shù)很簡(jiǎn)單 —— 聲明一個(gè)容量足夠保存任意字符的數(shù)組,將遍歷到的每個(gè)字符作為參數(shù)傳遞給這個(gè)數(shù)組,由于字符在現(xiàn)代計(jì)算機(jī)中均以 ASCII、Unicode 等編碼集存儲(chǔ),所以每當(dāng)遇到一個(gè)字符時(shí)就在數(shù)組中對(duì)應(yīng)字符編碼數(shù)值的位置遞增 1 即可,省去了記錄下標(biāo)的麻煩。
// 生成頻數(shù)表
#define CAPACITY 1<char * ptr = "DATACOMPRESSION"; // 聲明字符串DATACOMPRESSION
unsigned frequencies[CAPACITY] = {0}; // 聲明并初始化數(shù)組
while (*ptr != '') // 依次在每個(gè)字符對(duì)應(yīng)于數(shù)組的位置中遞增1
++frequencies[*ptr++];
經(jīng)過(guò)一番操作后,得到的數(shù)組狀態(tài)如下,下標(biāo)反映指針?biāo)肝恢茫?/span>
盡管浪費(fèi)了很多未被填充的空間,但這點(diǎn)數(shù)量級(jí)的浪費(fèi)實(shí)際上微不足道,況且填充的數(shù)據(jù)越多利用率也越高。
接下來(lái),采用優(yōu)先級(jí)隊(duì)列 priority_queue 數(shù)據(jù)結(jié)構(gòu)來(lái)構(gòu)建二叉樹(shù)是不二選擇,既滿足存儲(chǔ)節(jié)點(diǎn)序列,又可自動(dòng)排序,如此,事先也就不用再給頻數(shù)表排序了?,F(xiàn)在,封裝函數(shù) BuildTree,只需唯一形參 frequencies[CAPACITY]:
// 構(gòu)建二叉樹(shù)
INode* BuildTree(const unsigned (&frequencies)[CAPACITY])
{
struct NodeCmp // 聲明仿函數(shù)用于priority_queue排序
{
bool operator()(const INode * lhs, const INode * rhs) const { return lhs->weight > rhs->weight; }
};
priority_queue, NodeCmp> tree; // 得到對(duì)象tree*,>
for (unsigned i = 0; i < CAPACITY; ++i) // 構(gòu)造葉子節(jié)點(diǎn),返回地址到tree并排序
if (frequencies[i] != 0)
tree.push(new LeafNode(frequencies[i], static_cast(i)));
while (tree.size() > 1) // 不斷向上構(gòu)造內(nèi)部節(jié)點(diǎn),直至tree中只剩樹(shù)根
{
INode * leftChild = tree.top();
tree.pop();
INode * rightChild = tree.top();
tree.pop();
INode * parent = new InternalNode(leftChild, rightChild);
tree.push(parent);
}
return tree.top();
}
得到 priority_queue 的實(shí)例 tree 之后,便可開(kāi)始遍歷頻數(shù)表,將權(quán)值不為 0 的字符連同權(quán)值一起以葉子節(jié)點(diǎn)類型對(duì)象存進(jìn) tree,并會(huì)按權(quán)值遞增的順序排列。完畢后,循環(huán)依次取出隊(duì)頭前兩個(gè)最小的葉子節(jié)點(diǎn)記錄地址,生成上層內(nèi)部節(jié)點(diǎn)再入隊(duì)重新排序,最終返回樹(shù)根地址。
一切就緒,終于可以給字符編碼了!字符編碼兩要素 —— 字符與碼,一一對(duì)應(yīng),符合映射關(guān)系,用 vector 序列容器存儲(chǔ)碼、map 關(guān)聯(lián)容器存儲(chǔ)鍵值當(dāng)是再好不過(guò)了。仍用一個(gè)函數(shù)實(shí)現(xiàn)此功能,需要三個(gè)參數(shù):根節(jié)點(diǎn)地址、目的編碼、map 容器。在函數(shù)體中,借助 dynamic_cast 類型識(shí)別符判斷節(jié)點(diǎn)類型從而執(zhí)行不同語(yǔ)句。若為內(nèi)部節(jié)點(diǎn),則在每層通過(guò)之前構(gòu)建的二叉樹(shù)指針劃分為兩路,左路添 0 ,右路添 1,再分別遞歸調(diào)用本身而進(jìn)到下一層迭代;若為葉子節(jié)點(diǎn),則說(shuō)明已經(jīng)到達(dá)我們要編碼的字符處,于是插入<字符, 編碼>鍵值對(duì)到 map 中。
// 搜索二叉樹(shù)并編碼
using HuffCode = vector;
using HuffCodeMap = map;,>
void GenerateCodes(const INode * node, const HuffCode& prefix, HuffCodeMap& outCodes)
{
if (const InternalNode * in = dynamic_cast(node)) // 驗(yàn)證是否為內(nèi)部節(jié)點(diǎn)
{
// 劃分左路
HuffCode leftPrefix = prefix;
leftPrefix.push_back(false);
GenerateCodes(in->left, leftPrefix, outCodes);
// 劃分右路
HuffCode rightPrefix = prefix;
rightPrefix.push_back(true);
GenerateCodes(in->right, rightPrefix, outCodes);
}
else if (const LeafNode * lf = dynamic_cast(node)) // 驗(yàn)證是否為葉子節(jié)點(diǎn)
outCodes[lf->c] = prefix; // 插入鍵值對(duì)
}
至此,編碼的整體流程我們已經(jīng)基本實(shí)現(xiàn)了,接下來(lái)應(yīng)對(duì)其進(jìn)行測(cè)試、驗(yàn)證結(jié)果,用例如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CAPACITY 1<using namespace std;
using HuffCode = vector;
using HuffCodeMap = map;,>
class INode
{
public:
const unsigned weight;
virtual ~INode() {}
protected:
INode(unsigned weight) : weight(weight) {}
};
class InternalNode : public INode
{
public:
INode * const left;
INode * const right;
InternalNode(INode * leftChild, INode * rightChild) : INode(leftChild->weight + rightChild->weight), left(leftChild), right(rightChild) {}
~InternalNode()
{
delete left;
delete right;
}
};
class LeafNode : public INode
{
public:
const char c;
LeafNode(unsigned weight, char c) : INode(weight), c(c) {}
};
// 構(gòu)建樹(shù)
INode* BuildTree(const unsigned (&frequencies)[CAPACITY])
{
struct NodeCmp
{
bool operator()(const INode * lhs, const INode * rhs) const { return lhs->weight > rhs->weight; }
};
priority_queue, NodeCmp> tree;*,>
for (unsigned i = 0; i < CAPACITY; ++i)
if (frequencies[i] != 0)
tree.push(new LeafNode(frequencies[i], static_cast(i)));
while (tree.size() > 1)
{
INode * leftChild = tree.top();
tree.pop();
INode * rightChild = tree.top();
tree.pop();
INode * parent = new InternalNode(leftChild, rightChild);
tree.push(parent);
}
return tree.top();
}
// 搜索二叉樹(shù)并編碼
void GenerateCodes(const INode * node, const HuffCode& prefix, HuffCodeMap& outCodes)
{
if (const InternalNode * in = dynamic_cast(node))
{
HuffCode leftPrefix = prefix;
leftPrefix.push_back(false);
GenerateCodes(in->left, leftPrefix, outCodes);
HuffCode rightPrefix = prefix;
rightPrefix.push_back(true);
GenerateCodes(in->right, rightPrefix, outCodes);
}
else if (const LeafNode * lf = dynamic_cast(node))
outCodes[lf->c] = prefix;
}
int main()
{
char* SampleString = nullptr; // 聲明指向字符數(shù)組的指針
cout << "Input original string: ";
// 判定堆內(nèi)存分配成功與否及讀取輸入行
while ((SampleString = new char[CAPACITY]) && cin.getline(SampleString, CAPACITY))
{
// 編碼過(guò)程
cout << endl;
char * ptr = SampleString; // 創(chuàng)建地址副本
unsigned frequencies[CAPACITY] = {0}; // 初始化頻率表
while (*ptr != '') // 統(tǒng)計(jì)頻次
++frequencies[*ptr++];
INode * root = BuildTree(frequencies); // 得到對(duì)應(yīng)哈夫曼樹(shù)并返回根節(jié)點(diǎn)地址
HuffCodeMap codes;
GenerateCodes(root, HuffCode(), codes); // 為每個(gè)字符賦予哈夫曼碼
delete root;
// 遍歷map容器輸出不同字符與編碼
for (HuffCodeMap::const_iterator it = codes.begin(); it != codes.end(); ++it)
{
cout << it->first << ": ";
copy(it->second.begin(), it->second.end(), ostream_iterator(cout));
cout << endl;
}
cout << SampleString << ": ";
ptr = SampleString;
// 輸出字符串完整編碼
while (*ptr != '')
{
for (HuffCodeMap::const_iterator it = codes.begin(); it != codes.end(); ++it)
if (it->first == *ptr)
copy(it->second.begin(), it->second.end(), ostream_iterator(cout));
ptr++;
}
delete SampleString;
SampleString = nullptr;
// 解碼過(guò)程
char choice;
cout << endl << endl << "Decoding? (Y/N): ";
cin.get(choice);
// 判定是否繼續(xù)
if (toupper(choice) == 'Y')
{
char each; // 定義單字符
bool flag = true;
HuffCode total; // 定義bool向量
HuffCodeMap::const_iterator it = codes.begin(); // 創(chuàng)建初始迭代器
while (getchar() != '
')
continue;
cout << "Input encoded string: ";
// 獲取輸入行單個(gè)字符
while ((each = cin.get()) && each != '
')
{
each -= 48; // 轉(zhuǎn)換為數(shù)字表示
total.push_back(static_cast(each)); // 強(qiáng)轉(zhuǎn)為bool型壓入容器
// 依據(jù)編碼表反向匹配
while (it != codes.end())
{
if (total == it->second)
{
if (flag)
{
cout << "Original string: ";
flag = false;
}
cout << it->first; // 反向輸出字符
total.clear(); // 清空容器
}
++it;
}
it = codes.begin();
}
}
else
while (getchar() != '
')
continue;
cout << endl << string(60, '-') << endl << "Carry on, input next string: ";
}
cout << endl;
return 0;
}<>
初始時(shí),聲明一個(gè)指向字符數(shù)組的指針用于保存字符串,然后從堆中創(chuàng)建一塊 CAPACITY 大小的空間并獲取用戶輸入。編碼時(shí),需要注意的點(diǎn)是聲明頻率表時(shí)應(yīng)同時(shí)初始化為 0,避免最終頻次統(tǒng)計(jì)錯(cuò)誤。輸出單個(gè)字符編碼時(shí),通過(guò)相應(yīng)迭代器從頭至尾遍歷輸出每對(duì)鍵、值。若輸出完整編碼,將每個(gè)字符進(jìn)行一次比較匹配即可。解碼時(shí),用戶輸入的字符串為 01 長(zhǎng)序列,因而定義單字符以方便逐位比較。每讀取一位字符在 HuffCode 中嘗試一輪全匹配,成功即輸出,否則即進(jìn)入下一輪迭代。5.6動(dòng)態(tài)哈夫曼碼的設(shè)計(jì)
在此之前,我們一直所述的對(duì)象均為靜態(tài)哈夫曼編碼,靜態(tài)哈夫曼碼有個(gè)不太好的點(diǎn),你差不多注意到了 —— 傳統(tǒng)靜態(tài) Huffman 編碼需要對(duì)數(shù)據(jù)進(jìn)行兩次遍歷:第一次是構(gòu)造和傳輸 Huffman 樹(shù)到接收端,以收集消息中不同字符出現(xiàn)的頻率計(jì)數(shù);第二次再基于第一次構(gòu)造的靜態(tài)樹(shù)結(jié)構(gòu),編碼和傳輸消息本身。那么,這會(huì)導(dǎo)致在將其用于網(wǎng)絡(luò)通信時(shí)產(chǎn)生較大延遲,或者在文件壓縮應(yīng)用程序中產(chǎn)生額外的磁盤訪問(wèn)進(jìn)而減慢算法。
于是,動(dòng)態(tài)哈夫曼編碼誕生了。動(dòng)態(tài)哈夫曼編碼(Dynamic Huffman coding),又稱適應(yīng)性哈夫曼編碼(Adaptive Huffman coding),是基于哈夫曼編碼的自適應(yīng)編碼技術(shù)。它允許在符號(hào)正在傳輸時(shí)構(gòu)建代碼,允許一次編碼并適應(yīng)數(shù)據(jù)中變化的條件,即隨著數(shù)據(jù)流的到達(dá),動(dòng)態(tài)地收集和更新符號(hào)的概率(頻率)。一次編碼的好處是使得源程序可以實(shí)時(shí)編碼,但由于單個(gè)丟失會(huì)損壞整個(gè)代碼,因此它對(duì)傳輸錯(cuò)誤更加敏感。
所以,F(xiàn)aller 和 Gallager 兩人各提出了一種單次遍歷方案,后被 Knuth 大大改進(jìn),用于構(gòu)造動(dòng)態(tài) Huffman 編碼。發(fā)送器用來(lái)編碼消息中第 t+1 個(gè)字符的二叉樹(shù)(同時(shí)也是接收器用來(lái)重建第 t+1 個(gè)字符的二叉樹(shù))是消息前 t 個(gè)字符的二叉樹(shù)。如此,發(fā)送器和接收器就都會(huì)從相同的初始樹(shù)開(kāi)始,發(fā)送器永遠(yuǎn)不需要將樹(shù)發(fā)送給接收器。很顯然,這與靜態(tài) Huffman 算法的情況不同。
不久,又有研究者設(shè)計(jì)并證明了一種于所有單遍方案中,在最壞情況下表現(xiàn)仍然是最優(yōu)的算法 A,它可以用于網(wǎng)絡(luò)通信的通用編碼方案,也可以作為基于文字的壓縮算法中的一種高效子例程。
算法 A 具備以下優(yōu)點(diǎn):
-
對(duì)于編碼效率差異相對(duì)較大的小消息,每個(gè)字母占用更少的位
-
在 t 小于 104 時(shí),相比所有“兩遍算法”都表現(xiàn)得更好
-
能對(duì)消息進(jìn)行實(shí)時(shí)編解碼,每個(gè)字符使用不到一個(gè)額外的比特位對(duì)消息編碼
-
在文件壓縮、網(wǎng)絡(luò)通信和硬件實(shí)現(xiàn)方面有巨大應(yīng)用潛力
-
可用來(lái)增強(qiáng)其他壓縮方案
< 未完待續(xù)……>
ELT.ZIP是誰(shuí)?
ELT<=>Elite(精英),.ZIP為壓縮格式,ELT.ZIP即壓縮精英。
成員:
上海工程技術(shù)大學(xué)大二在校生閆旭
合肥師范學(xué)院大二在校生楚一凡
清華大學(xué)大二在校生趙宏博
成都信息工程大學(xué)大一在校生高云帆
黑龍江大學(xué)大一在校生高鴻萱
山東大學(xué)大三在校生張智騰
ELT.ZIP是來(lái)自6個(gè)地方的同學(xué),在OpenHarmony成長(zhǎng)計(jì)劃啃論文俱樂(lè)部里,與來(lái)自華為、軟通動(dòng)力、潤(rùn)和軟件、拓維信息、深開(kāi)鴻等公司的高手一起,學(xué)習(xí)、研究、切磋操作系統(tǒng)技術(shù)...
寫在最后
OpenHarmony 成長(zhǎng)計(jì)劃—“啃論文俱樂(lè)部”(以下簡(jiǎn)稱“啃論文俱樂(lè)部”)是在 2022年 1 月 11 日的一次日?;顒?dòng)中誕生的。截至 3 月 31 日,啃論文俱樂(lè)部已有 87 名師生和企業(yè)導(dǎo)師參與,目前共有十二個(gè)技術(shù)方向并行探索,每個(gè)方向都有專業(yè)的技術(shù)老師帶領(lǐng)同學(xué)們通過(guò)啃綜述論文制定技術(shù)地圖,按“降龍十八掌”的學(xué)習(xí)方法編排技術(shù)開(kāi)發(fā)內(nèi)容,并通過(guò)專業(yè)推廣培養(yǎng)高校開(kāi)發(fā)者成為軟件技術(shù)學(xué)術(shù)級(jí)人才。
啃論文俱樂(lè)部的宗旨是希望同學(xué)們?cè)陂_(kāi)源活動(dòng)中得到軟件技術(shù)能力提升、得到技術(shù)寫作能力提升、得到講解技術(shù)能力提升。大學(xué)一年級(jí)新生〇門檻參與,已有俱樂(lè)部來(lái)自多所高校的大一同學(xué)寫出高居榜首的技術(shù)文章。
如今,搜索“啃論文”,人們不禁想到、而且看到的都是我們——OpenHarmony 成長(zhǎng)計(jì)劃—“啃論文俱樂(lè)部”的產(chǎn)出。
OpenHarmony開(kāi)源與開(kāi)發(fā)者成長(zhǎng)計(jì)劃—“啃論文俱樂(lè)部”學(xué)習(xí)資料合集
1)入門資料:啃論文可以有怎樣的體驗(yàn)
https://docs.qq.com/slide/DY0RXWElBTVlHaXhi?u=4e311e072cbf4f93968e09c44294987d
2)操作辦法:怎么從啃論文到開(kāi)源提交以及深度技術(shù)文章輸出https://docs.qq.com/slide/DY05kbGtsYVFmcUhU
3)企業(yè)/學(xué)校/老師/學(xué)生為什么要參與 & 啃論文俱樂(lè)部的運(yùn)營(yíng)辦法https://docs.qq.com/slide/DY2JkS2ZEb2FWckhq
4)往期啃論文俱樂(lè)部同學(xué)分享會(huì)精彩回顧:
同學(xué)分享會(huì)No1.成長(zhǎng)計(jì)劃啃論文分享會(huì)紀(jì)要(2022/02/18)https://docs.qq.com/doc/DY2RZZmVNU2hTQlFY
同學(xué)分享會(huì)No.2 成長(zhǎng)計(jì)劃啃論文分享會(huì)紀(jì)要(2022/03/11)https://docs.qq.com/doc/DUkJ5c2NRd2FRZkhF
同學(xué)們分享會(huì)No.3 成長(zhǎng)計(jì)劃啃論文分享會(huì)紀(jì)要(2022/03/25)
https://docs.qq.com/doc/DUm5pUEF3ck1VcG92?u=4e311e072cbf4f93968e09c44294987d
現(xiàn)在,你是不是也熱血沸騰,摩拳擦掌地準(zhǔn)備加入這個(gè)俱樂(lè)部呢?當(dāng)然歡迎啦!啃論文俱樂(lè)部向任何對(duì)開(kāi)源技術(shù)感興趣的大學(xué)生開(kāi)發(fā)者敞開(kāi)大門。
掃碼添加 OpenHarmony 高校小助手,加入“啃論文俱樂(lè)部”微信群
后續(xù),我們會(huì)在服務(wù)中心公眾號(hào)陸續(xù)分享一些 OpenHarmony 開(kāi)源與開(kāi)發(fā)者成長(zhǎng)計(jì)劃—“啃論文俱樂(lè)部”學(xué)習(xí)心得體會(huì)和總結(jié)資料。記得呼朋引伴來(lái)看哦。
原文標(biāo)題:統(tǒng)計(jì)壓縮編碼機(jī)理分析(上篇)
文章出處:【微信公眾號(hào):開(kāi)源技術(shù)服務(wù)中心】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
開(kāi)源技術(shù)
+關(guān)注
關(guān)注
0文章
389瀏覽量
7914 -
OpenHarmony
+關(guān)注
關(guān)注
25文章
3660瀏覽量
16157
原文標(biāo)題:統(tǒng)計(jì)壓縮編碼機(jī)理分析(上篇)
文章出處:【微信號(hào):開(kāi)源技術(shù)服務(wù)中心,微信公眾號(hào):共熵服務(wù)中心】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論