無論是在我們的開發項目中,還是在我們的日常生活中,都會較多的涉及到文件壓縮。談到文件壓縮,可能會有人想問文件壓縮到底是怎麼實現的,實現的原理是什麼,對於開發人員來說,怎麼實現這樣一個壓縮的功能。
接下來,我們就來了解一下文件壓縮的相關知識。文件壓縮是如何實現的?這個我們就得了解一下數據結構,因為文件在壓縮的過程中會轉化為數據流,那麼如何將數據流進行對應的壓縮,這個問題就得靠算法來實現。那麼文件壓縮的算法是什麼呢?那就是HuffmanTree。
提到HuffmanTree,我們就需要順道講講數據結構和算法。在計算機中,數據結構和算法可以說是程序的靈魂。
數據結構:是相互之間存在一種或多種特定關系的數據元素的集合。按照視點的不同,我們將數據結構分為邏輯結構和物理結構。
(1).邏輯結構:是指數據對象中數據元素之間的相互關系。邏輯結構包含:集合結構(集合結構中的數據元素除了同屬於一個集合外,他們之間沒有其他關系);線性結構(線性結構中的數據元素之間是一對一的關系);樹形結構(樹形結構的數據元素之間存在一種一對多的層次關系);圖形結構(圖形結構的數據元素是多對多的關系)。
(2).物理結構:是指數據的邏輯結構在計算機中的存儲形式。物理結構包含:順序存儲結構(是把數據元素存放在地址連續的存儲單元裡,其數據間的邏輯關系和物理關系是一致的);鏈式存儲結構(是指把數據元素存放在任意的存儲單元裡,這組存儲單元可以是連續的,也可以是不連續的)
上面介紹了一下數據結構的分類,當然,說到HuffmanTree,那就需要提一下“樹形結構”。
樹:是N(N大於或等於0)個節點的有限集合。現在介紹一下樹的三種表示法:
(1).雙親表示法(在每個節點中,附設一個指示器指示雙親節點到鏈表中的位置);
(2).孩子表示法(每個節點有多個指針域,其中每個指針指向一個棵樹的根節點,我們把這種方法叫做鏈表表示法);
(3).孩子兄弟表示法(任意一棵樹,它的第一個孩子如果存在就是唯一的,它的友兄弟如果存在也是唯一的,因此,我們設置兩個指針,分別指向該節點的第一個孩子和此節點的又兄弟)。
上面提到樹,現在介紹一下二叉樹。
二叉樹:是N(N大於或等於0)個節點的有限集合,該集合或者為空,或者有一個根節點和兩棵互不相交的、分別稱為根節點的左子樹和右子樹的二叉樹組成。
接下來介紹一下幾種特殊的二叉樹:
(1).斜樹:所有的節點都只有在左子樹的二叉樹叫做左斜樹。所有節點都是只有右子樹叫做右斜樹。
(2).滿二叉樹:在一棵二叉樹中,如果所有分支節點都存在左子樹和右子樹,並且所有葉子都在同一層上,這樣的二叉樹成為滿二叉樹。
(3).完全二叉樹:對一棵具有N個節點的二叉樹按層序編號,如果編號為I(1大於或等於I小於或等於N)的節點與同樣深度的滿二叉樹中編號為I的節點在二叉樹中位置完全相同,則這棵二叉樹成為完全二叉樹。
前面我首先介紹了數據結構的定義和分類,接著介紹了樹,二叉樹。最後讓我們一起來具體的了解一下HuffmanTree。
從樹中的一個節點到另一個節點之間的分支構成兩個節點之間的路徑,路徑上的分支數目稱做路徑長度。樹的路徑長度就是從樹根到每一個節點的路徑長度之和。節點的帶權的路徑長度為從該節點到跟之間的路徑長度與節點上權的乘積。樹的帶權路徑長度為樹中所有葉子節點的帶權路徑長度之和。
HuffmanTree:帶權路徑長度WPL最小的二叉樹稱做赫夫曼樹。(又稱做:最優二叉樹)
赫夫曼編碼:規定赫夫曼樹的左分支代表0,又分支代表1,則從根節點到葉子節點所經過的路徑分支組成的0和1的序列便為該節點對應字符的編碼。
以上介紹了 HuffmanTree的相關概念知識,現在就需要將這個數據結構采用代碼實現,接下來提供一段采用C#代碼實現的 HuffmanTree。
1.位流的基類:
/// <summary> /// 位流的基類。 /// </summary> /// <remarks> /// 一個字節流轉換到一個位流的規則實現之間。 /// </remarks> public abstract class BitStream { /// <summary> /// 在數據流上快速的獲取最大位數 /// </summary> public abstract int MaxReadAhead { get; set; } /// <summary> /// 從流中讀取位。 /// </summary> /// <param name="count">讀取的比特數。</param> /// <returns>位為UInt32。</returns> public abstract uint Read(int count); /// <summary> /// 在流上查詢數據 /// </summary> /// <param name="count">查詢的位數。</param> /// <returns>位為UInt32。</returns> /// <remarks>此方法不消耗位(即移動文件指針)。</remarks> public abstract uint Peek(int count); /// <summary> /// 從流中消耗比特,而不返回它們。 /// </summary> /// <param name="count">消耗的比特數。</param> public abstract void Consume(int count); }
2.哈夫曼樹的實現:
/// <summary> ///哈夫曼樹的實現。 /// </summary> /// <remarks> /// 創建一個查找表,將采取任何位序列(最大樹深度的長度),指示輸出符號。在WIM文件,在實踐中,沒有一塊超過32768字節 ///長度,所以我們經常會產生一個更大的查找表比它的數據編碼。這使得異常快速符號查找O(1),但效率較低整體。 /// </remarks> public sealed class HuffmanTree { // 每個符號的最大位 private readonly int _numBits; // 最大符號 private readonly int _numSymbols; private readonly uint[] _buffer; public HuffmanTree(uint[] lengths) { Lengths = lengths; _numSymbols = lengths.Length; uint maxLength = 0; for (var i = 0; i < Lengths.Length; ++i) { if (Lengths[i] > maxLength) { maxLength = Lengths[i]; } } _numBits = (int)maxLength; _buffer = new uint[1 << _numBits]; Build(); } public uint[] Lengths { get; set; } public uint NextSymbol(BitStream bitStream) { var symbol = _buffer[bitStream.Peek(_numBits)]; // 我們可能在讀,復位比特流的位置 bitStream.Consume((int)Lengths[symbol]); return symbol; } private void Build() { var position = 0; //對於每一位長度… for (var i = 1; i <= _numBits; ++i) { // 檢查每個符號 for (uint symbol = 0; symbol < _numSymbols; ++symbol) { if (Lengths[symbol] != i) continue; var numToFill = 1 << (_numBits - i); for (var n = 0; n < numToFill; ++n) { _buffer[position + n] = symbol; } position += numToFill; } } for (var i = position; i < _buffer.Length; ++i) { _buffer[i] = uint.MaxValue; } } }
赫夫曼樹和赫夫曼編碼對於帶權路徑的二叉樹做了一些了解,用於初步理解壓縮原理。對於數據結構的理解,需要我們花費很多的時間,也需要我們在這些數據結構中做一個細致的分類。