计算二进制形式中1的数量这种问题,在各种刷题网站上比较常见,以往都是选择最笨的遍历方法“蒙混”过关。在了解Redis的过程中接触到了variable precision SWAR算法(以下简称VP-SWAR算法),算法异常简洁,是目前已知的同类方法中最快的。但如果对于位运算不是很熟悉的话,却不一定容易理解,所以有必要记录一下。

下面先看看VP-SWAR算法的完整实现,然后再逐行解释。

  public int vpSWAR(int i){
i = (i & 0x55555555) + ((i>>1) & 0x55555555);
i = (i & 0x33333333) + ((i>>2) & 0x33333333);
i = (i & 0x0F0F0F0F) + ((i>>4) & 0x0F0F0F0F);
i = (i * 0x01010101) >> 24;
return i;
}

VP-SWAR算法分为四步,第一步

i = (i & 0x55555555) + ((i>>1) & 0x55555555);

第一步的作用是计算每两位为一组的二进制形式包含1的个数。要理解这句话,我们需要从二进制的角度看看到底发生了什么。首先, 0x55555555 的二进制表示为 0101 0101 0101 0101 0101 0101 0101 0101 ,这个数字的规律是基数位为1,偶数位为0。为简单起见,我们只考虑两位,总共有四种情况,即:

i x i & x 结果
00 01 00
01 01 01
10 01 00
11 01 01

这里用x代表0b01(0b表示二进制)观察发现, i & (0b01) 是i的基数位对应x的1位,i的偶数位对应着x的0位, i & (0b01) 的结果会将i的偶数位置为0,而基数位保持不变,得到的结果就是i的基数位包含1的个数。 (i >> 1) & 0x55555555 先将i右移一位,也就是将i的基数位对应x的0位,i的偶数位对应着x的1位,然后再与 0x55555555 按位与,计算出来的是i的偶数位包含1的个数。两个计算结果相加就得到i每两位为一组中包含的1的数量,我们最后需要的就是这每两位一组的和。

第二步是在第一步的基础上,计算每四位为一组包含1的个数。按照每2位为一组分组用到了 0x55555555 这个数,那么自然的,按照每4位为一组分组自然就需要 0b0011 这种形式,这就是使用 0x33333333 的原因。理论上, i & (0b0011) 总共有16种情况,但是四位二进制位最多包含4个1,用二进制表示为 0b0100 ,所以经过第一步之后,i最多有5种取值,如下:

i x i & x 结果
0000 0011 0000
0001 0011 0001
0010 0011 0010
0011 0011 0011
0100 0011 0000

观察发现, i & (0b0011) 得到的是i的低两位包含1的个数,  (i >> 2) & 0b0011)得到的是i的高两位包含的1的个数,两个结果相加得到每四位包含的1的个数。注意,这里并不是说任何数与 0b0011 按位与得到的都是低两位包含的1的个数,这里的前提是第一步的计算,因为经过第一步计算之后,每两位包含多少个1已经记录了下来,再和 0b0011 按位与才得到正确的结果。例如, 0x0010 & 0x 0011=0x0010 ,但是我们不能说 0x0010 包含两个1,但是如果 0x0010 是经过第一步的计算得来,那才说明 0x0010 记录原始数据低两位有两个1。

第三步在第二步基础上,计算每8位有多少个1,由 0x01 和 0x0011 ,很自然想到 0x00001111 ,其对应的32位的十六进制数就是 0x0F0F0F0F 。

第四步就很有意思了,它不再是计算每16位包含1的个数,而是直接计算32位包含1的个数。对于32位的数来说,可以将其按每8位一组分为4组,分别用ABCD表示,例如 0x01020304 用这种形式表示为:

假设 0x01020304 是经过前三步计算之后得到的结果,那么要计算其总共包含多少个1,只需计算A+B+C+D。而ABCD表示的是不同的位区间范围,不能直接相加,该如何快速计算A+B+C+D的值呢?这里又用到了移位运算,将B、C、D分别左移8位、16位、24位,使其分别与A对齐:

我们发现,将数字i分别左移0位、8位、16位、24位然后相加的结果,就是 i * 0x01010101 ,因为 i + (i << 8) + (i << 16) + (i << 24) = i * (1 + 1 << 8 + 1 << 16 + 1 << 24) = i * 0x01010101 。对于32位数字来说,左移之后超过32位的部分会被舍弃,低位补0,将左移之后得到的四个数字相加,结果的高8位的值就是原32位数包含的1的个数,要得到这个值,只需要将结果右移24位,将值放在低8位即可。

到这里,整个算法就结束了,右移的结果就是1的数量。在Redis中,BITCOUNT命令同时使用了查表法和VP-SWAR这两种方法。当要计算的位数小于128位时,使用查表法,否则使用VP-SWAR算法。其中查表法的做法是,程序先存一个256长度的表,按顺序记录从0-255(即 0b00000000 - 0b11111111) 数中二进制1的个数,然后对于输入参数每8位查一次表。

variable precision SWAR算法的更多相关文章

  1. variable-precision SWAR算法介绍

    BITCOUNT命令是统计一个位数组中非0进制位的数量,数学上称作:”Hanmming Weight“ 目前效率最好的为variable-precision SWAR算法,可以常数时间内计算出多个字节 ...

  2. variable-precision SWAR算法:计算Hamming Weight

    variable-precision SWAR算法:计算Hamming Weight 转自我的Github 最近看书看到了一个计算Hamming Weight的算法,觉得挺巧妙的,纪录一下. Hamm ...

  3. [算法]从一道题引出variable-precision SWAR算法

    苏君君出了一道题,是牛客网上面的: 输入一个int型整数,输出该数二进制表示中1的个数.其中负数用补码表示. 其实这道题并不难,大家很容易想到的解法是转成字符串的思路,即如下所示: public st ...

  4. redis bitcount variable-precision swar算法

    花了不到一周的时间看完了一本reids设计与实现的书,感觉整体的设计有些地方的确很巧妙,各个结构之间联系的非常紧密,但是很简单,逻辑性的没有太多,但是学到了一个bitcount计数1的方法比较巧妙,记 ...

  5. General Decimal Arithmetic 浮点算法

    General Decimal Arithmetic http://speleotrove.com/decimal/ General Decimal Arithmetic [ FAQ | Decima ...

  6. Redis设计与实现-附加功能

    发布与订阅 redis订阅分为精准的频道订阅与模糊匹配的模式订阅: redis将所有频道的订阅关系都保存在服务器状态pubsub_channels字典里,键是频道名,值是一个记录所有订阅此频道的客户端 ...

  7. IEEE论文格式要求

    0.特别提示:本次会议要求各位作者根据审稿意见进行认真修改,然后经过大会主席的检查合格才允许上传IEEE eXpress,主要的目的是为了保证论文集的质量,不让论文格式出现五花八门的情况,确保会议后被 ...

  8. MATLAB中digits和vpa

    digits: DIGITS Set variable precision digits. Digits determines the accuracy of variable precision n ...

  9. SIMD---AVX系列

    AVX全称Advanced Vcetor Extension,是对SSE的后续扩展,主要分为AVX.AVX2.AVX512三种.在目前常见的机器上,大多只支持到AVX系列,因此其他SIMD扩展指令我们 ...

随机推荐

  1. Spring基础(一)

    一.Spring简介 Spring框架性质是属于容器性质的.容器中装什么对象就有什么功能.所以可以一站式开发(springmvc+ioc+spring jdbc).核心是IOC(控制反转)和AOP(面 ...

  2. .NET Core API后台架构搭建

    ASP.NET Core API后台架构搭建 项目文件:https://files.cnblogs.com/files/ZM191018/WebAPI.zip 本篇可以了解到: 依赖注入 Dapper ...

  3. ES6入门之变量的解构赋值(二)

    前言 在上一章 ES6入门之let和const命令中我们对ES6的相关语法已经有了初步了解,上一章中我们主要学习了三大部分的内容,let命令的使用,块级作用域,const命令的使用,那么从本篇博客将进 ...

  4. 【OUC2019写作】论文写作第九小组英语常用表达整理

    第一部分:  一.简要综述以往和现在研究: 某方法被认为如何如何:it is well known that; it is regarded as; it is believed to ; It is ...

  5. 《深入理解Java虚拟机》-----第10章 程序编译与代码优化-早期(编译期)优化

    概述 Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把*.java文件转变成*.class文件的过程;也可能是指虚拟机的后端运 ...

  6. php架构师都要会什么

    架构师的成长离不开踩坑,不断试验各种方案,各种踩坑,从小坑到大坑,逐渐归纳.另外就是多学习多交流,兼收并蓄,不用特别在意细节,观其大略,了解常见的各种东西的核心价值与短板所在.一个程序和计算系统软件体 ...

  7. Javascript字符串常用方法详解

    字符串 字符串就是一个或多个排列在一起的字符,放在单引号或双引号之中. 'abc'"abc" length属性 js里的字符串类似于数组,都是一个一个字符拼凑在一起组成的,因此可以 ...

  8. 1.Linux-CentOS7.6安装教程

    了解Linux Linux 就是一个操作系统,主要为企业提供支持与服务. 学习Linux需要具备什么基础?能看懂中文,能看懂简单的 English ​ 1.什么是Linux? Linux:和我们常见的 ...

  9. 判断一字串String中是否包含某一串字符串

    String ostype = data.getString("osType").toUpperCase(); //转换为大写 if (ostype.contains(" ...

  10. go map数据结构和源码详解

    目录 1. 前言 2. go map的数据结构 2.1 核心结体体 2.2 数据结构图 3. go map的常用操作 3.1 创建 3.2 插入或更新 3.3 删除 3.4 查找 3.5 range迭 ...