`
zgwangbo
  • 浏览: 1229 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

小而巧的数字压缩算法:zigzag

阅读更多

阅读facebook开源的RPCRemote Procedure Call)框架thrift源代码的时候,本来是在阅读框架,却不小心被zigzag这个钻石般闪耀的代码吸引。后来去百度搜索zigzag,却得到满屏图像相关的一个算法(看来起名字得有特点才行)。既然相关资料很少,而算法又这么有趣,老王就想要不写一篇这个算法的文章,分享给大家。

 

这个算法的java代码放在thriftorg.apache.thrift.protocol.TCompactProtocol类里,数据传输的时候用做数字的压缩,以减少数据的传输量。

 

为了写好这篇文章,同时方便大家阅读,老王把这个算法从thrift框架中摘离出来,清理了与算法无关的东西,然后用C语言重新实现了一遍,在文章末尾会完整的贴出来,大家可以围观。

 

好了,开始正题,跟老王一起来吧~

 

在聊这个算法之前,我们得先补补课,聊聊进制、补码相关的东东。

 

1、进制

这个内容是作为码工挣钱最基础的知识之一。所谓进制,全称是进位制,就是当某一个位上的信息满了,需要往前进位。比如,某一位上的信息只能容纳十个,超过十个就往前进一位,则是逢十进一的十进制;如果逢二进一,则是二进制;等等。进制之间是可以转换的,比如十进制的10 等于 二进制的1010, 也等于十六进制的A,通常写作:(10)10 = (1010)2 = (A)16

 

我之前看一本书就讲现在为什么大家通用的是十进制。一个比较有趣的答案说,因为人类只有10个手指头,数数的时候,挨个儿数过去刚好十个数,所以十进制自然而然成为默认的进制。如果人类是12个手指头,说不定就是十二进制了。

 

后来计算机的出现,一个数据的有无是最天然的信息承载单元,所以由0-1组成的二进制很天然的成为计算机的进制方式。在此基础上,为方便信息的传递,又采用了八进制、十六进制等进制。

 

好了,因为大家对进制这个东东其实也是比较了解,我就不多扯了,就先说到这儿。

 

2、补码

我们对一个十进制的正整数可以采用相关算法,得到他对应的二进制编码,比如:(10)10 = (1010)2 。不过,如果我们要表示负整数,怎么办呢?在计算机的世界里,我们就定义了原码、反码和补码这几个东东。

 

为了描述简单,我们都假设我们的数字用一个字节(1Byte=8bits)来表示。

 

A、原码

我们用第一个位表示符号(0为非负数,1为负数),剩下的位表示值。比如:

[+8] = [00001000]

[-8] = [10001000]

 

B、反码

我们用第一位表示符号(0为非负数,1为负数),剩下的位,非负数保持不变,负数按位求反。比如:

[+8] = [00001000] = [0000 1000]

[-8] = [10001000] = [1111 0111]

 

如果我们用原码或者补码来表示整数的二进制,有什么问题么?表面上看,似乎挺好的。不过仔细思考就会发现两个问题:

第一、0居然用两个编码(+0-0)来表示了:

原码:[0000 0000] = [1000 0000]

反码:[0000 0000] = [1111 1111]

第二、计算机要理解符号位的存在,否则符号位参与运算,就会出现诡异的现象:

原码:

1 + -1

= [00000001] + [1000 0001]

= [10000010]

= -2

明显是不对的!

 

反码:

1 + -1

= [00000001] + [1111 1110]

= [1111 1111]

= -0

表现的好诡异!

 

为了解决这些问题,我们在计算机体系中引入了补码。

 

C、补码

我们用第一位表示符号(0为非负数,1为负数),剩下的位非负数保持不变,负数按位求反末位加一。

[+8] = [00001000] = [0000 1000]

[-8] = [10001000] = [1111 1000]

 

那我们再看看,把符号放进去做运算会有什么样的效果呢?

1 + -1

= [00000001] + [1111 1111]

= [0000 0000]

= 0

 

很明显,通过这样的方式,计算机进行运算的时候,就不用关心符号这个问题,而只需要按照统一的逢二进一的原则进行运算就可以了。

 

好了,脑补了进制和补码以后,我们就可以进入正题了。

 

3zigzag

在绝大多数情况下,我们使用到的整数,往往是比较小的。比如,我们会记录一个用户的id、一本书的id、一个回复的数量等等。在绝大多数系统里面,他们都是一个小整数,就像12341024100等。

 

而我们在系统之间进行通讯的时候,往往又需要以整型(int)或长整型(long)为基本的传输类型,他们在大多数系统中,以4Bytes8Bytes来表示。这样,为了传输一个整型(int1,我们需要传输00000000_00000000_00000000_00000001 32bits,除了一位是有价值的1,其他全是基本无价值的0(此处发出一个声音:浪!费!啊!)。

 

那怎么办呢?牛逼的工程师想出了一个小而有趣的算法:zigzag

 

这个算法具体的思想是怎么样的呢?

 

对于正整数来讲,如果在传输的时候,我们把多余的0去掉(或者是尽可能去掉无意义的0),传输有意义的1开始的数据,那我们是不是就可以做到数据的压缩了呢?那怎么样压缩无意义的0呢?

 

答案也很简单,比如:00000000_00000000_00000000_00000001这个数字,我们如果能只发送一位1或者一个字节00000001,是不是就将压缩很多额外的数据呢?

 

当然,如果这个世界只有正整数,我们就会很方便的做到这一点。可惜,他居然还有负数!!!负数长什么样呢?(-1)10 = (11111111_11111111_11111111_11111111) ,前面全是1,你说怎么弄?!

 

zigzag给出了一个很巧的方法:我们之前讲补码讲过,补码的第一位是符号位,他阻碍了我们对于前导0的压缩,那么,我们就把这个符号位放到补码的最后,其他位整体前移一位:

(-1)10

=(11111111_11111111_11111111_11111111)

=(11111111_11111111_11111111_11111111)符号后移

但是即使这样,也是很难压缩的,因为数字绝对值越小,他所含的前导1越多。于是,这个算法就把负数的所有数据位按位求反,符号位保持不变,得到了这样的整数:

(-1)10

= (11111111_11111111_11111111_11111111)

= (11111111_11111111_11111111_11111111)符号后移

= (00000000_00000000_00000000_00000001)zigzag

 

而对于非负整数,同样的将符号位移动到最后,其他位往前挪一位,数据保持不变。

 

(1)10

= (00000000_00000000_00000000_00000001)

= (00000000_00000000_00000000_00000010)符号后移

= (00000000_00000000_00000000_00000010)zigzag

 

 

唉,这样一弄,正数、0、负数都有同样的表示方法了。我们就可以对小整数进行压缩了,对吧~

 

这两种case,合并到一起,就可以写成如下的算法:

整型值转换成zigzag值:

int int_to_zigzag(int n)

{

        return (n << 1) ^ (n >> 31);

}

 

n << 1 :将整个值左移一位,不管正数、0、负数他们的最后一位就变成了0

(1)10

= (00000000_00000000_00000000_00000001)

左移一位 => (00000000_00000000_00000000_00000010)

 

(-1)10

= (11111111_11111111_11111111_11111111)

左移一位 => (11111111_11111111_11111111_11111110)

 

n >> 31: 将符号位放到最后一位。如果是非负数,则为全0;如果是负数,就是全1

(1)10

= (00000000_00000000_00000000_00000001)

右移31 => (00000000_00000000_00000000_00000000)

 

(-1)10

= (11111111_11111111_11111111_11111111)

右移31 => (11111111_11111111_11111111_11111111)

 

最后这一步很巧妙,将两者进行按位异或操作:

(1)10 =>

(00000000_00000000_00000000_00000010) ^

(00000000_00000000_00000000_00000000)

= (00000000_00000000_00000000_00000010)

可以看到最终结果,数据位保持不变,而符号位也保持不变,只是符号位移动到了最后一位

 

(-1)10 =>

(11111111_11111111_11111111_11111110) ^

(11111111_11111111_11111111_11111111)

= (00000000_00000000_00000000_00000001)

可以看到,数据位全部反转了,而符号位保持不变,且移动到了最后一位。

 

就是这一行代码,就将这个相对复杂的操作做完了,真是写的巧妙。

 

zigzag值还原为整型值:

int zigzag_to_int(int n) 

{

        return (((unsigned int)n) >> 1) ^ -(n & 1);

}

 

类似的,我们还原的代码就反过来写就可以了。不过这里要注意一点,就是右移的时候,需要用不带符号的移动,否则如果第一位数据位是1的话,就会补1。所以,代码里用了无符号的右移操作:(((unsigned int)n) >> 1)。在java代码里,对应的移位操作:n >>> 1

 

好了,有了算法对数字进行转换以后,我们就得到了有前导0的另外一个整数了。不过他还是一个4字节的整数,我们接下来就要考虑怎么样将他们表示成尽可能少的字节数,并且还能还原。

 

比如:我们将1转换成(00000000_00000000_00000000_00000010)zigzag这个以后,我们最好只需要发送2bits10),或者发送8bits00000010),把前面的0全部省掉。因为数据传输是以字节为单位,所以,我们最好保持8bits这样的单位。所以我们有几种做法:

 

A、我们可以额外增加一个字节,用来表示接下来有效的字节长度,比如:00000001_00000010,8位表示接下来有1个字节需要传输,第二8位表示真正的数据。这种方式虽然能达到我们想要的效果,但是莫名的增加一个字节的额外浪费。有没有不浪费的办法呢?

 

B、字节自表示方法。zigzag引入了一个方法,就是用字节自己表示自己。具体怎么做呢?我们来看看代码:

int write_to_buffer(int zz, byte* buf, int size)

{

        int ret = 0;

        for (int i = 0; i < size; i++)

        {  

                if ((zz & (~0x7f)) == 0)

                {

                        buf[i] = (byte)zz;

                        ret = i + 1;

                        break;

                }

                else

                {

                        buf[i] = (byte)((zz & 0x7f) | 0x80);

                        zz = ((unsigned int)zz)>> 7;

                }

        }

        return ret;

}

 

大家先看看代码里那个(~0x7f),他究竟是个什么数呢?

(~0x7f)16

=(11111111_11111111_11111111_10000000)

他就是从倒数第八位开始,高位全为1的数。他的作用,就是看除开最后七位后,还有没有信息。

 

我们把zigzag值传递给这个函数,这个函数就将这个值从低位到高位切分成每7bits一组,如果高位还有有效信息,则给这7bits补上1bit10x80)。如此反复 直到全是前导0,便结束算法。

 

举个例来讲:

(-1000)10

= (11111111_11111111_11111100_00011000)

= (00000000_00000000_00000111_11001111)zigzag

 

我们先按照七位一组的方式将上面的数字划开:

(0000-0000000-0000000-0001111-1001111)zigzag

 

A、他跟(~0x7f)做与操作的结果,高位还有信息,所以,我们把低7位取出来,并在倒数第八位上补一个1(0x80)11001111

 

B、将这个数右移七位:(0000-0000000-0000000-0000000-0001111)zigzag

 

C、再取出最后的七位,跟(~0x7f)做与操作,发现高位已经没有信息了(全是0),那么我们就将最后8位完整的取出来:00001111,并且跳出循环,终止算法;

 

D、最终,我们就得到了两个字节的数据[11001111, 00001111]

 

通过上面几步,我们就将一个4字节的zigzag变换后的数字变成了一个2字节的数据。如果我们是网络传输的话,就直接发送这2个字节给对方进程。对方进程收到数据后,就可以进行数据的组装还原了。具体的还原操作如下:

int read_from_buffer(byte* buf, int max_size)

{

        int ret = 0;

        int offset = 0;

        for (int i = 0; i < max_size; i++, offset += 7)

        {

                byte n = buf[i];

                if ((n & 0x80) != 0x80)

                {

                        ret |= (n <<offset);

                        break;

                }

                else

                {

                        ret |= ((n & 0x7f) << offset);

                }

        }

        return ret;

}

 

整个过程就和压缩的时候是逆向的:对于每一个字节,先看最高一位是否有1(0x80)。如果有,就说明不是最后一个数据字节包,那取这个字节的最后七位进行拼装。否则,说明就是已经到了最后一个字节了,那直接拼装后,跳出循环,算法结束。最终得到4字节的整数。

 

4、总结

好了,整个算法就是差不多几十行,我们却用了几千字来描述他,这就是这个算法精妙的地方。

 

这个算法使用的基础就是认为在大多数情况下,我们使用的数字都是不大的数字,比如:book_idcomment_count等这种从几到几千数值的东东。那我们也能通过计算,得到每超过一个7位的信息以后,传输的字节数就会增加1。以至于,如果数字比较大的时候,原来4字节的数字还会变成5字节:

 



 

不过,在绝大多数情况下,小整数还是占主导的,所以整个算法才有了使用的基础。

 

好了,不知道老王有没有把这个算法讲清楚。如果没讲清楚,就来看代码吧(Talk is cheap, Show me the code)。

 

(注:这个代码是老王自己改写的,自己review了,貌似没有错误。如果大家觉得写的有问题,请指正。)

 





 

 
如果觉得c语言代码看起来不是很舒服,也可以去看thriftjava的源代码~

 

那这一期就聊到这里吧,如果你对老王聊的东西感兴趣,就关注老王的微信:simplemain更多精彩内容还在后面等着你哦 ^_^



  

 

  • 大小: 92.1 KB
  • 大小: 124.9 KB
  • 大小: 115.2 KB
  • 大小: 170.4 KB
  • 大小: 114.2 KB
分享到:
评论

相关推荐

    mesh 压缩算法

    **Mesh压缩算法** 在计算机图形学领域,Mesh(网格)是一种常见的数据结构,用于表示三维物体的表面。它由一组顶点、边和面(通常为三角形或四边形)组成,这些元素共同构建了物体的几何形状。在游戏开发、虚拟现实...

    有关图像jpeg压缩算法介绍及其源码

    JPEG压缩算法的核心在于离散余弦变换(DCT)和量化。 1. **离散余弦变换(DCT)**: DCT是JPEG压缩的关键步骤,它将图像从像素域转换到频率域。在8x8的图像块上进行,通过这个变换,高频细节信息会被集中在低频...

    图像压缩算法JPEG源代码实现(C语言)

    在这个项目中,我们关注的是用C/C++语言实现JPEG图像压缩算法的关键步骤。 1. **颜色空间转换**: 在JPEG压缩前,首先需要将图像从RGB(红绿蓝)颜色空间转换到YCbCr颜色空间。Y表示亮度,Cb和Cr代表色度信息。...

    基于Matlab环境的JPEG图像压缩算法.pdf

    JPEG图像压缩算法是数字图像处理领域的一项重要技术,广泛应用于多媒体数据的存储和传输过程中。JPEG(Joint Photographic Experts Group)是国际标准化组织(ISO)和国际电工委员会(IEC)联合成立的一个专家组,...

    基于3D-DCT变化的图像压缩解压缩算法,分别处理单个图片和视频图像序列。使用matlab2021a或者以上版本测试

    本项目聚焦于一种特定的图像和视频压缩算法——基于3D离散余弦变换(3D-DCT)的方法。这种方法通过将图像或视频序列转换到频域,对数据进行分析和编码,从而实现高效的数据压缩。 3D-DCT是一种扩展了传统2D-DCT的...

    izigzag_kaiyuan.rar_site:www.pudn.com_zigzag 反_zigzag反扫描_zigzag扫

    在图像处理和数字信号处理领域,Zigzag扫描是一种常用的技术,特别是在离散余弦变换(Discrete Cosine Transform, DCT)过程中。标题“izigzag_kaiyuan.rar_site:www.pudn.com_zigzag 反_zigzag反扫描_zigzag扫”...

    zigzag22.rar_zigzag

    在图像处理领域,Zigzag编码是一种常用的无损数据压缩技术,主要应用于图像的数字化存储和传输。这个“zigzag22.rar_zigzag”压缩包文件可能包含了一个名为“zigzag22.m”的MATLAB程序,该程序可能是用于演示或实现...

    matlab-基于RLC,zigzag变换,Haar滤波以及均匀量化处理的图像压缩解压缩matlab仿真-源码

    本项目聚焦于使用MATLAB实现的一种特定的图像压缩与解压缩方法,结合了RLC(Run-Length Coding)编码、Zigzag变换、Haar小波滤波以及均匀量化处理。下面将详细阐述这些知识点及其在图像压缩中的应用。 首先,RLC...

    基于MBFDF的两次JPEG压缩的检测算法

    本文将详细讨论基于MBFDF(频带子带的首位数字定律)的两次JPEG压缩检测算法,这是一种用于识别图像是否经过二次压缩的技术,进而帮助检测图像是否被篡改。 JPEG(Joint Photographic Experts Group)是一种广泛...

    图像压缩算法JPEG Baseline Huffman编码模式

    6. **Zigzag排序** 为了便于熵编码,DCT系数通常按照Zigzag顺序排列,这样可以使得相邻的系数更可能具有相近的值,有利于压缩。 7. **Huffman编码** Huffman编码是一种可变长度的无损熵编码方法,它根据出现频率...

    多媒体技术实验三数据压缩

    数据压缩技术是指将数字数据转换为更小尺寸的形式,以便于存储或传输的过程。数据压缩技术可以分为两大类:无损压缩和有损压缩。无损压缩是指将数据压缩到可以恢复原始数据的程度,而有损压缩是指将数据压缩到失去...

    基于DCT的JPEG图像压缩编码算法的MATLAB实现

    **基于DCT的JPEG图像压缩编码算法的MATLAB实现** JPEG(Joint Photographic Experts Group)是一种广泛应用于数字图像处理中的有损压缩标准,其核心是离散余弦变换(Discrete Cosine Transform, DCT)。在MATLAB...

    图像数字水印+matlab程序.pdf

    数字水印算法设计时必须考虑压缩的影响,研究表明,采用与压缩算法相同变换域的水印方法能提高稳健性。 图像数字水印的实现主要包括三个步骤:宿主图像的变换、水印的嵌入和水印的检测。在变换域中添加水印可以提高...

    JPEG.rar_jpeg_jpeg matlab_jpeg 压缩_jpeg压缩_jpeg压缩代码

    总之,这个压缩包中的资源提供了一个学习和实践JPEG压缩算法的平台,尤其是MATLAB代码可以帮助初学者从实际操作中理解这一重要的图像压缩技术。通过深入研究和实验,可以增强对数字图像处理的理解,为未来在相关领域...

    基于DCT的图像压缩编码算法的MATLAB实现.doc

    总的来说,基于DCT的JPEG图像压缩算法在MATLAB中的实现,不仅加深了对图像压缩理论的理解,也为实际应用提供了可行的解决方案。通过实验和分析,我们可以优化参数,找到最佳的压缩策略,以满足特定场景下的存储和...

    视频压缩代码

    理解并掌握帧内帧间预测、DCT变换、zigzag扫描以及熵编码等核心概念,对于开发高效的视频压缩算法至关重要。通过深入学习和实践"Video-compression-algorithm-master"项目,你可以进一步提升在视频处理领域的专业...

    JPEG图像压缩实验.pdf

    JPEG(Joint Photographic Experts Group)是一个由 ISO 和 ITU-T 两个组织机构联合组成的一个图像专家小组,负责制定静态的数字图像数据压缩编码标准。JPEG 算法是国际上通用的标准,适用于静止图像和电视图像的帧...

Global site tag (gtag.js) - Google Analytics