LZW算法壓縮字符串數據

有的時候代碼裏不得不帶上一串長的字符數據表,原本就是小功能,將這種不大不小的數據外部存放顯得累贅,放源碼裏又礙眼又佔空間。
這時候數據適合的能夠經過設計精巧的結構簡化存儲的佔位,沒辦法簡化的可能會手工替換一下重複次數多的字符,但數量一大就沒辦法手工操做了,這時候應該用壓縮算法來幫助咱們。html

選型

此次遇到數據相似這樣,只有三種字符:012算法

00000002000000000000000000000000000000000100000000000000000000001000010000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000010000000000000000100100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000100000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000001000010000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000010000000000000000001000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

整體長度實際上也不算多,大約上上面貼出來部分的十倍。因此選用的壓縮算法不用太複雜、也不能太耗時,能簡化存儲佔位就達到目的了。segmentfault

數據壓縮算法有蠻多,比較容易見到的像是Huffman編碼gzipZigZagLZ系列等,有一些適合文本壓縮有一些只適合文件。同時找了一些現成能夠壓縮的工具或者代碼簡單試了試Huffman編碼LZWLZ77LZ-stringGzip,發現仍是LZW在編碼後的長度與編碼代碼長度上最適合的,邊瞭解邊嘗試其實也花了一些時間,試好了代碼也差很少改出來了。工具

中間有考慮能夠分割固定位數轉化成對應字符,但因爲0太多,會轉出來不少非可見字符,因此仍是老老實實用壓縮算法。post

LZW 簡介

LZWLempel-Ziv-Welch的縮寫,最先是由LempelZiv提出的,後來在1984年Terry Welch提出了改進版。在如今常見的GIF文件中就用到這種算法。更詳細的介紹可看維基百科。優化

LZW其基本概念是將重複的數據以短符號替代,因此適合有大量重複字符出現的字符串,重複越多壓縮效率越好。缺點是對數據準確性要求很高,若是一個數據出現誤差,直接影響後面的數據解碼。
同時,若是壓縮的字符有不少獨立字符,這樣字典會變得愈來愈大,因此在有的算法中還會增長清除標誌,當字典到達預設大小時,會清除字典從新開始。像是GIF所使用的算法就有設置清除標誌,設置大小是2^12,超過則清除。編碼

LZW 算法

幾個基本概念:
dict,字典,通常是ASCII表作默認字典
cW,當前讀取到的字符,每次只讀入一位
pW,上一次留存的字符,第一次讀取則爲空,多是一位也多是多位
str,爲pWcW的字符拼接設計

編碼規則:

一、一次只讀一個字符
二、每次讀取完cW拼接在pW以後,造成一個新的字符串爲str
三、查詢dict裏是否有這個新字符串str
3.no、若是沒有,則將這個str存入dict中,並以一個新的字符做爲代指。並將cW的值存入pW中。
3.yes、若是有則將這個str存入pW中,等待與下一次的cW拼接成新的字符串
四、這樣循環直到結束,而後輸出所有代指的字符做爲編碼後的字符。code

解碼規則:

一、一次只讀取一個字符。
二、由於第一次編碼的pW爲空,因此解碼的第一個cW字符確定是默認dict裏存在的字符,咱們能夠直接解碼輸出。而且將cW的值存入pW中,以此爲解碼開端。
三、讀取第二個cW時,與前一個pW拼接,造成新的字符串爲str,判斷dict中是否存在,若是不存在則將這個組合存入dict中。再判斷將要解碼的字符是否存在dict中。
3.yes,若是dict中有,就讀取出對應字符。
3.no,若是dict中沒有。這時候咱們應該想到編碼的過程,遇到字典中存在的str時,咱們會暫存住與下一次cW拼接成新串,直到dict中沒有再存入。
因此遇到未知字符必然是咱們將要寫入字典的那一位字符,因此字典中確定已經存了這個字符的一部分字典已存字符+一位cW字符,並且這個字典已存在字符恰是上一次保存的字符串,一位cW字符則是這個字符串的開始字符,這樣咱們就能還原出這個字符並寫入字典中了。
四、不斷循環這個解碼過程,直到結束htm

JavaScript實現

解釋起來比較繁瑣,結合代碼看會更容易理解,網上的JavaScript實現代碼:

function compress(s){
    var dic = {};
    for(var i = 0; i < 256; i++){
        var c = String.fromCharCode(i);
        dic[c] = c;
    }
    var prefix = "";
    var suffix = "";
    var idleCode = 256;
    var result = [];
    for(var i = 0; i < s.length; i++){
        var c = s.charAt(i);
        suffix = prefix + c;
        if(dic.hasOwnProperty(suffix)){
            prefix = suffix;
        } else {
            dic[suffix] = String.fromCharCode(idleCode);
            idleCode++;
            result.push(dic[prefix]);
            prefix = "" + c;
        }
    }
    if(prefix !== ""){
        result.push(dic[prefix]);
    }
    return result.join("");
}

function uncompress(s){
    var dic = {};
    for(var i = 0; i < 256; i++){
        var c = String.fromCharCode(i);
        dic[c] = c;
    }
    var prefix = "";
    var suffix = "";
    var idleCode = 256;
    var result = [];
    for(var i = 0; i < s.length; i++){
        var c = s.charAt(i);
        if(dic.hasOwnProperty(c)){
            suffix = dic[c];    
        } else if(c.charCodeAt(0) === idleCode){
            suffix = suffix + suffix.charAt(0);
        } else {

        }
        if(prefix !== ""){
            dic[String.fromCharCode(idleCode)] = prefix + suffix.charAt(0);
            idleCode++;
        }
        result.push(suffix);
        prefix = suffix;
    }
    return result.join("");
}

適應性優化

簡化初始字典

咱們的數據只有三種字符:012。因此原始的字典能夠沒必要那麼大,直接寫死便可

var dic = { 0: 0, 1: 1, 2: 2};
修改輸出字符

咱們編碼出的數據是這樣的:

0Āā02ĂąĆćĈā1ĉČĊĂċčđĒēĔēĄĕĘęĚěĜĝĞĝ1ĐğēĢģģĥđĐĨĦĬĭĮįİıęėIJČīĤĵĹĞķĔĥļİĿĺłĀĴŃņŇĽŁňĕŊćķōŋőŒœŔŕŀĀŐĆřŖŜŝŞĒŅşşšŠŢŦŧŨũŪūŬŭČŤIJśŮųŴđ

會發現這段編碼若是放在源碼中,第一眼感受會是亂碼,並且真正的數據是這十倍,使用的很是見字符更多,看上去更像是亂碼,萬一真的亂碼了也沒法一眼辨認出來,因此咱們應當再美化一下。

一開始想的是用英文大小寫+常見特殊字符,但發現這些徹底不夠編碼後的組合使用,因此乾脆直接選漢字做爲替代字符。
缺點就是,漢字實際佔位會比英文字符大一些,但兩害相權取其輕,爲了提高一點美觀度,這點體積仍是能夠犧牲的。

源碼中使用的String.fromCharCode()恰好就能夠將UTF16序列轉換成字符,因此無需特別麻煩的處理,只要選好漢字的起始位置便可。漢字區間是0x4E00~0x9FA5轉化成十進制是19968~40869區間。只需簡單的將索引idleCode256改爲19968便可。

var idleCode = 19968;

輸出以下:

0一丁02丂丅丆萬丈丁1三丌上丂下不醜丒專且專丄丕丘丙業叢東絲丞絲1丐丟專丟丣丣嚴醜丐丨並丬中丮丯豐丱丙丗串丌丫兩丵丹丞丷且嚴丼豐丿爲乂一臨乃乆乇麗乁麼丕乊萬丷乍之乑乒乓喬乕乀一樂丆乙乖乜九乞丒久也也鄉習乢書乧乨乩乪乫乬乭丌乤串乛乮乳乴醜

雖然也不太好看,但放代碼裏至少比以前順眼了些。
中文參雜數字也不是很舒服,因此把012,也替換成漢字的

var dic = { 0: '零', 1: '一', 2: '二'};

這時候要考慮一個問題,在不斷遞增的狀況中極可能遇到,這樣就與字典中的數據重合了,因此應當選一個比較大的區間。386461996820108,因此在20108以後與38646以後的區間都是知足現有須要編碼的數據組合,以後再選一個漢字筆劃相對少的區間,簡單的嘗試了一下,最終選取25165

var idleCode = 25165;

來看看效果:

零才扎零二扏扒打扔払扎一扖扙扗扏託扚扞扟扠扡扠撲扢扥扦執扨擴捫掃捫一扝揚扠扯擾擾扲扞扝扵扳批扺扻扼扽找扦扤承扙扸扱抂抆掃抄扡扲抉扽抌抇抏才抁抐抓抔把抎投扢抗扔抄撫折択摶摳掄搶抍才抝打抦抣抩抪披扟抒擡擡抮抭抯抳抴抵抶抷抸抹抺扙抱承抨抻拀拁扞

比以前編碼的會更舒服一些,在大量數據兩種編碼效果會更明顯一點。

解碼也很簡單,作相應的替換就行,將字典替換成

var dic = { '零': '0', '一': '1', '二': '2'};

字典的值必須是字符串,由於代碼中用到了String下的方法。

idleCode也改爲25165爲起始值。

var idleCode = 25165;

這樣就能順利解碼了。

完整代碼

最後咱們整理一下代碼,完整代碼以下:

function LZW_compress(text){
    const dict = { 0: '零', 1: '一', 2: '二' }, result = []
    let temp = "", UTFCode = 25165 // 漢字筆畫較少的區間開始
    text.split("").reduce((prev, cur)=>{
        const string = prev + cur
        if(dict[string]) temp = string;
        else{
            dict[string] = String.fromCharCode(UTFCode++);
            result.push(dict[prev]);
            temp = cur.toString();
        }
        return temp
    }, "")
    if(temp) result.push(dict[temp]);
    return result.join("");
}


function LZW_uncompress(text){
    const dict = { "零": "0", "一": "1", "二": "2" }, result = []
    let UTFCode = 25165;
    text.split("").reduce((prev, cur)=>{
        let string = ""
        if(dict[cur]) string = dict[cur]
        else string = prev + prev.charAt(0)
        if(prev) dict[String.fromCharCode(UTFCode++)] = prev + string.charAt(0);
        result.push(string);
        return string
    }, "")
    return result.join("");
}

參考

相關文章
相關標籤/搜索