LZW算法原理及实现
原理
LZW的想在数据中创建一个短语词典。如果在此后的编码过程中又遇到了相同的字典,则用相应的索引号替代,而不是短语本身。
由于LZW字典产生的规则固定,所以不需要额外传递字典;解码端可以采取逆过程重建出来字典并进行解码。
编码
编码的流程大致如下:
步骤1:将词典初始化为包含所有可能的单字符,当前前缀P初始化为空。
步骤2:当前字符C=字符流中的下一个字符。
步骤3:判断P+C是否在词典中
-
如果“是”,则用C扩展P,即让P=P+C,返回到步骤2。
-
如果“否”,则
输出与当前前缀P相对应的码字W;
将P+C添加到词典中;
令P=C,并返回到步骤2
解码
解码的流程大致如下:
步骤1:在开始译码时词典包含所有可能的前缀根。
步骤2:令CW:=码字流中的第一个码字。
步骤3:输出当前缀-符串CW到码字流。
步骤4:先前码字PW=当前码字CW。
步骤5:当前码字CW=码字流的下一个码字。
步骤6:判断当前缀-符串CW 是否在词典中。
-
如果”是”,则把当前缀-符串CW输出到字符流。
- 当前前缀P=PW。
- 当前字符C=当前前缀字符串的第一个字符。
- 把P+C添加到词典。
-
如果”否”,则当前前缀P=PW。
- 当前字符C=当前字符串CW的第一个字符。
- 输出P+C到字符流,然后把它添加到词典中。
实现
位输入输出工具
因为索引号大于等于255,所以有时候1字节不够的,而是用int型数据又会造成大量字节的浪费,所以要根据索引的最大值合理选择存储所需要的位数,这就要用到按位进行输入输出的工具了。
struct bitWriter
{
char buffer;
int pos, bits;
ofstream &out;
bitWriter( ofstream &out ): out(out) { buffer = pos = bits = 0;}
void WriteBit( int bit )
{
if( pos == 8 )
{
out.write( &buffer, 1 );
pos = 0; buffer = 0;
}
buffer |= ( bit << pos);
pos ++; bits ++;
}
void Write( int value, int len )
{
while( len -- )
{
WriteBit( value & 1 );
value >>= 1;
}
}
void EndWrite()
{
out.write(&buffer,1);
}
};
struct bitReader
{
char buffer;
int pos, len, bits;
ifstream ∈
bitReader( ifstream &in ): in(in)
{
buffer = 0; pos = 8;
in.seekg(0,ios::end);
len = in.tellg(); bits = len * 8;
in.seekg(0,ios::beg);
}
unsigned int ReadBit()
{
if( pos == 8 )
{
buffer = in.get();
pos = 0;
}
int t = buffer & 1;
buffer >>= 1; pos ++;
return t;
}
unsigned int ReadBits( int n )
{
unsigned int ans = 0;
for( int i = 0; i < n; ++ i )
{
ans |= ( ReadBit() << i );
}
return ans;
}
};
编码器
利用Trie树来存储字典。分析发现编码所用的节点只需要存储字符的编码和子节点地址即可。
为了减少存储空间的同时保持一定的速度,使用unorder_map来存储节点地址。
最终时间复杂度为 O ( n ) O(n) O(n), n n n为文件长度。
struct encoderNode
{
int code;
unordered_map< char, encoderNode* > index;
encoderNode(){ code = -1; index.clear(); }
~encoderNode()
{
for( auto i : index )
if( nullptr != i.second )
delete i.second;
index.clear();
}
};
class LZWEncoder
{
private:
encoderNode* root;
encoderNode* pointer;
int tot, maxValue;
vector< int > coded_file;
public:
LZWEncoder() { root = new encoderNode; pointer = root; tot = maxValue = 255;}
~LZWEncoder() { delete root; }
void Init()
{
/*构建最初始的字典。*/
root = new encoderNode; tot = 256;
pointer = root;
for( int i = 0; i < 256; ++ i )
{
auto t = root->index[i] = new encoderNode;
t->code = i;
}
}
vector< int > Encode( unsigned char* buffer, int len )
{
int pos = 0;
char c;
while( pos < len )
{
//读取下一个字符
c = buffer[pos ++];
//如果当前字符串p+下一个字符不在字典中
if( nullptr == pointer->index[c] )
{
//将当前字符串p的编号输出
coded_file.push_back( pointer->code );
//将p+c加入字典
pointer = ( pointer->index[c] = new encoderNode );
pointer->code = tot ++;
pointer = root->index[c];
}
else pointer = pointer->index[c]; //如果在字典中就继续读取下一个字符,直到不再其中
}
if( len == pos ) coded_file.push_back( pointer->code ); //将未输出的部分输出
return coded_file;
}
vector< int > Encode( ifstream &in )
{
//重载,直接从文件流中进行解码
int pos = 0; char c;
in.seekg(0, ios::end); int len = in.tellg(); in.seekg(0, ios::beg);
unsigned char *buffer = new unsigned char[len];
in.read((char*) buffer, len );
auto v = Encode( buffer, len );
delete [] buffer;
return v;
}
vector< int > Encode( bitReader &reader )
{
//重载,从位工具中读取数据并解码
char c; int len = reader.len;
for( int i = 0; i < len; ++ i )
{
//读取下一个字符
c = reader.ReadBits(8);
//如果当前字符串p+下一个字符不在字典中
if( nullptr == pointer->index[c] )
{
//将当前字符串p的编号输出
coded_file.push_back( pointer->code );
//将p+c加入字典
pointer = ( pointer->index[c] = new encoderNode );
pointer->code = tot ++;
pointer = root->index[c];
}
else pointer = pointer->index[c]; //如果在字典中就继续读取下一个字符,直到不再其中
}
coded_file.push_back( pointer->code ); //将未输出的部分输出
return coded_file;
}
void PrintToFile( ofstream &out )
{
//打印到文件中
int len = coded_file.size();
out.write((char*)&len, sizeof(len));
int symSize = 32;
out.write((char*)&symSize, sizeof(symSize) );
for( auto i : coded_file ) out.write((char*)&i, sizeof(i));
}
void PrintToFile( bitWriter &writer )
{
//重载,使用满足要求的最小bit数打印到文件中
int len = coded_file.size();
writer.Write( len, 32 );
for( auto i : coded_file ) maxValue = max( maxValue, i );
//存储一个索引所需要的bit数
int symSize = ceil( log2(maxValue) );
writer.Write( symSize, 32 );
for( auto i : coded_file )
writer.Write( i, symSize );
writer.EndWrite();
}
};
解码器
分析发现,解码器只需要直到每个节点的父亲节点即可,通过这种方式来拓展字典也可以得到原始字符串。
struct decoderNode
{
int character;
int parent;
};
class LZWDecoder
{
private:
int tot;
vector< decoderNode > dict;
string decoded_file;
public:
void Init()
{
for( int i = 0; i < 256; ++ i ) dict.push_back( {( unsigned char)i, -1});
tot = 256;
}
string GetString( int pos )
{
//递归来生成字符串
string ans;
if ( dict[pos].parent == -1 )
{
//找到开头的字符
ans += (dict[pos].character);
return ans;
}
ans += GetString( dict[pos].parent );
//将当前字符加入到最终答案
ans += (dict[pos].character);
return ans;
}
string Decode( vector< int > &v )
{
//p和c分别指当前字符串编号和下一个字符串编号。
int p = v[0], c;
int len = v.size();
for( int i = 1; i < len; ++ i )
{
//获取下一个字符串的编号
c = v[i];
auto s = GetString(p); decoded_file += s; //将当前字符串加入到最终结果中
if( c < tot ) dict.push_back( { GetString(c)[0], p } ); //如果当前编号还没有在字典中出现,那就将下一个串的首字符拼接到当前串的后面
else dict.push_back({s[0], p } ); //否则将当前串的首字母拼接到当前串的后面
p = c; tot ++;
}
decoded_file += GetString(p);
return decoded_file;
}
string Decode( ifstream &in )
{
//重载,从文件流中读取文件来进行解码
int len, symSize; in.read((char*) &len, sizeof(int));
in.read((char*)&symSize, sizeof(int));
int p = in.get(), c;
for( int i = 1; i < len; ++ i )
{
//获取下一个字符串的编号
c = in.get();
auto s = GetString(p); decoded_file += s; //将当前字符串加入到最终结果中
if( c < tot ) dict.push_back( { GetString(c)[0], p } ); //如果当前编号还没有在字典中出现,那就将下一个串的首字符拼接到当前串的后面
else dict.push_back({s[0], p } ); //否则将当前串的首字母拼接到当前串的后面
p = c; tot ++;
}
decoded_file += GetString(p);
return decoded_file;
}
string Decode( bitReader &reader )
{
//重载,从位工具中读取数据
//p和c分别指当前字符串编号和下一个字符串编号。
int len = reader.ReadBits(32);
int symSize = reader.ReadBits(32);
int p = reader.ReadBits(symSize), c;
for( int i = 1; i < len; ++ i )
{
//获取下一个字符串的编号
c = reader.ReadBits(symSize);
auto s = GetString(p); decoded_file += s; //将当前字符串加入到最终结果中
if( c < tot ) dict.push_back( { GetString(c)[0], p } ); //如果当前编号还没有在字典中出现,那就将下一个串的首字符拼接到当前串的后面
else dict.push_back({s[0], p } ); //否则将当前串的首字母拼接到当前串的后面
p = c; tot ++;
}
decoded_file += GetString(p);
return decoded_file;
}
void PrintToFile( ofstream &out )
{
//将解码结果输出到文件
for( auto i : decoded_file )
{
unsigned char c = i;
out.write((char*)&c, sizeof(c));
}
}
};
实验过程及结果
文本文件测试
利用如下代码进行测试:
void Test1()
{
string inPath = "test.txt", outPath = "test_encoded.dat";
ifstream in( inPath, ios::binary ); ofstream out(outPath, ios::binary);
bitReader reader(in); bitWriter writer(out);
LZWEncoder encoder; LZWDecoder decoder;
encoder.Init(); decoder.Init();
encoder.Encode(in); encoder.PrintToFile(writer);
in.close(); out.close();
inPath = "test_encoded.dat", outPath = "test_decoded.txt";
in.open(inPath, ios::binary); out.open( outPath, ios::binary );
bitReader decode_reader(in);
decoder.Decode(decode_reader);
decoder.PrintToFile(out);
}
可见编码和解码过程是没有问题的。
同时比较文件大小:
可以看到文件确实被压缩了,大概压缩了
1
2
\frac{1}{2}
21左右。
压缩效率分析
使用十个不同类型的文件进行分析。新键一个文件夹存入十个不同类型的文件。
添加如下代码:
void Test2()
{
string path = "D:\\CLionProjects\\test";
string csvPath = "res.csv";
ofstream csv(csvPath);
csv << "file name,original bits,compressed bits,compression rate" << endl;
vector<string> v;
getAllFiles(path, v);
for (auto i : v)
{
if (i.back() == '.') continue;
LZWEncoder encoder; encoder.Init();
ifstream in(i, ios::binary); ofstream out(i + ".dat", ios::binary);
bitReader reader(in); bitWriter writer(out);
encoder.Encode(reader); encoder.PrintToFile(writer);
csv << i.substr(path.size() + 1) << ","
<< reader.bits << ","
<< writer.bits << ","
<< 1.0 - (1.0 * writer.bits / reader.bits) << endl;
in.close(); out.close();
}
csv.close();
}
int main()
{
Test2();
}
编码后的结果;
压缩效率分析:
可以看到,对与某些类型文件如zip,pdf,docx,jpg经过LZW算法之后大小不升反降。首先是他们重复率可能不高,其次LZW算法其实还可以进一步优化,如利用霍夫曼编码对索引进行编码等。
相比之下,LZW算法更适用于大文件(重复的可能性较大),和内容重复率较高的文件。
最终代码
header.h
#pragma once
#include <iostream>
#include <unordered_map>
#include <io.h>
#include <map>
#include <fstream>
#include <string>
using namespace std;
struct bitWriter
{
char buffer;
int pos, bits;
ofstream& out;
bitWriter(ofstream& out);
void WriteBit(int bit);
void Write(int value, int len);
void EndWrite();
};
struct bitReader
{
char buffer;
int pos, len, bits;
ifstream& in;
bitReader(ifstream& in);
unsigned int ReadBit();
unsigned int ReadBits(int n);
};
struct encoderNode
{
int code;
unordered_map< char, encoderNode* > index;
encoderNode();
~encoderNode();
};
class LZWEncoder
{
private:
encoderNode* root;
encoderNode* pointer;
int tot, maxValue;
vector< int > coded_file;
public:
LZWEncoder();
~LZWEncoder();
void Init();
vector< int > Encode(unsigned char* buffer, int len);
vector< int > Encode(ifstream& in);
vector< int > Encode(bitReader& reader);
void PrintToFile(ofstream& out);
void PrintToFile(bitWriter& writer);
};
struct decoderNode
{
int character;
int parent;
};
class LZWDecoder
{
private:
int tot;
vector< decoderNode > dict;
string decoded_file;
public:
void Init();
string GetString(int pos);
string Decode(vector< int >& v);
string Decode(ifstream& in);
string Decode(bitReader& reader);
void PrintToFile(ofstream& out);
};
bitOperator.cpp
#include "header.h"
bitWriter::bitWriter(ofstream& out) : out(out) { buffer = pos = bits = 0; }
void bitWriter::WriteBit(int bit)
{
if (pos == 8)
{
out.write(&buffer, 1);
pos = 0; buffer = 0;
}
buffer |= (bit << pos);
pos++; bits++;
}
void bitWriter::Write(int value, int len)
{
while (len--)
{
WriteBit(value & 1);
value >>= 1;
}
}
void bitWriter::EndWrite()
{
out.write(&buffer, 1);
}
bitReader::bitReader(ifstream& in) : in(in)
{
buffer = 0; pos = 8;
in.seekg(0, ios::end);
len = in.tellg(); bits = len * 8;
in.seekg(0, ios::beg);
}
unsigned int bitReader::ReadBit()
{
if (pos == 8)
{
buffer = in.get();
pos = 0;
}
int t = buffer & 1;
buffer >>= 1; pos++;
return t;
}
unsigned int bitReader::ReadBits(int n)
{
unsigned int ans = 0;
for (int i = 0; i < n; ++i)
{
ans |= (ReadBit() << i);
}
return ans;
}
de_encoder.cpp
#include"header.h"
#include <algorithm>
encoderNode::encoderNode() { code = -1; index.clear(); }
encoderNode::~encoderNode()
{
for (auto i : index)
if (nullptr != i.second)
delete i.second;
index.clear();
}
LZWEncoder::LZWEncoder() { root = new encoderNode; pointer = root; tot = maxValue = 255; }
LZWEncoder::~LZWEncoder() { delete root; }
void LZWEncoder::Init()
{
/*构建最初始的字典。*/
root = new encoderNode; tot = 256;
pointer = root;
for (int i = 0; i < 256; ++i)
{
auto t = root->index[i] = new encoderNode;
t->code = i;
}
}
vector< int > LZWEncoder::Encode(unsigned char* buffer, int len)
{
int pos = 0;
char c;
while (pos < len)
{
//读取下一个字符
c = buffer[pos++];
//如果当前字符串p+下一个字符不在字典中
if (nullptr == pointer->index[c])
{
//将当前字符串p的编号输出
coded_file.push_back(pointer->code);
//将p+c加入字典
pointer = (pointer->index[c] = new encoderNode);
pointer->code = tot++;
pointer = root->index[c];
}
else pointer = pointer->index[c]; //如果在字典中就继续读取下一个字符,直到不再其中
}
if (len == pos) coded_file.push_back(pointer->code); //将未输出的部分输出
return coded_file;
}
vector< int > LZWEncoder::Encode(ifstream& in)
{
//重载,直接从文件流中进行解码
int pos = 0; char c;
in.seekg(0, ios::end); int len = in.tellg(); in.seekg(0, ios::beg);
unsigned char* buffer = new unsigned char[len];
in.read((char*)buffer, len);
auto v = Encode(buffer, len);
delete[] buffer;
return v;
}
vector< int > LZWEncoder::Encode(bitReader& reader)
{
//重载,从位工具中读取数据并解码
char c; int len = reader.len;
for (int i = 0; i < len; ++i)
{
//读取下一个字符
c = reader.ReadBits(8);
//如果当前字符串p+下一个字符不在字典中
if (nullptr == pointer->index[c])
{
//将当前字符串p的编号输出
coded_file.push_back(pointer->code);
//将p+c加入字典
pointer = (pointer->index[c] = new encoderNode);
pointer->code = tot++;
pointer = root->index[c];
}
else pointer = pointer->index[c]; //如果在字典中就继续读取下一个字符,直到不再其中
}
coded_file.push_back(pointer->code); //将未输出的部分输出
return coded_file;
}
void LZWEncoder::PrintToFile(ofstream& out)
{
//打印到文件中
int len = coded_file.size();
out.write((char*)&len, sizeof(len));
int symSize = 32;
out.write((char*)&symSize, sizeof(symSize));
for (auto i : coded_file) out.write((char*)&i, sizeof(i));
}
void LZWEncoder::PrintToFile(bitWriter& writer)
{
//重载,使用满足要求的最小bit数打印到文件中
int len = coded_file.size();
writer.Write(len, 32);
for (auto i : coded_file) maxValue = max(maxValue, i);
int symSize = ceil(log2(maxValue));
writer.Write(symSize, 32);
for (auto i : coded_file)
writer.Write(i, symSize);
writer.EndWrite();
}
void LZWDecoder::Init()
{
for (int i = 0; i < 256; ++i) dict.push_back({ (unsigned char)i, -1 });
tot = 256;
}
string LZWDecoder::GetString(int pos)
{
//递归来生成字符串
string ans;
if (dict[pos].parent == -1)
{
//找到开头的字符
ans += (dict[pos].character);
return ans;
}
ans += GetString(dict[pos].parent);
//将当前字符加入到最终答案
ans += (dict[pos].character);
return ans;
}
string LZWDecoder::Decode(vector< int >& v)
{
//p和c分别指当前字符串编号和下一个字符串编号。
int p = v[0], c;
int len = v.size();
for (int i = 1; i < len; ++i)
{
//获取下一个字符串的编号
c = v[i];
auto s = GetString(p); decoded_file += s; //将当前字符串加入到最终结果中
if (c < tot) dict.push_back({ GetString(c)[0], p }); //如果当前编号还没有在字典中出现,那就将下一个串的首字符拼接到当前串的后面
else dict.push_back({ s[0], p }); //否则将当前串的首字母拼接到当前串的后面
p = c; tot++;
}
decoded_file += GetString(p);
return decoded_file;
}
string LZWDecoder::Decode(ifstream& in)
{
//重载,从文件流中读取文件来进行解码
int len, symSize; in.read((char*)&len, sizeof(int));
in.read((char*)&symSize, sizeof(int));
int p = in.get(), c;
for (int i = 1; i < len; ++i)
{
//获取下一个字符串的编号
c = in.get();
auto s = GetString(p); decoded_file += s; //将当前字符串加入到最终结果中
if (c < tot) dict.push_back({ GetString(c)[0], p }); //如果当前编号还没有在字典中出现,那就将下一个串的首字符拼接到当前串的后面
else dict.push_back({ s[0], p }); //否则将当前串的首字母拼接到当前串的后面
p = c; tot++;
}
decoded_file += GetString(p);
return decoded_file;
}
string LZWDecoder::Decode(bitReader& reader)
{
//重载,从位工具中读取数据
//p和c分别指当前字符串编号和下一个字符串编号。
int len = reader.ReadBits(32);
int symSize = reader.ReadBits(32);
int p = reader.ReadBits(symSize), c;
for (int i = 1; i < len; ++i)
{
//获取下一个字符串的编号
c = reader.ReadBits(symSize);
auto s = GetString(p); decoded_file += s; //将当前字符串加入到最终结果中
if (c < tot) dict.push_back({ GetString(c)[0], p }); //如果当前编号还没有在字典中出现,那就将下一个串的首字符拼接到当前串的后面
else dict.push_back({ s[0], p }); //否则将当前串的首字母拼接到当前串的后面
p = c; tot++;
}
decoded_file += GetString(p);
return decoded_file;
}
void LZWDecoder::PrintToFile(ofstream& out)
{
//将解码结果输出到文件
for (auto i : decoded_file)
{
unsigned char c = i;
out.write((char*)&c, sizeof(c));
}
}
main.cpp
#include <iostream>
#include "header.h"
void Test1()
{
string inPath = "test.txt", outPath = "test_encoded.dat";
ifstream in(inPath, ios::binary); ofstream out(outPath, ios::binary);
bitReader reader(in); bitWriter writer(out);
LZWEncoder encoder; LZWDecoder decoder;
encoder.Init(); decoder.Init(); encoder.Encode(in);
encoder.PrintToFile(writer);
in.close(); out.close();
inPath = "test_encoded.dat", outPath = "test_decoded.txt";
in.open(inPath, ios::binary); out.open(outPath, ios::binary);
bitReader decode_reader(in);
decoder.Decode(decode_reader);
decoder.PrintToFile(out);
}
void getAllFiles(string path, vector<string>& files)
{
long hFile = 0;
struct _finddata_t fileinfo;
string p;
if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1) {
do {
files.push_back(p.assign(path).append("\\").append(fileinfo.name));
} while (_findnext(hFile, &fileinfo) == 0);
_findclose(hFile);
}
}
void Test2()
{
string path = "D:\\CLionProjects\\test";
string csvPath = "res.csv";
ofstream csv(csvPath);
csv << "file name,original bits,compressed bits,compression rate" << endl;
vector<string> v;
getAllFiles(path, v);
for (auto i : v)
{
if (i.back() == '.') continue;
LZWEncoder encoder; encoder.Init();
ifstream in(i, ios::binary); ofstream out(i + ".dat", ios::binary);
bitReader reader(in); bitWriter writer(out);
encoder.Encode(reader); encoder.PrintToFile(writer);
csv << i.substr(path.size() + 1) << ","
<< reader.bits << ","
<< writer.bits << ","
<< 1.0 - (1.0 * writer.bits / reader.bits) << endl;
in.close(); out.close();
}
csv.close();
}
int main()
{
Test1();
Test2();
}