LZSS的筆記

  昨天看了下LZSS.C,就是那個4/6/1989 Haruhiko Okumura的經典代碼。

 

  很久沒有研究算法了,又沒有詳細的描述,只能從代碼和註釋裏面去理解。還真花了我不少時間。

 

  首先講解壓,LZSS的編碼是1 byte的flag,從低到高,如果bit=1,原樣輸出1 byte,如果bit=0,讀取2 byte,輸出長度和緩衝區位置保存在這2 byte中。

 

  其實標準的LZSS我還是第一碰到,以前碰到的多是輸出長度和回溯距離的組合。LZSS則多了一個緩衝區,一般大小N = 4096(0x1000),也就是12 bits,緩衝區位置佔掉了12 bits,那麼輸出長度就只能佔用4 bits。考慮到bit=0時至少要佔用2 bytes,所以輸出長度爲2時剛剛盈虧平衡,所以一般來說輸出長度是從3開始的。在代碼中THRESHOLD = 2,意思其實是長度必須大於2。這樣的話輸出長度的範圍就是3-18。代碼中F = 18,F就是最大的輸出長度。

 

  我碰到到是一個改版,N = 0x800,也就是11 bits,輸出長度變成了5 bits,THRESHOLD = 1,最後輸出長度的範圍是2-33。個人覺得THRESHOLD改成1實在是浪費了一個珍貴的輸出長度編碼。

 

  用了緩衝區和不用緩衝區的區別,我看就是多了一個字符串,就是緩衝區一開始填充的值。LZSS.C中默認填充的是空格,那麼大概是專門爲文本文件設計的。一般還是填充0比較多。具體怎麼回事下面再描述。

 

  緩衝區的大小N,一開始先填充N-F區域,然後一邊解壓,一邊循環的從N-F開始填充字符。一般來說開頭的字符很難形成重複,所以LZSS壓縮的特徵往往是FF xx xx xx ..(8 bytes) FF xx xx xx...(8 bytes),到後面出現大量重複了,就難以辨認了。如果一開頭是一段空格的話,那麼第一段就可以(pos = N-F-1, len = 8),這樣就可以輸出8個空格。如果不用緩衝區的話,開頭的8個空格就要變成(空格), (N-F, 7),算是節約了1個byte。

 

  下面講壓縮,其實最簡單的壓縮就是做一個for循環,從頭到尾一個個比較,最後保留匹配長度最大的那個。這樣的話複雜度是O(n*N*F),其中n是待壓縮文本的大小,F是最大輸出長度,這個值很小可以不管,N就是緩衝區大小或者回溯距離,只要n和N不是很大,速度都是秒的。

 

  LZSS.C中提供了一個優化的算法,其實整個代碼也就這段有看頭。首先定義了3個數組,lson, rson, dad組成了一顆二叉樹,lson, dad的大小都是N+1,要注意rson[N]/dad[N]其實並沒有被用到,N這個值在程序中被定義成NIL,故意多開一個只是爲了方便。這種爲了方便的情況出現很多,就不多說了。

 

  然後是rson在N+1的基礎上還多出了256,這是爲了存放1 byte的所有編碼,其實就是根。我覺得其實可以新開一個數組的,沒有必要用rson這個名字。rson其實用來保存大於等於的字符串,lson保存小於的字符串。dad就是保存dad。至於什麼叫做大於等於,用過strcmp總有體會吧,或者看看下面的說明。

 

  算法我用例子來說明吧,比如一段文字:

  --- 我 是 標 尺 ---

  1234 567 89012 345678

  abcd abc abcde abcdef

 

  注:爲了方便起見,rson['a']的含義其實是rson[N+1+'a'],文本位置從1開始,1其實是N-F

 

  1)讀取a,找到rson['a'],這個值初始爲NIL,那麼寫入位置1。

  2)讀取b-d,rson['b']-rson['d']等於2-4

  3.a)又讀取a,此時rson['a'] = 1, 那麼比較兩個字符串(從1開始的和從5開始的),比較下來長度=3,比較的最後一步是位置8的'a'-位置4的'd',cmp<0,所以此時檢查lson[1],lson[1]當然還是NIL,所以不再比較了,把lson[1]設置成4。意思就是說1和4,都是'a'開頭的字符串,4比1小。

  3.b)下面是輸出和補充字符,首先輸出(1, 3),至於怎麼寫flag和那2byte我就不講了。然後讀入3 bytes,考慮如果本算法已經執行了一段時間了,填充區已經填滿,那麼讀入的時候就佔用了先前的字符,那麼此時還要刪除先前字符的節點。讀的同時位置6開始的'bc'也要添加入樹中,不過就算匹配長度超過2,也不會輸出罷了。

  4)讀取位置8的'a',rson['a] = 1, 比較兩個字符串(位置8和位置1開始的字符串),最後結果是cmp = 'e'-'a' > 0,len = 4,檢查rson[1] = NIL,那麼寫入rson[1] = 8,後面的步驟和3.b一樣。

  5)讀取位置12的'e',rson['e'] = 12

  6)讀取位置13的'a',rson['a'] = 1,比較兩個字符串(位置13和位置1開始的字符串),最後結果是cmp = 'e'-'a' > 0,len = 4,檢查rson[1] = 8,那麼再比較這兩個字符串(位置13和位置8開始的字符串),最後結果是cmp = 'f' - 'a' > 0,len = 5,再檢查rson[8] = NIL,那麼rson[8] = 13。最後輸出的就是(8, 5)。

 

  解釋到這裏應該就很清楚了,這裏面有這樣一個關係,就是如果當前字符串比這個節點大,那麼只有往右支找纔有前途,反之亦然。這個很好想,比如說根是'abcde',左支是'abcaa',根和左支的相同字符數是3,如果當前字符串是'abz',和根的相同字符數是2,還小於3,那麼到左支去也就是平手,如果當前字符串是'abcdz',和根的相同字符數是4,那已經超過左支了。現在我是變化當前字符串,如果當前字符串不變,和根的相同字符數是N,根的左支所有的子節點和根的相同字符數如果大於N,也最多和根平手,如果小於N,就肯定輸了。去右支,雖然可能碰到'az'這種更差情況,但'abcdzaaa'這種更好情況也只可能在右支出現。所以說去右支纔有前途。

 

  最後還有個有意思的東西,就是在正式讀取待壓縮數據前,會將N-F前的F個字符加入到樹中,作用我剛剛提到過,在開頭有8個空格的情況下,可以節省1byte,此時的輸出時(pos = N-F-1, len = 8),那麼只要添加一個字符不就行了麼。其實考慮開頭不是空格的情況,一段代碼在中間部分出現了縮進(顯然很多人喜歡將tab轉成4個空格),於是依次出現了4個空格,8個空格,12個空格等等。一般的回溯距離+輸出長度的話,編碼會是這樣的(空格), (-1, 3),...,(-x1, 4), (-4, 4),...,(-x2, 8), (-4, 4),那麼標準的LZSS,且加入過F個空格的話,編碼就簡單多了:(N-F-4, 4),...,(N-F-8, 8), ..., (N-F-12, 12)

 

  算法中還有些刪除節點,邊界判斷之類的東西,這都是基礎的東西,不講了。這個優化算法的效率應該是O(n*logN*F)

 

6/10/2010 10:50:00 AM 初稿

 

2010年6月24日21:37:34 補充:

  關於效率有點問題,優化算法每一個字符都要添加進二叉樹中,所以效率是O(n*logN*F),但是用循環遍歷的不需要添加每一個字符,找到一個匹配串之後,指針就向後移動了。所以效率應該是O(r*n*N*F),新增的係數r接近壓縮率,但比壓縮率要小,這個值不妨認爲是0.1。那麼只有在logN < 0.1N的時候優化算法才划算。所以N應該大於2^6。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章