【位操作】比特位计数(bit counting)之四【分治法】【3bit分组】右移位法【彻底打通任督二脉】【位运算】

前二篇博客都是对“比特位计数”【分治法】【2bit分组】算法的剖析,今天这篇博客则是要对一个“比特位计数”【分治法】【3bit分组】算法版本进行分析,这个算法的代码同样巧夺天工,非常经典。下面我们请主角闪亮登场,其源代码如下所示:

	/***“比特位计数”分治法【3bit】分组版本***/
	public static int bitsCount(int x) {
	    int n;
	    n = (x >>> 1) & 033333333333; //八进制表示法
	    x = x - n;
	    n = (n >>> 1) & 033333333333;
	    x = x - n;

	    x = (x + (x >>> 3)) & 030707070707;
	    return x % 63;
}

该算法是基于分治法的一个算法版本,这个分治法把32bit的整型数二进制编码,从右到左,以每三个相邻的bit位为一组划分为11组(最左侧一组实际上只有2个bit位,可以在左侧补一个0,凑成3bit),也就是把问题分解为11个子问题。然后各个子问题各自求解,子问题的解存放在各自的大小为3bit的位段空间里。然后,自左向右合并相邻两个子问题的解,得到的结果存放到各自的6bit位段空间里;最终6组粒度为6bit的解叠加合并,生成整个问题的解。

这个算法,如果不知道算法原理,其可读性是非常差的,极其难以理解。

算法原理:
在分析前先介绍一个知识点,是这个【分治法】【3bit分组】的算法原理:
用xyz表示一个长度为3bit的二进制编码整数(整数取值范围0-7),其中x、y、z都是二进制bit位,其取值都是0或1。则有以下公式成立:
xyz - (xyz>>1) - (xyz>>2) = x + y + z

在这里插入图片描述

32bit的整型数的二进制编码,以三个相邻的bit位为一组,可划分为11组。其中最高位(最左边)一组实际上只有2bit。如用yz来表示这组二进制编码,yz可以看作3bit的0yz。
0yz - (0yz>>1) - (0yz>>2) = 0 + y + z 等式也成立。
前提条件 这个算法有一个前提条件:向左移位时,左边补0,右边移出的bit位直接丢弃。这就需要有掩码“03”(八进制写法,对应二进制是0B011)的加持。
各组二进制编码,移位时要用掩码(&0B011)进行处理,以防止各组子问题求解时相互干扰。

三个步骤 分治法解决问题的三个步骤:
(1)问题分解:把问题分解为一组规模更小的相同或类似的子问题。
(2)子问题求解:求解子问题。
(3)问题解的合并(收集):对子问题解进行合并和收集,最后得到问题的解。

下面我们对这个【分治法】【3bit分组】的算法进行分析,算法可分为二部分:

第一部分:
第一部分共4行代码,实现了分治法中的前二个步骤(问题分解和子问题求解)

		/***第一部分的4行代码。实现分治法的前二个步骤***/
	    n = (x >>> 1) & 033333333333; //八进制表示法
	    x = x - n;
	    n = (n >>> 1) & 033333333333;
	    x = x - n;

代码中的033333333333是八进制表示法,是一个特殊掩码,实际上它是由不同数位上的11组小掩码“03”(八进制写法,对应二进制是0B011)组合而成。
特殊掩码033333333333其二进制表示为(去掉第一个0,刚好32位与整型数的粒度一样大小): 1101 1011 0110 1101 1011 0110 1101 1011

问题分解(分组)就是借助掩码(0B011)完成的,同时掩码可确保每组子问题移位时左边补0,右边移出丢弃。这个设计巧夺天工,精妙无比。

这4行代码,前2行代码,每个分组实现xyz - (xyz>>1) 部分的问题求解;后2行代码,则实现 ( xyz - (xyz>>1) ) - (xyz>>2) 部分的问题求解。

为了便于读者理解和观察,我们用8bit的整数来演示算法。假如方法bitsCount(int x)的输入参数为:0B0111 0101 。8个bit可分解成三个分组(第一个分组只有2bit),分别为:01 110 101。
下面是推演过程:
第一个推演图,演示的代码是: n = (x >>> 1) & 033333333333;
在这里插入图片描述
图中矩形中的1,是向右移位时丢弃的数位。
下面的第二个推演图,演示的代码是:x = x - n;
在这里插入图片描述
第三个推演图,演示的代码是: n = (n >>> 1) & 033333333333;
在这里插入图片描述

下面的第四个推演图,演示的代码也是:x = x - n;
在这里插入图片描述
最后,计算得到三个子问题的解:
在这里插入图片描述
考察输入参数x为8bit的整数:0B0111 0101 。x的初值:01 110 101 三组编码,它们的解正是:1,2,2。完全正确。至此,第一部分,问题分解和各子问题求解完成。

介绍完算法原理,我们再来用一个32位整型数来演示说明比特位计数过程。
假设输入参数为x=-1190294757,其十六进制表示:0XB90D8B1B‬ ,其二进制编码: 0B10111001000011011000101100011011。

读者若有兴趣,可参照上面算法原理,自行演算一下32位整型数的比特位计数过程。如下,是我的推演结果,当第一部分的最后一行“x = x - n;”计算完毕后各个子问题的解,如下所示:
x = x - n: 01 011 001 000 010 010 000 010 001 010 010
在这里插入图片描述

第二部分:二行代码,实现了分治算法的第三个步骤,问题解的合并(收集)。

	    /***第二部分的2行代码。实现分治法的前三个步骤:问题解的合并***/
	    x = (x + (x >>> 3)) & 030707070707;
	    return x % 63;

实际上,若输入是8位二进制码的整数,问题解的合并只需一行代码:return x % 7;

但对于32位的二进制整数,需要下面二行来实现问题解的合并:
x = (x + (x >>> 3)) & 030707070707;
return x % 63;

我们接力上文中32位整型数的比特位计数过程的11组子问题的解,进行推演说明。

先来推演第一行代码:x = (x + (x>>>3)) & 030707070707;//每6bit合并为3bit
这行代码可拆分为两部分:x = x&030707070707 + (x>>>3)&030707070707;
说明: 实际上,这样的分配律对于“按位与&”操作符是不正确的。读者可自行分析验证。因此,本算法中这行中规中矩的写法是拆分后的形式。之所以可以把掩码作为公共因子提取出来,是有前提条件的,即两组粒度为3bit的解合并后不会溢出(合并的解还是可用3bit来表示)。但提取公共因子有一个优点是可以减少一次按位与(&030707070707)操作,可提高效率,节省算力。

此代码的作用是将第一部分中得到的11组子问题的解,相邻两组解(粒度大小3bit)进行合并,并将解各自存放在3bit的位段空间里。设计得很巧妙。
030707070707是掩码,其二进制为:11 000 111 000 111 000 111 000 111 000 111。这个掩码实际上由1组“11”(这个设计天衣无缝,只可意会不可言传。虽知其意,但却难以分解清楚。)和5组“000 111”掩码合并而成。

现在,x中保存着11组3bit子问题的解:
在这里插入图片描述
代码中,x&030707070707 它的作用是把偶数分组的解掩盖掉。图解如下:
在这里插入图片描述
(x>>>3)&030707070707 的作用是先(x>>>3),再把奇数分组的解掩盖掉。图解如下:
在这里插入图片描述
其中,矩形框中的数据编码是右移时移出的比特位,直接丢弃了。

最后,把n1和n2相加,经过合并,把11组解合并为6组6bit(前3bit是全0)长的解。如下图所示:
在这里插入图片描述
运算符%(取模)的妙用

最后一行代码:return x % 63; 也是实现问题解的折叠合并功能。以前,我见识过利用运算符%(取模)实现循环功能,本算法中运算符%(取模)竟然还有如此妙用:折叠合并功能。
整数63,用二进制表示:0B111111 。“x % 63”图解示意图,如下:
在这里插入图片描述

最终折叠合并得到问题的解:010 000 换算为整数是16。

实际上,其效果与下面这行代码完全一样:
return ((x&0x3F) + ((x>>>6)&0X3F) + ((x>>>12)&0X3F) + ((x>>>18)&0X3F) + ((x>>>24)&0X3F) + ((x>>>30)&0X3F));

至此,比特位计数(bit counting)之四【分治法】【3bit分组】右移位法,全部剖释完毕。

以下是三种比特位计数(bit counting)分治法的比较测试例程:

/***
 * @author QiuGen
 * @description  比特位计数算法——分治法的右移位法【2bit】分组和【3bit】分组版本测试
 * 实现功能:演示分治法的右移位法【2bit】分组版本和【3bit】分组版本的比特位计数算法
 * @date 2024/6/4
 * ***/
/***比特位计数(bit counting)算法分治法系列测试***/
public class BitsCountBTest {
	/***分治法的右移位法【2bit】分组版本一***/
	public static int bitCountShiftRB (int i)
	{
	    i = (i & 0x55555555) + ((i >>> 1) & 0x55555555); //分解16组2bit
	    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); //每4bit合并为2bit
	    i = (i & 0x0F0F0F0F) + ((i >>> 4) & 0x0F0F0F0F); //每8bit合并为4bit
	    
	    i = (i * (0x01010101)) >> 24;  //版本一
	    //i = i % 255 ; //版本二 
	    //return ((i&0xFF) + ((i>>>8)&0XFF) + ((i>>>16)&0XFF) + ((i>>>24)&0XFF));
	    /***  版本三  ***
	    i = (i + (i >>> 8)) & 0x00FF00FF; //把每16bit合并为8bit
	    i = (i + (i >>> 16)) & 0x0000FFFF; //把每32bit合并为16bit(实际用到6bit)
	    /****/
	    /***  版本四 ***
	    i = (i + (i >>> 8));
	    i = (i + (i >>> 16));
	    i = i & 0x3f;
	    /****/
	    return i;
	}
	
	/***分治法的右移位法【2bit】分组版本二***/
	/***Integer类bitCount()方法***/
	public static int bitCount(int i) { 
		// HD, Figure 5-2
	    i = i - ((i >>> 1) & 0x55555555); //十六进制表示法
	    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
	    i = (i + (i >>> 4)) & 0x0f0f0f0f;
	    i = i + (i >>> 8);
	    i = i + (i >>> 16);
	    return i & 0x3f;
	}   /**分治法的右移位法【2bit】分组版本二**/


	/***分治法【3bit】分组版本***/
	public static int bitsCount3bit(int x) {
	    int n;
	    n = (x >>> 1) & 033333333333; //八进制表示法
	    x = x - n;
	    n = (n >>> 1) & 033333333333;
	    x = x - n;
	    x = (x + (x >>> 3)) & 030707070707; 

	    return x % 63;
	}
	
	public static void printXB(int x) {
		System.out.println("x="+x + "  bitCount比特位计算值:"+bitCount(x));
		System.out.println("x="+x + "  bitsCount3bit比特位计算值:"+bitsCount3bit(x));
		System.out.println("x="+x + "  bitCountShiftRB比特位计算值:"+bitCountShiftRB(x));
	}

	public static void main(String[] args) {
		int x = 0B01111010010101010010000111110010;
		printXB(x);
		x = 13;
		printXB(x);
		x = 39;
		printXB(x);
		x = 377;
		printXB(x);
		x = 0XB90D8B1B;
		printXB(x);
	}

}

三种分治法的测试结果是一致的,请看测试效果图:
在这里插入图片描述

参考文献:
【位操作笔记】位计数算法 分治法统计 5 一组3bit的版本

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值