在int中找到第n个SET位

时间:2011-10-05 23:51:12

标签: algorithm function binary

我想找到n最低设置位的位置,而不仅仅是最低设置位。 (我谈论n位置的价值)

例如,说我有:
0000 1101 1000 0100 1100 1000 1010 0000

我想找到第4位设置。然后我想要它返回:
0000 0000 0000 0000 0100 0000 0000 0000

如果popcnt(v) < n,如果此函数返回0会有意义,但此案例的任何行为对我来说都是可以接受的。

如果可能的话,我正在寻找比循环更快的东西。

7 个答案:

答案 0 :(得分:23)

现在来自BMI2 instruction setPDEP非常容易。这是一个带有一些示例的64位版本:

#include <cassert>
#include <cstdint>
#include <x86intrin.h>

inline uint64_t nthset(uint64_t x, unsigned n) {
    return _pdep_u64(1ULL << n, x);
}

int main() {
    assert(nthset(0b0000'1101'1000'0100'1100'1000'1010'0000, 0) ==
                  0b0000'0000'0000'0000'0000'0000'0010'0000);
    assert(nthset(0b0000'1101'1000'0100'1100'1000'1010'0000, 1) ==
                  0b0000'0000'0000'0000'0000'0000'1000'0000);
    assert(nthset(0b0000'1101'1000'0100'1100'1000'1010'0000, 3) ==
                  0b0000'0000'0000'0000'0100'0000'0000'0000);
    assert(nthset(0b0000'1101'1000'0100'1100'1000'1010'0000, 9) ==
                  0b0000'1000'0000'0000'0000'0000'0000'0000);
    assert(nthset(0b0000'1101'1000'0100'1100'1000'1010'0000, 10) ==
                  0b0000'0000'0000'0000'0000'0000'0000'0000);
}

答案 1 :(得分:10)

事实证明,没有循环确实可以做到这一点。预先计算此问题的(至少)8位版本是最快的。当然,这些表占用了缓存空间,但几乎在所有现代PC场景中仍然应该有净加速。在此代码中,n = 0返回最小设置位,n = 1是第二个,等等

使用__popcnt解决方案

有一个使用__popcnt内在的解决方案(你需要__popcnt非常快,或者通过一个简单的循环解决方案的任何性能提升都没有实际意义。幸运的是大多数SSE4 +时代处理器都支持它。)

// lookup table for sub-problem: 8-bit v
byte PRECOMP[256][8] = { .... } // PRECOMP[v][n] for v < 256 and n < 8

ulong nthSetBit(ulong v, ulong n) {
    ulong p = __popcnt(v & 0xFFFF);
    ulong shift = 0;
    if (p <= n) {
        v >>= 16;
        shift += 16;
        n -= p;
    }
    p = __popcnt(v & 0xFF);
    if (p <= n) {
        shift += 8;
        v >>= 8;
        n -= p;
    }

    if (n >= 8) return 0; // optional safety, in case n > # of set bits
    return PRECOMP[v & 0xFF][n] << shift;
}

这说明了分而治之的方法是如何运作的。

常规解决方案

还有一种“通用”架构的解决方案 - 没有__popcnt。它可以通过8位块处理来完成。您还需要一个查找表来告诉您一个字节的popcnt:

byte PRECOMP[256][8] = { .... } // PRECOMP[v][n] for v<256 and n < 8
byte POPCNT[256] = { ... } // POPCNT[v] is the number of set bits in v. (v < 256)

ulong nthSetBit(ulong v, ulong n) {
    ulong p = POPCNT[v & 0xFF];
    ulong shift = 0;
    if (p <= n) {
        n -= p;
        v >>= 8;
        shift += 8;
        p = POPCNT[v & 0xFF];
        if (p <= n) {
            n -= p;
            shift += 8;
            v >>= 8;
            p = POPCNT[v & 0xFF];
            if (p <= n) {
                n -= p;
                shift += 8;
                v >>= 8;
            }
        }
    }

    if (n >= 8) return 0; // optional safety, in case n > # of set bits
    return PRECOMP[v & 0xFF][n] << shift;
}

当然,这可以通过循环完成,但是展开的表单更快,循环的不寻常形式会使编译器不太可能自动为您重新打印它。

答案 2 :(得分:9)

v-1有一个零,其中v具有最低有效“1”位,而所有更高有效位都相同。这导致以下功能:

int ffsn(unsigned int v, int n) {
   for (int i=0; i<n-1; i++) {
      v &= v-1; // remove the least significant bit
   }
   return v & ~(v-1); // extract the least significant bit
}

答案 3 :(得分:1)

我看不到没有循环的方法,会让人想到的是什么;

int set = 0;
int pos = 0;
while(set < n) {
   if((bits & 0x01) == 1) set++;
   bits = bits >> 1;
   pos++;
}

之后, pos 将保持第n个最低值设置位的位置。

我能想到的另一件事就是分而治之的方法,它可能产生O(log(n))而不是O(n)......但可能不会。

编辑:你说任何行为,所以非终止是好的,对吧? :P

答案 4 :(得分:1)

def bitN (l: Long, i: Int) : Long = {
  def bitI (l: Long, i: Int) : Long = 
    if (i == 0) 1L else 
    2 * { 
      if (l % 2 == 0) bitI (l / 2, i) else bitI (l /2, i-1) 
    }
  bitI (l, i) / 2
}

递归方法(在scala中)。如果modulo2为1,则递减i,位置。返回时,乘以2.由于乘法作为最后一个操作被调用,因此它不是尾递归,但由于Longs事先已知大小,所以最大堆栈不是太多大。

scala> n.toBinaryString.replaceAll ("(.{8})", "$1 ")
res117: java.lang.String = 10110011 11101110 01011110 01111110 00111101 11100101 11101011 011000

scala> bitN (n, 40) .toBinaryString.replaceAll ("(.{8})", "$1 ")
res118: java.lang.String = 10000000 00000000 00000000 00000000 00000000 00000000 00000000 000000

答案 5 :(得分:1)

我知道这个问题要求的东西比循环更快,但复杂的无循环答案可能比快速循环需要更长的时间。

如果计算机有 32 位 On 并且 int 是一个随机值,那么它可能有例如 16 个,如果我们正在 16 个中寻找一个随机位置,我们通常可能正在寻找第 8 个。只用几条语句在一个循环中循环 7 或 8 次还不错。

v

循环通过删除最低设置位 (n-1) 次来工作。 将被移除的第 n 位就是我们要寻找的一位。

如果有人想测试这个......

int findNthBit(unsigned int n, int v)
{
    int next;
    if (n > __builtin_popcount(v)) return 0;
    while (next = v&v-1, --n) 
    {
        v = next;
    }
    return v ^ next;
}

如果担心循环执行的次数,例如如果函数被定期调用时使用较大的 #include "stdio.h" #include "assert.h" // function here int main() { assert(findNthBit(1, 0)==0); assert(findNthBit(1, 0xf0f)==1<<0); assert(findNthBit(2, 0xf0f)==1<<1); assert(findNthBit(3, 0xf0f)==1<<2); assert(findNthBit(4, 0xf0f)==1<<3); assert(findNthBit(5, 0xf0f)==1<<8); assert(findNthBit(6, 0xf0f)==1<<9); assert(findNthBit(7, 0xf0f)==1<<10); assert(findNthBit(8, 0xf0f)==1<<11); assert(findNthBit(9, 0xf0f)==0); printf("looks good\n"); } 值,可以简单地添加以下形式中的一两行

n

if (n > 8) return findNthBit(n-__builtin_popcount(v&0xff), v>>8)  << 8;

这里的想法是第 n 个永远不会位于底部的 n-1 位。更好的版本不仅会清除底部的 8 位或 12 位,还会清除所有底部 (n-1) 位(当 n 为大值且我们不想循环那么多次时)。

 if (n > 12) return findNthBit(n - __builtin_popcount(v&0xfff),  v>>12) << 12;

我用 if (n > 7) return findNthBit(n - __builtin_popcount(v & ((1<<(n-1))-1)), v>>(n-1)) << (n-1); 对此进行了测试,并在清除了底部 19 位之后,因为在那里找不到答案,它通过循环 4 次来查找剩余位中的第 5 位以删除4 个。

所以改进版是

findNthBit(20, 0xaf5faf5f)

值 7,限制循环被相当随意地选择为限制循环和限制递归之间的折衷。可以通过删除递归并跟踪移位量来进一步改进该功能。如果我能从在家上学我的女儿获得一些平静,我可以试试这个!

这是一个最终版本,通过跟踪从被搜索位的底部移出的低阶位的数量来删除递归。

最终版本

int findNthBit(unsigned int n, int v)
{
    int next;
    if (n > __builtin_popcount(v)) return 0;
    if (n > 7) return findNthBit(n - __builtin_popcount(v & ((1<<(n-1))-1)), v>>(n-1)) << (n-1);
    while (next = v&v-1, --n) 
    {
        v = next;
    }
    return v ^ next;
}

答案 6 :(得分:0)

在Jukka Suomela给出的答案的基础上,使用可能不一定可用的机器特定指令,也可以编写一个与_pdep_u64完全相同的函数,而不依赖于任何机器。它必须遍历其中一个参数中的设置位,但仍可以描述为C ++ 11的constexpr函数。

constexpr inline uint64_t deposit_bits(uint64_t x, uint64_t mask, uint64_t b, uint64_t res) {
    return mask != 0 ? deposit_bits(x, mask & (mask - 1), b << 1, ((x & b) ? (res | (mask & (-mask))) : res)) : res;
}

constexpr inline uint64_t nthset(uint64_t x, unsigned n)  {
    return deposit_bits(1ULL << n, x, 1, 0);
}