了解使用二进制操作数计算二进制数中1的数量的函数

时间:2015-04-30 02:02:26

标签: c binary-operators

最近,我遇到了这个问题,要求您返回二进制字符串中连续数字的数量,从左到右(所以11101001 = 3011111 = 0,{{1等等)。问题是只使用二进制操作数,而没有任何循环。

在网上浏览了一下之后(感谢Google),我找到了其他人提出的解决方案,并希望更好地帮助理解它。

111101 = 4

从我所知道的,该函数显然检查了32位整数的前16位,然后根据它们是否都是1来进入下一位,然后接下来的4位取决于它们是否全是1,然后是2,等1,等等。

不幸的是,我有点迷失代码是如何完成这一点的,并希望得到一些帮助理解。

例如,在这里:

int leftBitCount(int x) {
    int v = x;
    int r;  // store our result here
    int shift;
    int full = !(~x); // we must add one if we have 0xffffffff

    // Check the top 16 bits and add them to our result if they exist
    r     = !(~(v>>16)) << 4;
    v   <<= r;
    // check the remaining 8 bits
    shift = !(~(v >> 24)) << 3;
    v   <<= shift;
    r    |= shift;
    // remaining 4 bits
    shift = !(~(v>>28)) << 2;
    v   <<= shift;
    r    |= shift;
    // remaining 2 bits
    shift = !(~(v >> 30)) << 1;
    v   <<= shift;
    r    |= shift;
    // remaining 1 bits
    r    ^= 1&((v>>31));

    // remember to add one if we have 32 on bits
    return r + full;
}

我可以看到r = !(~(v>>16)) << 4; v <<= r; 正在被转移和否定,但我不知道这有助于解决任何事情。

2 个答案:

答案 0 :(得分:2)

r = !(~(v>>16)) << 4;

以下是此行发生的事情:

  1. v >> 16v(此时此值为x)的值移至右侧16个位置。这个表达式的低16位是v的前16位,有趣的是,这个表达式的前16位是实现定义的。我曾经使用过的C编译器已将签名值的右移作为算术移位,而您发布的代码似乎依赖于这一事实,但并不一定如此。有关详细信息,请参阅this question的答案。

  2. ~运算符执行步骤1中生成的值的按位补码。此处的重要观察是,当且仅当(a)v的前16位全部1s,和(b)正在使用的C编译器执行算术移位,然后 all 步骤1中表达式的位将为1s,这意味着~(v>>16)将为零。< / p>

  3. !运算符对步骤2中生成的值执行逻辑否定,这意味着当且仅当步骤2中的值为零时,!(~(v>>16))将为1(表示v的前16位都是1。否则,该值将为零。

  4. 最后,<< 4只是乘以16.如果r的前16位设置为1,则为x赋值16,或者如果前16位中的任何一位为0(或者如果正在使用的C编译器执行逻辑右移),则值为零。

  5. 接下来,这一行:

    v <<= r;
    

    如果v的前16位全部为1 - 意味着r的值为16 - 那么我们就不需要再检查这些位了,所以这一行换了{16}左边的v,它基本上丢弃了我们已经检查过的前16位,并将8个次最重要的位置于最高位置,因此我们可以检查下一个位。

    另一方面,如果v的前16位所有1s - 意味着r的值为0 - 那么底部 v的16位变得无关紧要,因为无论v中前导1位的数量是多少,我们都知道它必须小于16.所以在这种情况下,{{1保持v <<= r的值不变,我们通过检查v中的八个原始最顶部位继续下一步。

    算法的其余部分基本上重复这些步骤,除了每个步骤中被检查的位数减半。所以这段代码检查前8位:

    v

    这里唯一不同的是shift = !(~(v >> 24)) << 3; v <<= shift; r |= shift; 正在使用逻辑OR进行扩充,而不是像在16位步骤中那样直接分配。在这种情况下,这在功能上等同于加法,因为我们知道r的值先前为0或16,而r的值同样为0或8,并且这些值都不是&# 39; 1位出现在相应的位置。然后这位代码与前4位相同:

    shift

    然后是前2:

    shift = !(~(v>>28)) << 2;
    v <<= shift;
    r |= shift;
    

    最后,前1:

    shift = !(~(v >> 30)) << 1;
    v <<= shift;
    r |= shift;
    

    这样测试的比特数是16 + 8 + 4 + 2 + 1 = 31,这显然是原始值中总比特数的一个,因此在所有32比特都是的情况下是不够的。 1秒。假设所有32位都是1,那么r ^= 1&((v>>31)); 的原始值将在每一步中移位:

    x

    到目前为止看到的代码从未考虑过的是W位,即原始值的最低有效位。当且仅当左侧的所有位都是1时,该位​​将成为结果的一部分,这意味着该值中的所有位都是1,这是在此处执行检查的目的。代码顶部:

    16-bit step: 123456789ABCDEFGHIJKLMNOPQRSTUVW
     8-bit step: HIJKLMNOPQRSTUVW----------------
     4-bit step: PQRSTUVW------------------------
     2-bit step: TUVW----------------------------
     1-bit step: VW------------------------------
    

    这与我们之前看到的按位补码和逻辑否定的组合相同;当且仅当 int full = !(~x); 中的所有位都为1时,~x将为零,因此x在这种情况下为1,在任何其他情况下为零。

答案 1 :(得分:1)

好,

    int full = !(~x); // we must add one if we have 0xffffffff

按位求反运算符(~)翻转其操作数中的所有凹坑。如果设置了操作数中的所有位,则结果为零,逻辑否定运算符(!)将转换为值1(true)。否则,逻辑否定将其操作数转换为0(false)。

    // Check the top 16 bits and add them to our result if they exist
    r     = !(~(v>>16)) << 4;

假设为32位宽的操作数v被右移,因此其上半部分的结尾位于结果的最后16位。看起来代码假设(不安全地)如果符号位被置位(即算术移位),空出的位将是一次填充,因此如果原始数字的前16位全部被设置,那么按位否定产生零,逻辑否定转换为1.左移1(== 2 ^ 0)四位产生16(== 2 ^ 4),这是所讨论的位数。如果不是所有前16位都是1(或者某些但不是所有这些位都被设置,并且右移位以零而不是1),那么最终会得到0 << 4,这当然是0

    v   <<= r;

我们刚刚计算的位数(16或0)向左移位。我们现在考虑原始顶部位的较小块或其他位的较小块,现在转移到顶部位置。

    shift = !(~(v >> 24)) << 3;
    v   <<= shift;

与以前相同的游戏,但现在我们只考虑一个8位的块。

    r    |= shift;

将刚测量的位数(8或0)添加到结果中。

其余的继续以相同的模式,到这里:

    r    ^= 1&((v>>31));

如果v的顶部位置位,它只会翻转结果的最后一位;从那时起,代码未设置该位,^=可以很容易地+=|=。然后我们有:

    return r + full;

请记住,如果参数设置了所有位,则full取值为1,否则为零。上面的代码已经用尽了它一次设置r的一个位所能发挥的所有技巧,因为它刚刚完成了最低有效位。如果输入值中确实还有一个位设置,则需要使用+运算符将结果加1或模拟6位加法器。

请注意:此代码似乎依赖于某些实现定义的行为来完成其工作。您可以通过使用无符号整数而不是有符号整数来避免实现定义。你仍然需要稍微修改一下表达式,但我把它留给你来解决。