使用F#來實現哈夫曼編碼吧

最近算法課要求實現哈夫曼編碼,因爲前面的問題都是使用了F#來解決,偶然換成C#也十分古怪,報告也很差看,風格差太多。一開始是打算把C#版本的哈夫曼編碼換用F#來寫,結果寫到一半就以爲日了狗了。。。畢竟FP水平圖樣,處處mutable,各類<-...因而想看看有沒有現成的F#實現的哈夫曼編碼。node

F#的算法實現這種東西自己很差找,不過M$彷佛有着預見性,得來全不費功夫。。。算法

原文express

open System

/// 哈夫曼編碼使用了一個葉子節點爲輸入符號,
/// 內部節點是他們全部符號組合的指望頻率的
/// 二叉樹。
type HuffmanTree = 
    | Leaf of char * float
    | Node of float * HuffmanTree * HuffmanTree

/// 爲包含給定符號的字符串和指望的頻率提供編碼和解碼
type HuffmanCoder(symbols: seq<char>, frequencies : seq<float>) =
   
    /// 從輸入的頻率構建一個哈夫曼編碼樹
    let huffmanTreeLeafs =    
        Seq.zip symbols frequencies
        |> Seq.toList
        |> List.map Leaf
        
    /// 用於從哈夫曼編碼樹的節點獲取頻率
    let frequency node =
        match node with
        | Leaf(_,p) -> p
        | Node(p,_,_) -> p    

    /// 從根節點列表構建一個哈夫曼編碼樹,遍歷它直到惟一根節點
    let rec buildCodeTree roots = 
        match roots |> List.sortBy frequency with
        | [] -> failwith "Cannot build a Huffman Tree for no inputs" 
        | [node] -> node
        | least::nextLeast::rest -> 
                   let combinedFrequency = frequency least + frequency nextLeast
                   let newNode = Node(combinedFrequency, least, nextLeast)
                   buildCodeTree (newNode::rest)
               
    let tree = buildCodeTree huffmanTreeLeafs
     
    /// 爲哈夫曼編碼樹的全部葉子構建哈夫曼編碼表
    let huffmanCodeTable = 
        let rec huffmanCodes tree = 
            match tree with
            | Leaf (c,_) -> [(c, [])]
            | Node (_, left, right) -> 
                let leftCodes = huffmanCodes left |> List.map (fun (c, code) -> (c, true::code))
                let rightCodes = huffmanCodes right |> List.map (fun (c, code) -> (c, false::code))
                List.append leftCodes rightCodes
        huffmanCodes tree 
        |> List.map (fun (c,code) -> (c,List.toArray code))
        |> Map.ofList

    /// 使用哈夫曼編碼表編碼字符串
    let encode (str:string) = 
        let encodeChar c = 
            match huffmanCodeTable |> Map.tryFind c with
            | Some bits -> bits
            | None -> failwith "No frequency information provided for character '%A'" c
        str.ToCharArray()
        |> Array.map encodeChar
        |> Array.concat
       
    /// 使用哈夫曼編碼樹將一個二進制數組解碼爲字符串
    let decode bits =
        let rec decodeInner bitsLeft treeNode result = 
            match bitsLeft, treeNode with
            | [] , Node (_,_,_) -> failwith "Bits provided did not form a complete word"
            | [] , Leaf (c,_) ->  (c:: result) |> List.rev |> List.toArray
            | _  , Leaf (c,_) -> decodeInner bitsLeft tree (c::result)
            | b::rest , Node (_,l,r)  -> if b
                                         then decodeInner rest l result
                                         else decodeInner rest r result
        let bitsList = Array.toList bits
        new String (decodeInner bitsList tree [])
                 
    member coder.Encode source = encode source
    member coder.Decode source = decode source

模式匹配

模式匹配是F#中至關基本而且很是強大的特性。使用模式匹配可讓代碼在清晰地表達其行爲的同時更加簡潔。上方所陳的每個函數都使用到了模式匹配——亦是大量的F#典型代碼。編程

簡單說來,好比在huffmanCodes裏,模式匹配使得其能夠在可能出現的聯合數據結構中輕鬆切換:數組

match tree with
| Leaf (c,_) -> //...
| Node (_, left, right) -> //...

更多複雜的例子(好比上面的decodeInner)不難發現模式匹配有助於引導你的代碼。你例舉了每個你知道如何處理的情形,而且你所匹配的葉子節點暗示了數據須要在那種情形下定義。而後編譯器將會熱情地告訴你你有哪些沒有覆蓋到的情形。當我一開始撰寫這個函數的時候,我就沒有考慮到第一種狀況,而後編譯器告訴我數據結構

Warning: Incomplete pattern matches on this expression. The value '([],Node (_, _, _))' will not be matched

很明顯對了!這個特定的輸入指示了用戶可能提供非法輸入!app

管道

管道是一個用來描述聲明一系列輸入執行操做的不錯的方式。這種哲學思想相似於在命令行裏的管道——將左邊的輸入做爲參數傳遞給右邊。ide

由於F#庫提供了一票很好的基本類型的操做用於處理你的數據,因此很容易經過管道描述這一系列的轉換操做。例如,您能夠很容易地聲明篩選、映射、摺疊、壓縮或是從新包裝數據。函數

集合

代碼使用了4種經常使用 F#/.NET 集合:測試

  • F# 列表:不可變鏈表,在一個用到了列表的遞歸算法裏用到了。
  • F# 映射:不可變字典,用於存儲每一個符號。
  • F# 序列 = .NET "IEnumerable":基本集合的接口,用於輸入。
  • .NET 數組:基本類型的數組用於輸出編碼。

值得注意的是,在集合間切換很是容易,使用諸如List.toArrayMap.ofList的函數就能輕鬆搞定。

「這是.NET的一部分!」又曰「華而不實」

當我寫這段代碼的時候,我開啓了實驗模式,我僅僅在表層寫了一些小的函數而後用F# Interactive來執行。當我以爲這個函數工做正常便想將全部的功能使用一個類來包裝起來,而後給出一個漂亮的.NET接口,我就是這樣作的:

  1. 把全部的內容拍到一個級別
  2. 把代碼包裹起來:

    type HuffmanCoder(symbols : seq<char>, frequencies : seq<float>) = 
    
        // 要包裹的代碼...
    
        member coder.Encode source = encode source
        member coder.Decode source = decode source

真是不能低估這神奇的能力!在F#裏,從實驗編碼到零件設計編碼的過渡簡單而平穩。之因此F#能夠這麼來,是由於其是一個混合了函數式和麪向對象的語言而且巧妙地被集成進了.NET。而且正是由於這個緣故,使用F#能夠輕鬆構建大型.NET系統的組件。我據說有人對F#中的函數式面向對象批評曰其「華而不實」,可是我很是喜歡它,由於不論何時它都能讓我縱享絲滑。:-)

若是你感興趣,這是C#的寫法:

public class HuffmanCoder
{
    public HuffmanCoder(IEnumerable<char> symbols, IEnumerable<double> frequencies);

    public string Decode(bool[] source);
    public bool[] Encode(string source);
}

嗯!

我以爲這段代碼真是一個絕佳的例子讓我表達了在某些事情上我更偏向於F#。算法代碼的特性與F#的語法和編程風格更加契合。你能夠先只寫一大堆簡單、可組合的函數,每一個函數就那麼幾行的模式匹配和管道運算。當你這麼作,你就可使用F# Interactive來測試算法和代碼,把它們變成正確函數。當他們工做時,你能夠平穩地將它們整合進.NET組件而且提供給其它.NET組件。

相關文章
相關標籤/搜索