以 Huffman coding 為例看函數式編程

不同編程即為不同解決問題的思路

解決一個問題有很多思路,比如:

  1. 過程式(C語言):將解決問題的方式分解成若干步驟來控制數據。
  2. 面向對象式 (Java/Ruby):以對象為基本單位,定義其行為控制其內部狀態,通過不同對象之間的協作解決問題。
  3. 函數式(Lisp):一切皆為函數,用連貫的思維方式定義過程,常用遞歸。
  4. 組合式:將不同的解決方式組合起來,golang 經常會將面向對象與過程式組合起來。

示例: Huffman編碼

用 Lisp 的一個方言 Scheme 來實現:

輸入 '((A 1) (B 3) (C 2)), A B C 為帶編碼字符,1 2 3 為出現次數。
輸出 '((b 0) (a 0 1) c 1 1)

定義葉子節點

(define (make-leaf symbol weight)
  (list 'leaf symbol weight))

(define (leaf? object)
  (eq? (car object) 'leaf))

(define (symbol-leaf object)
  (cadr object))

(define (weight-leaf object)
  (caddr object))

如果沒有接觸過 lisp 的同學可能對上面的表示方式有點陌生,其實就是用括號代表方法調用,括號里的第一個位置是方法名稱,后面的是調用該方法的參數。

上面的幾行代碼定義了葉子節點 leaf 及相關函數。

定義樹節點
(define (make-code-tree left right)
  (list 
    left
    right
    (append (symbols left) (symbols right))
    (+ (weight left) (weight right))))

(define (left-branch tree) (car tree))

(define (right-branch tree) (cadr tree))

(define (symbols tree)
  (if (leaf? tree)
    (list (symbol-leaf tree))
    (caddr tree)))

(define (weight tree)
  (if (leaf? tree)
    (weight-leaf tree)
    (cadddr tree)))

同樣的道理,定義了用于在構造 Huffman 樹中非葉子的節點 tree 及其相關取值函數。

有序 list 構造方法
(define (adjoin-set x set)
  ;如果 set 為空,則返回以 x 作為唯一元素的 list
  (cond ((null? set) 
      (list x)) 
    ;如果 set 的第一個元素的 weight 大于 x 的 weight,則將 x 和 set 組合成一個新的 list 返回
    ((> (weight (car set)) (weight x))
      (cons x set)) 
    ; 否則將 set 的以第一個只取出,讓后遞歸調用 `adjoin-set`
    (else (cons (car set) (adjoin-set x (cdr set)))))) 
(define (make-leaf-set pairs)
  (if (null? pairs)
    '()
    (let ((pair (car pairs)))
      (adjoin-set (make-leaf (car pair) (cadr pair))
        (make-leaf-set (cdr pairs))))))

adjoin-set 的功能就是 x 插入到有序 list set 中,保證插入后的 list 仍然有序。lisp 中的 cond 可理解為 其他語言中的 switch,而 cons 可理解為將兩個元素結合成一個 list。 乍一看這個所謂“插入”元素的方法有點奇怪,而且沒有用任何臨時變量。其思路將整個插入的過程用遞歸調用的方式表示: 用過程(函數)代替了臨時變量。舉了例子:(adjoin-set 3 '(1 2)),執行順序是:

(cons 1 (adjoin-set 3 '(2)))
(cons 1 (cons 2 (adjoin-set 3 '())))
(cons 1 (cons 2 (cons 3 '())))
(cons 1 (cons 2 '(3)))
(cons 1 '(2 3))
'(1 2 3)

可以看到在執行序列中,推遲 cons 的執行,用參數求值壓棧從而省去了臨時變量。在 make-leaf-set 中思路也一樣:不斷地從 paris 中取元素,交給 adjoin-set 插入到 list 中。整個編寫過程中基本上用程序流暢地表達了我們的解題思路。

Huffman樹構造方法

在插入元素這種簡單的問題中函數式威力還遠遠沒有體現出來,請看下面構造 Huffman樹 的函數實現:

(define (make-tree leaves)
  (cond ((or (null? (car leaves)) (null? (cadr leaves)))
      (error "leaves is not enough"))
    ((null? (cddr leaves))
      (make-code-tree (car leaves) (cadr leaves)))
    (else (make-tree (adjoin-set (make-code-tree (car leaves) (cadr leaves)) (cddr leaves))))))

幾行代碼就將構造 Huffman樹 的核心邏輯表達清楚了:將按 weight 升序 leaves 的前兩個拿出來做成一個 tree node,adjoin-set 到剩下的 leaves 中,然后不斷重復這個操作,直到 leaves 中只剩下兩個元素,將這兩個元素最為 最終 Huffman樹 的左右子樹,然后返回。怎么樣?一氣呵成。

編碼

對 Huffman樹 遍歷編碼的實現也是精煉得有種思維的美感:
先進行左子樹遍歷,直到找到葉子節點,構造成結果 list 中一個元素,然后回到上一層遞歸,進入右子樹,不斷重復直到遍歷完所有節點。

(define (encode tree)
  (define (visit n bits)
    (if (leaf? n)
      ; 找到了一個葉子節點
      (cons (symbol-leaf n) bits)
      ; 用 cons 對 visit 的遞歸調用
      (cons (visit (left-branch n) (cons 0 bits))
        (visit (right-branch n) (cons 1 bits)))))
  (visit tree '()))

;測試
(define leaf-set (make-leaf-set '((A 1) (B 3) (C 2))))
(define tree (make-tree leaf-set))
(encode tree) ; outputs: ((b 0) (a 0 1) c 1 1)

詳細代碼請進 github

ps: 本篇用到部分《計算機程序的構造與解析》代碼。強烈建議大家學習 MIT 的這門公開課。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 3.4 說說相等和內部表示 在Lisp中主要有5種相等斷言,因為不是所有的對象被創建的時候都是相等意義上的相等。數...
    geoeee閱讀 1,867評論 0 6
  • Lisp的本質 - climbdream的個人空間 - 開源中國社區https://my.oschina.net/...
    葡萄喃喃囈語閱讀 714評論 0 10
  • 說明 函數式編程和面向對象編程可以說是編程的兩大宗教,猶如編輯器之爭一樣,之間口角不斷。我雖然靠著OOP的主力語言...
    lingyv閱讀 1,714評論 1 14
  • 第一部分Common Lisp介紹第1章 介紹一下Lisp你在學的時候覺得已經明白了,寫的時候更加確信了解了,教別...
    geoeee閱讀 3,007評論 5 8
  • 1.不過做什么事,尤其是一個重大的決定,一定要多問幾個為什么。最好把它寫下來,放到一個比較明顯的地方,時刻提醒自己...
    弘毅浪跡天涯閱讀 70評論 0 0