有的時候代碼裏不得不帶上一串長的字符數據表,原本就是小功能,將這種不大不小的數據外部存放顯得累贅,放源碼裏又礙眼又佔空間。
這時候數據適合的能夠經過設計精巧的結構簡化存儲的佔位,沒辦法簡化的可能會手工替換一下重複次數多的字符,但數量一大就沒辦法手工操做了,這時候應該用壓縮算法來幫助咱們。html
此次遇到數據相似這樣,只有三種字符:0
、1
、2
。算法
00000002000000000000000000000000000000000100000000000000000000001000010000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000010000000000000000100100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000100000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000001000010000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000010000000000000000001000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
整體長度實際上也不算多,大約上上面貼出來部分的十倍。因此選用的壓縮算法不用太複雜、也不能太耗時,能簡化存儲佔位就達到目的了。segmentfault
數據壓縮算法有蠻多,比較容易見到的像是Huffman編碼
、gzip
、ZigZag
、LZ
系列等,有一些適合文本壓縮有一些只適合文件。同時找了一些現成能夠壓縮的工具或者代碼簡單試了試Huffman編碼
、LZW
、LZ77
、LZ-string
、Gzip
,發現仍是LZW
在編碼後的長度與編碼代碼長度上最適合的,邊瞭解邊嘗試其實也花了一些時間,試好了代碼也差很少改出來了。工具
中間有考慮能夠分割固定位數轉化成對應字符,但因爲0太多,會轉出來不少非可見字符,因此仍是老老實實用壓縮算法。post
LZW
是Lempel-Ziv-Welch
的縮寫,最先是由Lempel
與Ziv
提出的,後來在1984年Terry Welch
提出了改進版。在如今常見的GIF
文件中就用到這種算法。更詳細的介紹可看維基百科。優化
LZW
其基本概念是將重複的數據以短符號替代,因此適合有大量重複字符出現的字符串,重複越多壓縮效率越好。缺點是對數據準確性要求很高,若是一個數據出現誤差,直接影響後面的數據解碼。
同時,若是壓縮的字符有不少獨立字符,這樣字典會變得愈來愈大,因此在有的算法中還會增長清除標誌,當字典到達預設大小時,會清除字典從新開始。像是GIF所使用的算法就有設置清除標誌,設置大小是2^12,超過則清除。編碼
幾個基本概念:dict
,字典,通常是ASCII表作默認字典cW
,當前讀取到的字符,每次只讀入一位pW
,上一次留存的字符,第一次讀取則爲空,多是一位也多是多位str
,爲pW
與cW
的字符拼接設計
一、一次只讀一個字符
二、每次讀取完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實現代碼:
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(""); }
咱們的數據只有三種字符:0
、1
、2
。因此原始的字典能夠沒必要那麼大,直接寫死便可
var dic = { 0: 0, 1: 1, 2: 2};
咱們編碼出的數據是這樣的:
0Āā02ĂąĆćĈā1ĉČĊĂċčđĒēĔēĄĕĘęĚěĜĝĞĝ1ĐğēĢģģĥđĐĨĦĬĭĮįİıęėIJČīĤĵĹĞķĔĥļİĿĺłĀĴŃņŇĽŁňĕŊćķōŋőŒœŔŕŀĀŐĆřŖŜŝŞĒŅşşšŠŢŦŧŨũŪūŬŭČŤIJśŮųŴđ
會發現這段編碼若是放在源碼中,第一眼感受會是亂碼,並且真正的數據是這十倍,使用的很是見字符更多,看上去更像是亂碼,萬一真的亂碼了也沒法一眼辨認出來,因此咱們應當再美化一下。
一開始想的是用英文大小寫+常見特殊字符,但發現這些徹底不夠編碼後的組合使用,因此乾脆直接選漢字做爲替代字符。
缺點就是,漢字實際佔位會比英文字符大一些,但兩害相權取其輕,爲了提高一點美觀度
,這點體積仍是能夠犧牲的。
源碼中使用的String.fromCharCode()
恰好就能夠將UTF16序列轉換成字符,因此無需特別麻煩的處理,只要選好漢字的起始位置便可。漢字區間是0x4E00
~0x9FA5
轉化成十進制是19968
~40869
區間。只需簡單的將索引idleCode
由256
改爲19968
便可。
var idleCode = 19968;
輸出以下:
0一丁02丂丅丆萬丈丁1三丌上丂下不醜丒專且專丄丕丘丙業叢東絲丞絲1丐丟專丟丣丣嚴醜丐丨並丬中丮丯豐丱丙丗串丌丫兩丵丹丞丷且嚴丼豐丿爲乂一臨乃乆乇麗乁麼丕乊萬丷乍之乑乒乓喬乕乀一樂丆乙乖乜九乞丒久也也鄉習乢書乧乨乩乪乫乬乭丌乤串乛乮乳乴醜
雖然也不太好看,但放代碼裏至少比以前順眼了些。
中文參雜數字也不是很舒服,因此把0
、1
、2
,也替換成漢字的零
、一
、二
。
var dic = { 0: '零', 1: '一', 2: '二'};
這時候要考慮一個問題,在不斷遞增的狀況中極可能遇到零
、一
、二
,這樣就與字典中的數據重合了,因此應當選一個比較大的區間。38646
是零
、19968
是一
、20108
是二
,因此在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(""); }