鱼C论坛

 找回密码
 立即注册
查看: 1120|回复: 0

[技术交流] LZW算法的实现以及注意事项

[复制链接]
发表于 2020-8-11 10:21:39 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
lZW 压缩算法的原理是比较简单的,其通过建立一个字典,不断地把第一次出现的字符串加入字典,当字典中的字符串再次出现时,就将该字符串用字典中对应的编码替换。如果替换编码比原字符串短,则达到压缩的目的。LZW算法的优点是无需存储字典,因为其字典是根据处理内容动态创建的,压缩时,一边创建字典一边压缩,解压时也是一边创建字典一边解压,非常神奇。
压缩过程:以前一个读入的字符为前缀,以后一个读入的字符为后缀,组成一个词条,在字典中查找该词条,如果不存在,则将该词条赋予一个编码,并加入字典。由于标准ASIIC码占据了0-255,因此,字典中的编码可以从256起,逐一累加。如果该词条已存在,则用其对应的编码将该词条替换,然后再读入下一个字符进行处理,这时需要注意的是:后缀规则不变,前缀则不再是上一个读入的字符,换成了刚使用过的编码。
压缩过程注意事项:
1、        由于编码是从256起,其长度大于一个字符,如256就占据了9个Bits,为了解码时能够识别,所有写入的字符长度必须保持一致,如用9位编码,所有编码长度都得用9Bits,原始ASIIC码写入时也必须扩展成9Bits。当然,9Bits所能表示的最大的数仅仅是511,在字典里很快就会被用尽,这时,可以把编码长度扩展到10Bits(1023)、11Bits(2047)、12Bits(4095)等等,但是太大也不合适,一是编码时字典将比较庞大,耗费的内存空间会比较多,查找起来也将非常耗时;二是原始ASIIC码扩展得太大,达不到压缩效果。
2、        当写入一个编码时,程序是无法预知它与下一个读入的字符组成的词条是否已经存在于字典中,如果已经存在,则在写入下一个编码时须将指针退回至前一编码位置,将前一编码覆盖掉。
3、        当字符串中第一次出现连续重复字符时,需谨慎处理。如处理aaa这样的字符串时,首先,a+a组成词条,因其第一次出现,字典中不存在a+a这样的词条,于是将其编入字典,假设编码为351,写入字串为aa,当处理到第3个a时,a+a组成词条,查找字典,找到该词条,取出其对应编码351,按注意事项2中的规则,将编码指针退回至前一编码位置,写入351,此时写入字串变成了a351。解码时,将无法再组成a+a这样的词条来解码,导致解码失败。因此,在出现新创建的词条紧接着就被找到的情况时,应判定为未找到,但不再创建新词条。
4、        每处理一个字符,都将组成一个词条,然后去字典中查找该词条,这个查找字典的过程是整个编码过程中最耗时的部分。如果把字典设置成队列,顺序查找的时间复杂度为O(n),当然,可以考虑用二分查找算法来取代顺序查找算法,但是二分查找算法要求关键字是有序的,而事实上,作为关键字的词条是无序的,这时,可以考虑把字典设置成二叉树,把比当前节点词条大的新词条设为右子节点,小的新词条设为左子节点。这时,查找算法的时间复杂度就变成了O(ln¬¬2n)。
5、        字符串压缩实例
编码过程
编码前        aababcabcdabcdeabcdef        长度        21
编码后        aab[257]c[259]d[261]e[263]f        长度        12.375
步骤        前缀        后缀        编码        是否在码表        写入字符        写入方式
1        -        a        -        N        a         
2        a        a        256        N        a         
3        a        b        257        N        b         
4        b        a        258        N        a        覆盖
5        a        b        257        Y        257       
6        257        c        259        N        c         
7        c        a        260        N        a        覆盖
8        a        b        257        Y        257       
9        257        c        259        Y        259       
10        259        d        261        N        d         
11        d        a        262        N        a        覆盖
12        a        b        257        Y        257       
13        257        c        259        Y        259       
14        259        d        261        Y        261       
15        261        e        263        N        e         
16        e        a        264        N        a        覆盖
17        a        b        257        Y        257       
18        257        c        259        Y        259       
19        259        d        261        Y        261       
20        261        e        263        Y        263       
21        263        f        265        N        f         

解压过程:按压缩时写入的Bits长度为单位读取内容,比如说压缩时是按9Bits写入的,则解压时也按9Bits读取。每读入一个编码,看是否时原始ASIIC码(<256)?是则压缩成一个字节(8Bits)写入,同时按前缀+后缀的规则创建字典,若非原始ASIIC码,则在字典中找到该编码对应的词条,并将其解码为前缀+后缀,按照规则,组成词条的后缀一定是原始ASIIC码,可以直接压缩成8Bits写入,而前缀则有可能是原始ASIIC码,也有可能不是,如果不是,则把该前缀作为编码,继续在字典中查找其对应的词条,一直到前缀为原始ASCII码为止。因为解压过程中在字典中查找词条的关键字不再是词条,而是编码,按照规则,编码是逐一累加有序的,所以解压时可以把字典设置为hash表,查找算法的时间复杂度仅为O(1)。
字符串解压实例:
解码前        aab[257]c[259]d[261]e[263]f                  
解码后        aababcabcdabcdeabcdef                  
步骤        读入字符        解码为        前缀        后缀        编码        写入字符
1        a        a        -        a        -        a
2        a        a        a        a        256        a
3        b        b        a        b        257        b
4        257        ab        b        a        258        ab
5        c        c        257        c        259        c
6        259        abc        c        a        260        abc
7        d        d        259        d        261        d
8        261        abcd        d        a        262        abcd
9        e        e        261        e        263        e
10        263        abcde        e        a        264        abcde
11        f        f        263        f        265        f


  1. #ifndef        H_LZW_ENCODE
  2. #define H_LZW_ENCODE

  3. class CLzwEncode
  4. {
  5. private:
  6.         CLzwEncode(const CLzwEncode &);
  7.         CLzwEncode & operator = (const CLzwEncode &);

  8. private:

  9.         //构建 LZW 字典结构
  10.         typedef struct tagLzwDictionary
  11.         {
  12.                 //原码前缀 + 后缀
  13.                 WORD src[2];
  14.                 //编码
  15.                 WORD dst;

  16.                 struct tagLzwDictionary * pLeft;
  17.                 struct tagLzwDictionary * pRight;

  18.         }LZW, *LPLZW;
  19.        
  20.         //编码解码参数
  21.         typedef struct tagCodeParam
  22.         {
  23.                 //编码位数 9 - 12
  24.                 BYTE bits;
  25.                 //源字符串大小
  26.                 long srcSize;
  27.                 //已读取源字符串字符数
  28.                 long srcOffset;
  29.                 //目标字符串中已写入字符数
  30.                 long dstOffset;
  31.                 //一个字节中已使用的位
  32.                 long bitsUsed;
  33.         }CODEPARAM,*LPCODEPARAM;

  34. public:
  35.         CLzwEncode();
  36.         virtual ~CLzwEncode();

  37. private:

  38.         //计算编码位数对应的最大数
  39.         WORD inline MaxCode(BYTE Bits)
  40.         {
  41.                 return 0xFFFF >> (16 - Bits);
  42.         }

  43.         void copyCode(void * src,const void * dst);

  44.         long compareCode(const void * src,const void * dst);
  45.        
  46.         LPLZW CreateNode(const LZW & lzw,WORD * pCode);       
  47.        
  48.         void        DeleteTree(LPLZW pLzw);
  49.         //清空二叉树字典
  50.         void        DestoryTree();
  51.         //压缩时创建二叉树字典
  52.         BOOL        CreateTree(LZW & lzw,WORD * newCode = NULL);
  53.         //清空哈希字典
  54.         void        DestoryHash();
  55.         //解压时创建哈希字典
  56.         void        CreateHash(LZW & lzw);
  57.         //解码一个已编码串
  58.         long        DecodeOne(LZW & lzw,char * buffer);
  59.         //将编码后的字符写入字符串
  60.         long        WriteCode(BYTE bits,long * bitsUsed,WORD code,char * lpBuffer);
  61.         //从已编码的字符串中读取一个编码
  62.         long        ReadCode(BYTE bits,long * bitsUsed,WORD * code,char * lpBuffer);
  63.         //按 bits 编码
  64.         void        innerEncode(CODEPARAM & param,char * src,char * dst);
  65.         //按 bits 解码
  66.         void        innerDecode(CODEPARAM & param,char * src,char * dst);
  67.         //
  68.        

  69. public:
  70.         long        Encode(long nsize,char * src,char * dst);
  71.         long        Decode(long nsize,char * src,char * dst);

  72. private:
  73.         static const   int m_sizeOfTable;
  74.         WORD        m_nUsed;
  75.         LZW                m_table[4096];
  76.         LPLZW        m_lpHead;
  77. };

  78. #endif
复制代码

  1. #include "LzwEncode.h"

  2. const int CLzwEncode::m_sizeOfTable = 4096;

  3. CLzwEncode::CLzwEncode()
  4. {
  5.         m_nUsed = 256;
  6.         m_lpHead = NULL;
  7.         memset(m_table,0,sizeof(m_table));
  8. }

  9. CLzwEncode::~CLzwEncode()
  10. {
  11.         DestoryTree();
  12. }

  13. void CLzwEncode::copyCode(void * src,const void * dst)
  14. {
  15.         _asm
  16.         {
  17.                 MOV                ESI,dst
  18.                 MOV                EDI,src
  19.                 LODSD
  20.                 STOSD
  21.         }
  22. }

  23. long CLzwEncode::compareCode(const void * src,const void * dst)
  24. {
  25.         long nRet = 0;
  26.        
  27.         _asm
  28.         {
  29.                 MOV                ESI,src
  30.                 LODSD
  31.                 MOV                ESI,dst
  32.                 SUB                EAX,DWORD PTR [ESI]
  33.                 MOV                nRet,EAX
  34.         }
  35.        
  36.         return nRet;
  37. }

  38. void CLzwEncode::DeleteTree(LPLZW pLzw)
  39. {
  40.         if(pLzw != NULL)
  41.         {
  42.                 if(pLzw->pLeft != NULL)
  43.                 {
  44.                         DeleteTree(pLzw->pLeft);
  45.                 }
  46.                
  47.                 if(pLzw->pRight != NULL)
  48.                 {
  49.                         DeleteTree(pLzw->pRight);
  50.                 }
  51.                
  52.                 delete pLzw;
  53.         }
  54. }

  55. void CLzwEncode::DestoryTree()
  56. {
  57.         DeleteTree(m_lpHead);
  58.         m_lpHead = NULL;
  59.         m_nUsed = 256;
  60. }

  61. void CLzwEncode::DestoryHash()
  62. {
  63.         memset(m_table,0,sizeof(m_table));
  64.         m_nUsed = 256;
  65. }

  66. CLzwEncode::LPLZW CLzwEncode::CreateNode(const LZW & lzw,WORD * pCode)
  67. {
  68.         LPLZW lpNode = new LZW;

  69.         lpNode->pLeft = NULL;
  70.         lpNode->pRight = NULL;
  71.         lpNode->dst = m_nUsed ++;

  72.         copyCode(lpNode->src,lzw.src);

  73.         if(pCode != NULL)
  74.         {
  75.                 * pCode = lpNode->dst;
  76.         }

  77.         return lpNode;
  78. }


  79. BOOL CLzwEncode::CreateTree(LZW & lzw,WORD * newCode)
  80. {
  81.         BOOL ret = FALSE;
  82.         LPLZW lpNode = NULL;
  83.        
  84.         if(m_lpHead == NULL)
  85.         {
  86.                 m_lpHead = CreateNode(lzw,newCode);
  87.         }
  88.         else
  89.         {
  90.                 lpNode = m_lpHead;       

  91.                 do
  92.                 {
  93.                         long flag = compareCode(lpNode->src,lzw.src);

  94.                         if(flag == 0)
  95.                         {
  96.                                 //当源串第一次出现连续字符时如: aaa
  97.                                 //编码函数处理到前面两个aa时,为aa编码,假定为256。
  98.                                 //而处理到后面两个aa时,按规则,将会用256替换后面两个aa,即编码为 a256
  99.                                 //到解码时,解码函数将只能找个一个a,无法还原字典 aa = 256
  100.                                 //因此当出现连续字符时,必须特殊处理为未找到编码。
  101.                                 if(lpNode->dst + 1 < m_nUsed)
  102.                                 {
  103.                                         lzw.dst = lpNode->dst;
  104.                                         ret = TRUE;
  105.                                 }
  106.                                 break;
  107.                         }

  108.                         if(flag > 0)
  109.                         {
  110.                                 if(lpNode->pRight == NULL)
  111.                                 {
  112.                                         lpNode->pRight = CreateNode(lzw,newCode);
  113.                                         break;
  114.                                 }

  115.                                 lpNode = lpNode->pRight;
  116.                         }
  117.                         else
  118.                         {
  119.                                 if(lpNode->pLeft == NULL)
  120.                                 {
  121.                                         lpNode->pLeft = CreateNode(lzw,newCode);
  122.                                         break;
  123.                                 }
  124.                                 lpNode = lpNode->pLeft;
  125.                         }
  126.                 }while (TRUE);
  127.         }
  128.         return ret;
  129. }

  130. void CLzwEncode::CreateHash(LZW & lzw)
  131. {
  132.         long i = m_nUsed - 256;

  133.         if(i == 0 || compareCode(m_table[i-1].src,lzw.src) != 0)
  134.         {
  135.                 copyCode(m_table[i].src,lzw.src);
  136.                 m_table[i].dst = m_nUsed ++;
  137.         }
  138. }

  139. long CLzwEncode::DecodeOne(LZW & lzw,char * buffer)
  140. {
  141.         long i = m_sizeOfTable;
  142.         WORD code = lzw.src[1];
  143.        
  144.         do
  145.         {
  146.                 //计算编码在字典中的定位
  147.                 long local = code - 256;
  148.                 //字典中的与该编码对应的源码为前缀src[0]+后缀src[1]
  149.                 //其中后缀一定为原始ASIIC码,将其取出
  150.                 buffer[--i] = static_cast<char>(m_table[local].src[1]);       
  151.                
  152.                 //取得源码前缀
  153.                 code = m_table[local].src[0];

  154.                 //如果前缀是原始ASIIC码,说明解码结束
  155.                 if(code < 256)
  156.                 {
  157.                         buffer[--i] = TO_CHAR(code);
  158.                         //以LZW.src[0]为前缀,以code为后缀,构建编码
  159.                         LZW tmp = lzw;
  160.                         tmp.src[1] = code;
  161.                         CreateHash(tmp);
  162.                         break;
  163.                 }
  164.                 //非原始ASIIC码则继续解码
  165.         }while(TRUE);

  166.         return i;
  167. }

  168. long CLzwEncode::WriteCode(BYTE bits,long * bitsUsed,WORD code,char * lpBuffer)
  169. {
  170.         unsigned long temp = code;
  171.         //计算实际写入Bit数 = 当前字节已使用Bits + 需要写入的Bits
  172.         long writes = *bitsUsed + bits;
  173.        
  174.         //当前字节已使用Bits不为0的处理
  175.         if(*bitsUsed != 0)
  176.         {
  177.                 //取得当前字节,因为要做移位运算,转化成无符号字符
  178.                 BYTE oldChar = lpBuffer[0];
  179.                 //已使用的Bits在低位,左移将未使用的Bits清零
  180.                 //右移位恢复已使用的Bits
  181.                 oldChar <<= (8 - *bitsUsed);
  182.                 oldChar >>= (8 - *bitsUsed);
  183.                 //将需要写入的Bits左移,为当前字节已使用Bits腾出位置
  184.                 temp <<= *bitsUsed;
  185.                 //将当前字节已使用Bits与需要写入的Bits相结合
  186.                 temp |= oldChar;
  187.         }
  188.        
  189.         //写入4个字节
  190.         memcpy(lpBuffer,&temp,sizeof(long));

  191.         //计算写入后当前字节的已使用Bits
  192.         *bitsUsed = writes % 8;
  193.         //返回写入的字节数
  194.         return writes / 8 ;;
  195. }

  196. long CLzwEncode::ReadCode(BYTE bits,long * bitsUsed,WORD * code,char * lpBuffer)
  197. {
  198.         unsigned long temp = 0;
  199.         //计算实际读取Bit数 = 当前字节已读取Bits + 需要读取的Bits
  200.         long reads = * bitsUsed + bits;
  201.        
  202.         //读取4个字节
  203.         memcpy(&temp,lpBuffer,sizeof(long));
  204.        
  205.         //右移清除掉已读取的Bits
  206.         temp >>= *bitsUsed;
  207.         //保留需要读取的Bits,将多余的Bits清0
  208.         temp <<= (32 - bits);
  209.         temp >>= (32 - bits);
  210.        
  211.         //将读取的Bits转化为WORD
  212.         *code = static_cast<WORD>(temp);
  213.         //计算读取后当前字节的已读取Bits
  214.         *bitsUsed = reads % 8;
  215.         //返回读取的自己数
  216.         return (reads / 8);
  217. }

  218. void CLzwEncode::innerEncode(CODEPARAM & param,char * src,char * dst)
  219. {
  220.         BOOL bFound = FALSE;
  221.         long preOff = param.dstOffset;
  222.         long offset = param.dstOffset;
  223.         long preUsed = param.bitsUsed;
  224.         long bitsUsed = param.bitsUsed;
  225.         LZW lzw = {0};
  226.        
  227.         //清除字典
  228.         DestoryTree();

  229.         //读取第一个字符
  230.         lzw.src[0] = TO_BYTE(src[param.srcOffset++]);
  231.         lzw.src[1] = lzw.src[0];
  232.         lzw.dst = lzw.src[0];
  233.         //写入第一个字符
  234.         offset += WriteCode(param.bits,&bitsUsed,lzw.src[1],dst+offset);


  235.         while(param.srcSize > param.srcOffset)
  236.         {
  237.                 //读取后续字符
  238.                 lzw.src[1] = TO_BYTE(src[param.srcOffset++]);
  239.                 lzw.src[0] = lzw.dst;
  240.                 lzw.dst = lzw.src[1];
  241.                
  242.                
  243.                 WORD code = 0;
  244.                 //以src[0]为前缀,src[1]为后缀,在字典中查找编码,
  245.                 //若没找到,则建立新的编码
  246.                 bFound = CreateTree(lzw,&code);
  247.                
  248.                
  249.                 //到达编码表最后一个元素
  250.                 if(code == MaxCode(param.bits))
  251.                 {
  252.                         //写入结束标志
  253.                         param.srcOffset--;
  254.                         offset += WriteCode(param.bits,&bitsUsed,code,dst+offset);
  255.                         break;
  256.                 }
  257.                
  258.                 if(bFound)
  259.                 {
  260.                         //已编码,则将前一个写入字符覆盖写入
  261.                         offset = preOff;
  262.                         bitsUsed = preUsed;
  263.                         offset += WriteCode(param.bits,&bitsUsed,lzw.dst,dst+offset);
  264.                 }
  265.                 else
  266.                 {
  267.                         //未编码,写入原始字符
  268.                         preUsed = bitsUsed;
  269.                         preOff = offset;
  270.                         offset += WriteCode(param.bits,&bitsUsed,lzw.src[1],dst+offset);
  271.                 }       
  272.         }

  273.         param.bitsUsed = bitsUsed;
  274.         param.dstOffset = offset;
  275. }

  276. void CLzwEncode::innerDecode(CODEPARAM & param,char * src,char * dst)
  277. {
  278.         long offset = param.srcOffset;
  279.         long bitsUsed = param.bitsUsed;
  280.         BOOL bFind = FALSE;
  281.         LZW lzw = {0};
  282.        
  283.         DestoryHash();

  284.         //读入第一个字符
  285.         offset += ReadCode(param.bits,&bitsUsed,&lzw.src[0],src+offset);
  286.         lzw.src[1] = lzw.src[0];
  287.         dst[param.dstOffset++] = TO_CHAR(lzw.src[0]);
  288.        
  289.         while(param.srcSize > offset)
  290.         {
  291.                 lzw.src[0] = lzw.src[1];
  292.                 //读取后续字符
  293.                 offset += ReadCode(param.bits,&bitsUsed,&lzw.src[1],src+offset);
  294.                
  295.                 //是结束标志则退出循环
  296.                 if(lzw.src[1] == MaxCode(param.bits))
  297.                 {
  298.                         break;
  299.                 }
  300.                
  301.                 //是原始ASICC(0-255)字符,直接写入,并创建字典
  302.                 if(lzw.src[1] < 256)
  303.                 {
  304.                         dst[param.dstOffset++] = TO_CHAR(lzw.src[1]);
  305.                         CreateHash(lzw);
  306.                 }
  307.                 //非原始字符(>255)的处理
  308.                 else
  309.                 {
  310.                         char buffer[m_sizeOfTable] = {0};
  311.                         //在字典中查找其编码并返回原字符串
  312.                         long local = DecodeOne(lzw,buffer);                       
  313.                         memcpy(dst+param.dstOffset,buffer+local,m_sizeOfTable - local);
  314.                         param.dstOffset += (m_sizeOfTable - local);
  315.                 }
  316.         }

  317.         param.bitsUsed = bitsUsed;
  318.         param.srcOffset = offset;
  319. }

  320. long CLzwEncode::Encode(long nsize,char * src,char * dst)
  321. {
  322.         CODEPARAM param = {0};
  323.         param.bits = 9;
  324.         param.srcSize = nsize;

  325.         do
  326.         {
  327.                 innerEncode(param,src,dst);

  328.                 if(param.srcOffset >= param.srcSize)
  329.                 {
  330.                         break;
  331.                 }

  332.                 if(param.bits < 12)
  333.                 {
  334.                         param.bits ++;
  335.                 }

  336.         } while(TRUE);

  337.         return param.dstOffset;
  338. }

  339. long CLzwEncode::Decode(long nsize,char * src,char * dst)
  340. {
  341.         CODEPARAM param = {0};
  342.         param.bits = 9;
  343.         param.srcSize = nsize;

  344.         do
  345.         {
  346.                 innerDecode(param,src,dst);
  347.                 if(param.srcOffset >= param.srcSize)
  348.                 {
  349.                         break;
  350.                 }

  351.                 if(param.bits < 12)
  352.                 {
  353.                         param.bits ++;
  354.                 }
  355.         } while (TRUE);

  356.         return param.dstOffset;
  357. }
复制代码
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-5-3 04:57

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表