LZW算法原理及实现

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 &in;
    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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值