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

分治算法
分治算法,字面意思就是“分而治之”,就是把一个复杂的问题分解成两个或多个相同或相似的子问题,再把子问题分成更小的子问题,……,直到最后子问题可以直接求解,原问题的解就是所有子问题解的合并。
在计算机科学中,分治法就是运用分治思想的一种很重要的算法。分治法是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)等等。

分治法适用场景和应用分治法解决的问题一般步骤:
(1)问题可分解:问题可以分解为一组规模更小的相同或类似的子问题,子问题再分解为更小的子问题。例如,汉诺塔(Tower of Hanoi)问题。
(2)子问题求解:子问题容易求解,对子问题求解。
(3)问题解的合并(收集):对子问题解进行合并和收集,最后得到问题的解。

分治法的关键是问题解的合并。解的合并,没有统一的模式,具体问题要具体分析,以获得较好的合并算法。

比特位计数基于分治原理的右移位算法版本

基于分治原理的右移位算法的比特位计数版本,其中用到的位运算技巧非常精妙,令人叹为观止。
比特位计数基于分治法的右移位法版本的源代码,如下所示:

	/***比特位计数基于分治法的右移位法版本***/
	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;  //解的合并和收集
	    return i; //返回语句
	}

基于分治原理的右移位算法版本剖析:
该算法是基于分治法的右移位算法版本。要用到右移位算法的知识点,我们先来复习一下,上篇博客介绍的右移位算法版本,
【位操作】比特位计数(bit counting)之一【位运算】【汉明重量】【汉明距离】
其代码如下所示:

	/***比特位计数的右移位法版本***/
	public static int bitCountShiftR (int n)
	{
	      int count=0;    
	      while (n!=0)
	      {  
	         count += n & 0B1 ; //关键代码,其中0B1=1
	         n >>= 1 ;
	      }
	      return count ;
	}

比特位计数其原始的右移位版本的关键代码是count += n & 0B1 ;
关键代码中的0B1(二进制写法),可以有多种写法:0X1(十六进制)、01(八进制)或1(自然数)。但用0B1最本质,也最直观。
这行关键代码又可分解为:
int tmp = n & 0B1 ; //这里“n & 0B1”是关键中的关键
count += tmp;

分治法的右移位算法版本的算法实际上只有二部分,问题分治和解的合并。我们下面逐一进行分解剖析:
问题分治
第一行代码:i = (i & 0x55555555) + ((i >> 1) & 0x55555555);
算法中就用了这一行代码,实现了问题的分治和求解。
这里的分治法是把一个整型数32bit位长的二进制编码分解为16组2bit位长的二进制编码进行处理,一行代码完成了分治和求解。
这里的0x55555555 用二进制编码表示为0101 0101 0101 0101 0101 0101 0101 0101,一共是16组01。每组01实际上是一种特殊的分组掩码。
(i&0x55555555)处理了每组2bit右边(奇数位)的比特位,实现“n & 0B1”的功能。
((i>>1) & 0x55555555) 通过(i>>1)向右移位处理了每组2bit左边(偶数位)的比特位,实现“n & 0B1”的功能。
而且 i = (i&0x55555555) + ((i>>1) & 0x55555555); 刚好还把各组求解结果保存在各自的地盘里。这种设计真的是鬼斧神工,天衣无缝。

我们用一个实际例子来演示算法:输入参数 i=2052399602
正整数i=2052399602 用二进制编码表示:int i = 0B01111010010101010010000111110010;
详细的演算分析如下图所示:
图解分治
我们可以考察每一组的解,如i的最左边的4组(8位二进制码)是:“0111 1010”其子问题的解分别为01 10 01 01,各自转换为整数为1、2、1、1,正好是比特位为1的位数。
表格列出了16组子问题的解,S1表示第1组解,S2是第2组解,共16组。
问题的16组解

解的合并
解的合并共有3行代码,也就是分三个步骤:
i = (i & 0x33333333) + ((i >> 2) & 0x33333333); //每4bit合并为2bit
i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F); //每8bit合并为4bit
i = (i * (0x01010101)) >> 24; //解的合并和收集

前2行代码,其处理方法类似,只是粒度不同。第3行处理方式不同。

步骤一:i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
i的初始值:0110 0101 0101 0101 0001 0001 1010 0001
0x33333333的二进制值:0011 0011 0011 0011 0011 0011 0011 0011
0x33333333可看作8组0x3(0011)掩码。每组掩码处理2组2bit数据,处理的数据粒度为2bit。
在这里插入图片描述
然后i>>2先移位,再把偶数组解给掩掉,得到“结果②”。最后2个结果进行合并(相加)。
在这里插入图片描述
“解的合并”步骤一的具体分析演示如下图所示:
在这里插入图片描述

上图,请考察一下合并前i的左边4个bit位(2组):0110;合并后则为:0011。
这2组解分别为01和10相当于整数值为1和2;合并后为0011,其值刚好是1+2=3。

步骤二:i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);
现在,i的初始值为: 0011 0010 0010 0010 0001 0001 0100 0001
0x0F0F0F0F的二进制值: 0000 1111 0000 1111 0000 1111 0000 1111。
0x0F0F0F0F可看作4组0x0F(0000 1111)掩码。每组掩码处理2组4bit数据,处理的数据粒度为4bit。
请读者自行参照“步骤一”进行分析(算法完全相同,只是处理的数据粒度从2bit增加到4bit)。

最终把8组4bit粒度的解两两合并成4组,并保存在各自的8bit(实际有效值在低4bit里)的位段空间里。
步骤二完成时,i的值为:0000 0101 0000 0100 0000 0010 0000 0101

步骤三:i = (i * (0x01010101)) >> 24;
我们来分析这一行代码,这个算法与前面的完全不同,这是4组8bit解的合并。
这里i的初值为:0000 0101 0000 0100 0000 0010 0000 0101
因为0x01010101 = 0x01000000 + 0x00010000 + 0x00000100 + 0x00000001,所以
i * (0x01010101) = i * (0x01000000 + 0x00010000 + 0x00000100 + 0x00000001)
知识点:乘法与移位的关系
乘法与移位的关系

所以,i * (0x01010101) = i <<24 + i <<16 + i <<8 +i;
根据Java的向左移位规则,向左移n位时,移出的高位丢弃,右边补n位0。

下图演示了 i * (0x01010101) 运算:
在这里插入图片描述
图中圆角矩形中的0,是移位时,右边补n位0得到的。左边移出的高位都丢弃了。为了让读者看得更明白,我们先不把这4个值加起来。因为i = (i * (0x01010101)) >> 24; 等价于 i = ((i<<24) + (i<<16) + (i<<8) +i)>>24;
最后的右移“>>24”操作,获得的最终结果,是问题的解。下图是算法演示,:
在这里插入图片描述
最后得到结果,正整数i=2052399602的比特位计数值为16(0001 0000)。
说明:上图中,圆角矩形中的0,都是正整数“x>>24”右移时左边补24位0得到的。对于正整数,按位右移“>>”与按位无符号右移“>>>”等价。右边移出的低位都被丢弃。
算法最后一条返回语句:return i;
有的读者可能弄不明白,为什么i就是问题的答案?其实更严谨的写法是:return i&0x3f;
但其结果与“return i;”完全一致。从图解中可看到,实际上i的高位都是0,实际上只有最后6个比特位才是它的有效值,本例中为“010000”。返回语句:“return i;”是优化写法,可以省算力提高效率。

扩展说明
步骤三的合并代码:i = (i * (0x01010101)) >> 24;
这行代码可以有多种写法,效果完全相同,但是效率有所不同:
(1)写法一:(算法原理与步骤一相同,只是数据粒度倍增。将在下篇【2bit】分组版本博客中剖析))
i = (i + (i >>> 8)) & 0x00FF00FF; //把每16bit合并为8bit
i = (i + (i >>> 16)) & 0x0000FFFF; //把每32bit合并为16bit(实际用到6bit)
(2)写法二,这是写法一的优化算法(其原理将在下篇【2bit】分组版本博客中剖析):
i = i + (i >>> 8); //对于正整数用“>>”和“>>>”是等价的。
i = i + (i >>> 16);
i = i & 0x3f; //0x3f的二进制表示0B0011 1111
(3)写法三,这也是一种优化算法(其原理将在后文【3bit】分组版本博客中剖析):
i = i % 255 ; //255是0xFF其二进制表示0B1111 1111
这种算法是基于“运算符%(取模)”奇妙的折叠合并功能。
因为有前文提到的前提条件:最终把8组4bit粒度的解两两合并成4组,并保存在各自的8bit(实际有效值在低4bit里)的位段空间里。
算式中255,其实也是一个特殊的掩码,其二进制表示为0B1111 1111。
i % 255 实际作用竟然可把4组8bit的解,折叠合并起来,得到问题的最终解。
其效果如右式: ((i&0xFF) + ((i>>>8)&0XFF) + ((i>>>16)&0XFF) + ((i>>>24)&0XFF));
其图解从略,也可参见“后文【3bit】分组版本博客中剖析”。

下面是完整的测试演示程序:

/***比特位计数(bit counting)算法分治法系列***/
/***
 * @author QiuGen
 * @description  比特位计数算法——分治法的右移位法【2bit】分组版本bitCountShiftRB (int i)
 * 实现功能:演示分治法的右移位法【2bit】分组版本的比特位计数算法
 * @date 2024/5/15
 * ***/
public class BitsCountB {
	/***比特位计数的右移位法算法版本***/
	public static int bitCountShiftR (int n)
	{
	      int count=0;    
	      while (n!=0)
	      {  //这里的0X1,也可用01,0B1或1。
	         count += n & 0B1 ; //0X1
	         n >>= 1 ;
	      }
	      return count ;
	}
	
	/***分治法的右移位法【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 ; //版本二
	    /***  版本三  ***
	    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;
	}
	
	public static void printXB(int x) {
		System.out.println("x="+x + "  bitCountShiftR比特位计算值:"+bitCountShiftR(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);
	}
}

测试结果图:
在这里插入图片描述

参考文献:
汉明重量(Hamming Weight)以及 redis的bitcout底层

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值