如何查找32位缓冲区中是否有n个连续的设置位?

时间:2012-08-21 11:02:50

标签: algorithm bit-manipulation

也许你可以帮助我解决以下问题,这可以帮助我加速我正在考虑的内存管理器(我不确定是否存在解决方案 - 我没有找到解决方案)。

我有一个32位寄存器,我需要查找其中是否有n个连续的设置位,如果是,那么它们的偏移量是多少。例如,如果寄存器保持以下值111100000000000000000001111111000且n等于4 - 接受以下任何答案(偏移量从0开始):

  

3,4,5,6,28

我所拥有的原子操作是所有常规的按位运算(&,|,〜,...),并且还找到最低有效位偏移(上面的寄存器中为3)。算法(假设存在一个) - 应该不超过5个原子操作。

4 个答案:

答案 0 :(得分:3)

如果存在执行该算法的算法,则最差情况复杂度至少为O(m-n),其中m是寄存器中的位数,n是数字你正在寻找的连续设置位。这很容易看到,因为如果设置了所有位,您的算法将必须输出精确的m-n项,因此它的复杂性不能低一些。

修改

这里有类似问题的优雅解决方案Looping through bits in an integer, ruby,找到了1序列的长度。

如果您事先知道要查找的游戏的n长度,则此算法仅需n个步骤。然后可以在约5个步骤中从算法的前一步骤中的尾随零的数量恢复偏移。这不是非常有效,但可能比循环解决方案更好,特别是对于小n

编辑2

如果事先知道n,我们可以为它找出一系列必要的变化。例如。如果我们正在寻找7位运行,那么我们必须做

x &= x >> 1
x &= x >> 3
x &= x >> 1
x &= x >> 1

重点是,如果n/2为偶数,我们会向右移n位;如果n为奇数,我们会向右移{1,然后相应地更新nn = n - 1 or n = n / 2 },正如@harold建议的那样。动态估算这些值会很昂贵,但如果我们预先计算它们,那么效率会非常高。

编辑3

更好的是,对于任何n,只要在ceil(log(2,n))floor(n/2)之间,无论我们采取哪种转变,都需要完全2^floor(log(2,n-1))步骤。见下面的评论。

答案 1 :(得分:1)

Qnan发布的链接显示了一般案例的优雅解决方案。

对于m的特定值,可以进一步优化。

例如,对于m == 4,您可以这样做:

x &= (x >> 1);
x &= (x >> 2);
// at this point, the first bit set in x indicates a 4 bit set sequence.

对于m == 6:

x &= (x >> 1);
x &= (x >> 1);
x &= (x >> 3);

最后,这只会减少到因子m。

<强>更新

另请注意,对于较高的值,在每个可能的位置检查位序列实际上可能更便宜。

例如,对于m = 23,模式只能从0到9的位置开始。

答案 2 :(得分:0)

对于每个可能的字节值(0-255),计算开始时的位数,结束时的位数和字节内连续位的最长数以及该序列的偏移量。例如,对于0x11011101,开头有2位,末尾有1位,连续有3个连续位。

将此值存储在4个数组中,例如startendlongestlongest_offset

然后,将32位数视为4字节数组,并按如下方式迭代这些字节:

int search_bit_sequence(uint32 num, int desired) {
  unsigned char *bytes = (unsigned char *)&num;
  int i, acu;
  for (acu = i = 0; i < 4; i++) {
    int byte = bytes[i];
    acu += start[byte];
    if (acu >= desired)
      return (i * 8 - (acu - start[byte]));

    if (longest[byte] >= desired)
      return ( i * 8 + longest_offset[byte]);

    if (longest[byte] < 8)
      acu = end[byte];
  }
  return -1; /* not found */
}

更新:请注意,CPU的字节顺序可能需要更改循环方向。

答案 3 :(得分:0)

我检查了this question and answers并提出了以下想法。

int i = n-1;
uint32_t y = x;
while(y && i--) {
    y = y & (y << 1);
};

如果有y个连续设置位,则在上述操作之后n非零。接下来要做的是找到那里设置的最不重要的值。以下代码将删除除最不重要之外的所有设置位。

z = y - (y & (y-1));

现在我们只有一位设置,我们需要找到该位的位置。我们可以使用带有32个案例的switch语句。

static inline int get_set_position(const uint32_t z) {
    switch(z) {
        case 0x1:
            return 0;
        case 0x2:
            return 1;
        ....
        .... // upto (1<<31) total 32 times.
    }
    return -1;
}

最后,为了得到结果,我们需要减少n-1。所以总程序如下。

static inline int get_set_n_position(const uint32_t x, const uint8_t n) {
    if(!n) return -1;
    int i = n-1;
    uint32_t y = x;
    while(y && i--) {
        y = y & (y << 1);
    };
    if(!y) return -1;
    uint32_t z = y - (y & (y-1));
    if(!z) return -1;
    int pos = get_set_position(z);
    if(pos < 0) return -1;
    assert(pos >= (n-1));
    return pos - (n-1);
}

现在有人担心大端。我想我只需要更改big-endian的get_set_position()以使其工作(假设连续的设置位定义根据字节序更改)。

让我分享一个使用gcc提供的builtin_ctzl的测试代码。

OPP_INLINE int get_set_n_position(BITSTRING_TYPE x, const uint8_t n) {
    if(!n || n > BIT_PER_STRING) return -1;
    int i = n-1;
    while(x && i--) {
        x = x & (x << 1);
    };
    if(!x) return -1;
    int pos = __builtin_ctzl(x);
    return pos - (n-1);
}

代码在O(1)时间内工作,因为32是常量(如@Qnan注意到的)。如果寄存器的大小不同,它也可以在O(n)中工作。

注意:由于评论和单元测试,我修复了错误。