位运算获取最接近的2指数

1,160 阅读3分钟

问题背景 HashMap源代码里面有这样一个方法,计算一个整数向上最接近的2指数,其实现过程如下,可以说是非常巧妙。

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

以上代码的逻辑实现过程不是很明显,但是换一种思路来理解之后,便豁然开朗。

二进制形式 首先来看一个整数的二进制表示形式,为了简化说明过程,使用8位整数,Java语言的32位整形同样适用:

0100 1101

要获取这个整数的最接近的2指数,只需要找到其最高位所在位置,然后在该位基础上往左一位,得到结果为:

1000 0000

根据上述原则,以下计算可以非常快速的出结果:

0010 1111 --> 0100 0000
0010 1001 --> 0100 0000
0010 0001 --> 0100 0000
0001 1111 --> 0010 0000
0001 1011 --> 0010 0000
...

但是,有一种情况例外,即当该整数原本就是2的指数时,那么其本身就是最接近的2指数,即: 0001 0000 --> 0001 0000 1000 0000 --> 1000 0000 0000 0001 --> 0000 0001 ...

为了协调这种情况,一个小技巧就是,直接将原来的数减1,然后采用之前的方法:

0001 0000 --> 0000 1111 --> 0001 0000
1000 0000 --> 0111 1111 --> 1000 0000
0100 1111 --> 0100 1110 --> 1000 0000
0010 0001 --> 0010 0000 --> 0100 0000
...

代码实现逻辑 有了这个思路之后,代码实现需要解决的问题就是,如何定位到最高位1所在位置,hashmap中的方法非常巧妙,通过采用右移位的方式,实现了这种目的。

// 待计算的数值(79)为:
0100 1111

// 1、先减去1 :int n = cap - 1;
0100 1110
// 2、右移1位后,与自身进行位或运算:n |= n >>> 1;
 0100 1110
  010 0111
 011X XXXX  //此处部分位置结果用X表示,是因为其取值对最终结果没有影响,为了方便理解
// 右移2位后,与自身进行位或运算:n |= n >>> 2;
011X XXXX
 001 1XXX
0111 1XXX  //此处部分位置结果用X表示,是因为其取值对最终结果没有影响,为了方便理解,
// 右移4位后,与自身进行位或运算:n |= n >>> 2;
0111 1XXX
 000 0111
0111 1111  
// 3、最终结果加1,得到结果为128

其他实现方式 除了上述方法之外,也可以采用如下方式:

static final int tableSizeFor2(int cap) {
    int carry = 0;
    for (int c = cap - 1; c > 0; c >>>= 1) {
        carry++;
    }
    return cap == 0 ? 1 : (1 << (carry));
}