介绍
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 |
|
|
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个是时常出现重复的字符时,也可以比较好的压缩,还有是对于机器的硬件要求不太高。