介绍
LZW算法是非常常见的一种压缩算法,他的压缩原理是对于多次重复出现的字符串,进行压缩,至于怎么压缩,在后文中会细细描述,LZW算法可以用在很多的场合,诸如图像压缩,文本压缩等等,而且算法简单易懂,并不是人们想象中的那么深奥。
算法原理
在介绍算法原理之前,得先明白几个概念:
1、Prefix,在这里代表前缀字符的意思。
2、Suffix,对应的意思是后缀字符的意思。
为什么提到这2个概念呢,是因为后面的字符的压缩的输入的过程就与这2者相关。这里假设压缩的是文本字符,字符内容如下:
ababbabab
测试的数据未必是必须多的,上面的字符中还是存在着一些重复的字符段的,可以满足题目的要求的。好,下面是压缩的流程:
1、从左往右逐一的读取源文件中的字符,构成前缀,后缀字符词组的方式。
2、如果构成的词组没有被编码过,则进行编码,并且输出此时的前缀字符,然后后缀字符替代前缀字符,后缀字符继续从文件中读入。
3、如果构成的词组被编码过,就是说这个词组之前出现过,是重复的,则不输出,将对应于此时词组的编码赋给词组的前缀,然后继续读入后缀字符。
第几步 |
前缀 |
后缀 |
词 |
存在对应码 |
输出 |
码 |
1 |
a |
(,a) |
||||
2 |
a |
b |
(a,b) |
no |
a |
256 |
3 |
b |
a |
(b,a) |
no |
b |
257 |
4 |
a |
b |
(a,b) |
yes |
||
5 |
256 |
b |
(256,b) |
no |
256 |
258 |
6 |
b |
a |
(b,a) |
yes |
||
7 |
257 |
b |
(257,b) |
no |
257 |
259 |
8 |
b |
a |
(b,a) |
yes |
9 |
257 |
b |
(257,b) |
yes |
||
上述的最后一步是在输入结束之后,最后将(257,b)变为259后输出,所以最后的输出为:
a,b,256,257,259。
解压的时候过程正好相反,根据码表,做码制与字符的替换输出就行了,具体细节可以参照我的代码实现。
算法代码实现:
输入源文件srcFile.txt:
ababbabab
词组类WordFix.java:
package LZW; import java.util.HashMap; import java.util.Map; /** * 词组,包括前缀和后缀 * * @author lyq * */ public class WordFix { // 词组前缀 String prefix; // 词组后缀 String suffix; // 编码词组映射表 HashMap<WordFix, Integer> word2Code; public WordFix(String prefix, String suffix, HashMap<WordFix, Integer> word2Code) { this.prefix = prefix; this.suffix = suffix; this.word2Code = word2Code; } /** * 设置前缀 * * @param str */ public void setPrefix(String str) { this.prefix = str; } /** * 设置后缀 * * @param str */ public void setSuffix(String str) { this.suffix = str; } /** * 获取前缀字符 * * @return */ public String getPrefix() { return this.prefix; } /** * 判断2个词组是否相等,比较前后字符是否相等 * * @param wf * @return */ public boolean isSame(WordFix wf) { boolean isSamed = true; if (!this.prefix.equals(wf.prefix)) { isSamed = false; } if (!this.suffix.equals(wf.suffix)) { isSamed = false; } return isSamed; } /** * 判断此词组是否已经被编码 * * @return */ public boolean hasWordCode() { boolean isContained = false; WordFix wf = null; for (Map.Entry entry : word2Code.entrySet()) { wf = (WordFix) entry.getKey(); if (this.isSame(wf)) { isContained = true; break; } } return isContained; } /** * 词组进行编码 * * @param wordCode * 此词组将要被编码的值 */ public void wordFixCoded(int wordCode) { word2Code.put(this, wordCode); } /** * 读入后缀字符 * * @param str */ public void readSuffix(String str) { int code = 0; boolean isCoded = false; WordFix wf = null; for (Map.Entry entry : word2Code.entrySet()) { code = (int) entry.getValue(); wf = (WordFix) entry.getKey(); if (this.isSame(wf)) { isCoded = true; // 编码变为前缀 this.prefix = code + ""; break; } } if (!isCoded) { return; } this.suffix = str; } /** * 将词组转为连续的字符形式 * * @return */ public String transToStr() { int code = 0; String currentPrefix = this.prefix; for(Map.Entry entry: word2Code.entrySet()){ code = (int) entry.getValue(); //如果前缀字符还是编码,继续解析 if(currentPrefix.equals(code + "")){ currentPrefix =((WordFix) entry.getKey()).transToStr(); break; } } return currentPrefix + this.suffix; } }
压缩算法工具类LZWTool.java:
package LZW; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * LZW解压缩算法工具类 * * @author lyq * */ public class LZWTool { // 开始的编码的编码号从256开始 public static int LZW_CODED_NUM = 256; // 待压缩文件地址 private String srcFilePath; // 目标文件地址 private String desFileLoc; // 压缩后的目标文件名 private String desFileName; // 结果字符,将被写到输出文件中 private String resultStr; // 编码词组映射表 HashMap<WordFix, Integer> word2Code; // 源文件数据 private ArrayList<String> totalDatas; public LZWTool(String srcFilePath, String desFileLoc, String desFileName) { this.srcFilePath = srcFilePath; this.desFileLoc = desFileLoc; this.desFileName = desFileName; word2Code = new HashMap<>(); totalDatas = new ArrayList<>(); readDataFile(totalDatas); } /** * 从文件中读取数据 * * @param inputData * 输入数据容器 */ private void readDataFile(ArrayList<String> inputData) { File file = new File(srcFilePath); ArrayList<String[]> dataArray = new ArrayList<String[]>(); try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; String[] tempArray; while ((str = in.readLine()) != null) { tempArray = new String[str.length()]; for (int i = 0; i < str.length(); i++) { tempArray[i] = str.charAt(i) + ""; } dataArray.add(tempArray); } in.close(); } catch (IOException e) { e.getStackTrace(); } System.out.print("压缩前的字符:"); for (String[] array : dataArray) { for (String s : array) { inputData.add(s); System.out.print(s); } } System.out.println(); } /** * 进行lzw压缩 */ public void compress() { resultStr = ""; boolean existCoded = false; String prefix = totalDatas.get(0); WordFix wf = null; for (int i = 1; i < totalDatas.size(); i++) { wf = new WordFix(prefix, totalDatas.get(i), word2Code); existCoded = false; // 如果当前词组存在相应编码,则继续读入后缀 while (wf.hasWordCode()) { i++; // 如果到底了则跳出循环 if (i == totalDatas.size()) { // 说明还存在词组编码的 existCoded = true; wf.readSuffix(""); break; } wf.readSuffix(totalDatas.get(i)); } if (!existCoded) { // 对未编码过的词组进行编码 wf.wordFixCoded(LZW_CODED_NUM); LZW_CODED_NUM++; } // 将前缀输出 resultStr += wf.getPrefix() + ","; // 后缀边前缀 prefix = wf.suffix; } // 将原词组的后缀加入也就是新的词组的前缀 resultStr += prefix; System.out.println("压缩后的字符:" + resultStr); writeStringToFile(resultStr, desFileLoc + desFileName); } public void unCompress(String srcFilePath, String desFilePath) { String result = ""; int code = 0; File file = new File(srcFilePath); ArrayList<String[]> datas = new ArrayList<String[]>(); try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; String[] tempArray; while ((str = in.readLine()) != null) { tempArray = str.split(","); datas.add(tempArray); } in.close(); } catch (IOException e) { e.getStackTrace(); } for (String[] array : datas) { for (String s : array) { for (Map.Entry entry : word2Code.entrySet()) { code = (int) entry.getValue(); if (s.equals(code + "")) { s = ((WordFix) entry.getKey()).transToStr(); break; } } result += s; } } System.out.println("解压后的字符:" + result); writeStringToFile(result, desFilePath); } /** * 写字符串到目标文件中 * * @param resultStr */ public void writeStringToFile(String resultStr, String desFilePath) { try { File file = new File(desFilePath); PrintStream ps = new PrintStream(new FileOutputStream(file)); ps.println(resultStr);// 往文件里写入字符串 } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
测试调用类Client.java:
package LZW; /** * LZW解压缩算法 * @author lyq * */ public class Client { public static void main(String[] args){ //源文件地址 String srcFilePath = "C:\\Users\\lyq\\Desktop\\icon\\srcFile.txt"; //压缩后的文件名 String desFileName = "compressedFile.txt"; //压缩文件的位置 String desFileLoc = "C:\\Users\\lyq\\Desktop\\icon\\"; //解压后的文件名 String unCompressedFilePath = "C:\\Users\\lyq\\Desktop\\icon\\unCompressedFile.txt"; LZWTool tool = new LZWTool(srcFilePath, desFileLoc, desFileName); //压缩文件 tool.compress(); //解压文件 tool.unCompress(desFileLoc + desFileName, unCompressedFilePath); } }
结果输出:
压缩前的字符:ababbabab 压缩后的字符:a,b,256,257,259, 解压后的字符:ababbabab
在文件目录中的3个文件显示:
算法的遗漏点
算法整体不是很难,仔细去想一般都能找到压缩的方式,就是在解压的过程中药考虑到编码前缀解析掉之后,他的编码前缀还可能是一个编码所以需要递归的解析,在这个测试例子中你可能没有看见预想到的压缩效果,那时因为文本量实在太小,就几个字节,当测试的文本达到几十k的时候,并且捏造的数据中出现大量的重复字符串时,压缩的效果就会显现出来。
LZW算法的特点
LZW压缩算法对于可预测性不大的数据压缩的效果会比较好,还有1个是时常出现重复的字符时,也可以比较好的压缩,还有是对于机器的硬件要求不太高。