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

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

 

这个算法的java代码放在thrift的org.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]原=[00001000]反

[−8]=[10001000]原=[11110111]反

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

 

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

 

原码:[00000000]原=[10000000]原

反码:[00000000]反=[11111111]反

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

原码:

 

1+(−1)=[00000001]原+[10000001]原=[10000010]原=−2

明显是不对的!

 

反码:

 

1+(−1)=[00000001]反+[11111110]反=[11111111]反=−0

表现的好诡异!

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

 

C、补码

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

 

[+8]=[00001000]原=[00001000]补

[−8]=[10001000]原=[11111000]补

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

 

1+(−1)=[00000001]补+[11111111]补=[00000000]补=0

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

 

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

 

3、zigzag

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

 

而我们在系统之间进行通讯的时候,往往又需要以整型(int)或长整型(long)为基本的传输类型,他们在大多数系统中,以4Bytes和8Bytes来表示。这样,为了传输一个整型(int)1,我们需要传输00000000_00000000_00000000_00000001 32个bits,除了一位是有价值的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这个以后,我们最好只需要发送2bits(10),或者发送8bits(00000010),把前面的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补上1个bit的1(0x80)。如此反复 直到全是前导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_id、comment_count等这种从几到几千数值的东东。那我们也能通过计算,得到每超过一个7位的信息以后,传输的字节数就会增加1。以至于,如果数字比较大的时候,原来4字节的数字还会变成5字节:

8c395c1bdf64451387e8ba7f3205afb2.png

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

 

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

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

44c0bc2d99324e31adbc6174cb237e41.png

f1850151ed8249c0b5e3ef11f5740132.png 

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

 

那这一期就聊到这里吧~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/417138.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Google checkstyle实战

概述 CheckStyle检查代码是否符合制定的规范。CheckStyle检查是基于源码的&#xff0c;无需编译&#xff0c;执行速度快。 CheckStyle的主要流程是&#xff1a; 对Java文件进行词法语法分析&#xff0c;生成语法树。载入配置文件&#xff08;checkstyle-metadata.xml以及自定…

【C++】认识类和对象

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 一、什么是面向对象&#xff1f;二、类的引入三、类的定义四、类的访问限定符与…

(C语言)Sleep函数,system函数,数组练习,详解与运用

一维数组详解&#xff1a;http://t.csdnimg.cn/zahZF 二维数组详解&#xff1a;http://t.csdnimg.cn/h2mLe 我们看过可一维数组与二维数组&#xff0c;现在我们来进行简单的练习。 题目&#xff1a;编写代码&#xff0c;演⽰多个字符从两端移动&#xff0c;向中间汇聚 1. …

AI大模型 拍照搜题

最近&#xff0c;发现一款小程序【问智通】&#xff0c;实现了拍照搜题结合AI大模型&#xff0c;省去了打字和敲数学公式向AI提问&#xff0c;完美的补充了其它拍照搜题平台拍不到&#xff0c;没解析等不足&#xff01;&#xff01;&#xff01; 小程序码&#xff1a; APP下载…

Python:运算符、内置函数和序列基本用法

一、学习目标 1&#xff0e;熟练使用Python运算符。 2&#xff0e;熟练使用Python内置函数。 3&#xff0e;掌握输入、输出函数的使用方法。 4&#xff0e;了解列表、元组、字典、集合的概念和基本用法。 二、相关练习 1&#xff0e;输入一个自然数250&#xff0c;输出其…

如何识别代理服务器的IP地址?

识别一个IP地址是否是由代理服务器发出的&#xff0c;是一项具有挑战性的任务。代理服务器是一种中间网络设备&#xff0c;用于转发客户端的请求和响应&#xff0c;从而隐藏原始客户端的IP地址。由于代理服务器的广泛使用&#xff0c;识别它们对于网络安全、数据分析和市场调研…

【踩坑专栏】追根溯源,从Linux磁盘爆满排查故障:mycat2与navicat不兼容导致日志暴增

昨天遇到了一个比较奇怪的问题&#xff0c;就是在挂起虚拟机的时候&#xff0c;虚拟机提示我XX脚本正在运行&#xff0c;很奇怪&#xff0c;我没有运行脚本&#xff0c;为什么会提示我这个呢。今天恢复虚拟机&#xff0c;也提示了一下脚本的问题&#xff0c;而且发现Linux明显异…

爬取一人之下所有图片网址以及图片的源代码

将网址保存到join中&#xff0c;图片源代码保存到本地目录中 import requests from lxml import etree import json import os from urllib import request# 设置Bing搜索URL和请求头 url https://cn.bing.com/images/search?q%E4%B8%80%E4%BA%BA%E4%B9%8B%E4%B8%8B%E5%9B%B…

BUUCTF AWD-Test1

打开靶场是这个有些简陋的界面。 随便点点&#xff0c;找到这个东西。 看到ThinkPHP&#xff0c;思路瞬间清晰&#xff0c;老熟人了。这个就是ThinkPHP漏洞。根据版本我们去找一下poc。 /index.php/?sIndex/\think\View/display&content%22%3C?%3E%3C?php%20phpinfo();…

通过多进程并发方式(fork)实现服务器

以下内容为视频学习记录。 1、父进程accept后返回的文件描述符为cfd以及用于创建连接的lfd; 调用fork()创建子进程后&#xff0c;子进程继承cfd,lfd&#xff0c;通过该cfd与连接过来的客户端通信,lfd对子进程来说没用&#xff0c;可以直接close(lfd); 对于父进程来说&#x…

Presto简介、部署、原理和使用介绍

Presto简介、部署、原理和使用介绍 1. Presto简介 1-1. Presto概念 ​ Presto是由Facebook开发的一款开源的分布式SQL查询引擎&#xff0c;最初于2012年发布&#xff0c;并在2013年成为Apache项目的一部分&#xff1b;Presto 作为现在在企业中流行使用的即席查询框架&#x…

用flymsg代替飞鸽传书(ipmsg、IPMessenger、聊天、文件传输)

用flymsg代替飞鸽传书(ipmsg、IPMessenger、聊天、文件传输) 一&#xff0e;简介 flymsg是由国人所开发的免费软件&#xff0c;是一款局域网内即时通信软件&#xff0c;基于TCP/IP&#xff08;UDP&#xff09;,可运行于多种操作平台&#xff08;Win,Mac,UNIX,Java&#xff09…

统计业务流量的毫秒级峰值 - 华为机试真题题解

考试平台&#xff1a; 时习知 分值&#xff1a; 200分&#xff08;第二题&#xff09; 考试时间&#xff1a; 两小时&#xff08;共3题&#xff09; 题目描述 业务模块往外发送报文时&#xff0c;有时会出现网卡队列满而丢包问题&#xff0c;但从常规的秒级流量统计结果看&…

MyBatis 学习(一)之 MyBatis 概述

目录 1 MyBatis 介绍 2 MyBatis 的重要组件 3 MyBatis 执行流程 4 参考文档 1 MyBatis 介绍 MyBatis 是一个半自动化的 ORM &#xff08;Object-Relational Mapping&#xff0c;对象关系映射&#xff09;持久层框架&#xff0c;它允许开发者通过 XML 或注解将对象与数据库中…

【python】Python Turtle绘制流星雨动画效果(附源码)

在这篇技术博客中&#xff0c;我们将学习如何使用 Python 的 Turtle 模块绘制一个流星雨的动画效果。通过简单的代码实现&#xff0c;我们可以在画布上展现出流星闪耀的场景&#xff0c;为视觉带来一丝神秘与美感。 一、效果图&#xff1a; 二、准备工作 &#xff08;1)、导入…

生产报工异常信息提示器如何精确提醒管理人员

在现代生产环境中&#xff0c;生产报工异常信息的及时提醒对于管理人员来说至关重要。为了精确提醒管理人员并确保生产流程的顺利进行&#xff0c;智能信息接收腕表作为一种先进的工具&#xff0c;结合了多项功能&#xff0c;可以有效地实现生产报工异常信息的精确提醒。以下将…

【vmware安装群晖】

vmware安装群晖 vmware安装群辉&#xff1a; vmware版本&#xff1a;17pro 下载链接&#xff0c; https://customerconnect.vmware.com/cn/downloads/details?downloadGroupWKST-1751-WIN&productId1376&rPId116859 激活码可自行搜索 教程&#xff1a; https://b…

RK3568 Android12 适配抖音 各大APP

RK3568 Android12 适配抖音 各大APP SOC RK3568 system:Android 12 平台要适配抖音和各大APP 平台首先打开抖音发现摄像头预览尺寸不对只存在右上角,我将抖音APP装在手机上预览,发现是全屏 一开始浏览各大博客 给出的解决方法是修改framework 设置为全屏显示: framewo…

day09_面向对象_构造方法_封装

今日内容 零、 复习昨日 一、构造方法 二、重载 三、封装 零、 复习昨日 1 类和对象是什么关系? 类是模板(原材料)对象是具体实例(成品)类创建出对象 2 类中有什么?(类的成员) 成员属性(成员变量), 成员方法 3 创建对象的语法? 类名 对象名 new 类名(); 4 调用对象属性,方法…

Spade CNN技术细节

Input: (1,3,256,64,128) 做downsample 成 : (1,3,8,16,32) 首先有一个 EqualConv3D(3,128,3,3,stride1,padding1) 对 input 进行卷积得到&#xff1a; (1,128,8,16,32) input channel: 3 output channel: 128 EqualConv3D 就是一个普通的3D CNN&#xff0c; 只是 用到了 equ…