获取尾随 1 位的数量
Posted
技术标签:
【中文标题】获取尾随 1 位的数量【英文标题】:Getting the number of trailing 1 bits 【发布时间】:2010-03-04 16:05:28 【问题描述】:我可以做任何有效的按位运算来获得整数结尾的设置位数吗?例如 1110 = 10112 将是两个尾随 1 位。 810 = 10002 将是 0 尾随 1 位。
有没有比线性搜索更好的算法?我正在实现一个随机跳过列表并使用随机数来确定插入元素时的最大级别。我正在处理 C++ 中的 32 位整数。
编辑: 汇编程序是不可能的,我对纯 C++ 解决方案感兴趣。
【问题讨论】:
【参考方案1】:计算 ~i & (i + 1)
并将结果用作在包含 32 个条目的表中的查找。 1
表示 0 个 1,2
表示一个 1,4
表示两个 1,依此类推,除了 0
表示 32 个 1。
【讨论】:
这是另一种解决方案,我无法一目了然地确定其正确性。您能否链接到或包含工作代码? 这对我来说很有意义。基本上,通过对i
进行补码,我们得到在末尾的 1 位序列之后的 0 位设置为 1。通过将 1 加到i
,我们将相同的位设置为 1。通过两者相加,我们得到 2 的幂,因为其他的都是 0,对吗?但是,即使我保留lookup[i] = ~i & (i + 1)
,这如何帮助我获得我感兴趣的实际计数?号码n
?通过使用查找表,我只能获得 2 的幂,我仍然需要一种方法来获得它的对数,不是吗?所以我不确定这是否比其他解决方案更好。
i+1 切换所有尾随的和最右边的零。 ~i 切换所有位。将它们加在一起,您将得到一个值,其中原始最右边的零为 1,其余全部为 0。查找表是针对那个单一的位置的。
我想我也会实现这个... :)
请参阅下面的答案以摆脱表格。【参考方案2】:
从 Ignacio Vazquez-Abrams 那里得到答案,并用计数而不是表格来完成它:
b = ~i & (i+1); // 这会在尾随 1 的左侧给出 1 b--; // 这让我们得到了需要计数的尾随 1 b = (b & 0x55555555) + ((b>>1) & 0x55555555); // 1 位数字的 2 位和 b = (b & 0x33333333) + ((b>>2) & 0x33333333); // 2 位数字的 4 位和 b = (b & 0x0f0f0f0f) + ((b>>4) & 0x0f0f0f0f); // 4位数字的8位和 b = (b & 0x00ff00ff) + ((b>>8) & 0x00ff00ff); // 8 位数字的 16 位和 b = (b & 0x0000ffff) + ((b>>16) & 0x0000ffff); // 16 位数字的总和最后 b 将包含 1 的计数(掩码,添加和移动计数 1)。 当然,除非我搞砸了。使用前进行测试。
【讨论】:
非常好,根本没有查找表。谢谢! 但是,这是否比其他解决方案更好?除了使用更少的内存之外,我认为 256 个整数查找表解决方案执行的操作更少。很难测试,因为无论如何它们都非常快,但是您认为哪一个会在非常慢的计算机上胜出? 它应该比任何带有循环或条件的解决方案都快,但您确实需要测试才能确定。 可以展开查找解决方案的循环,但我认为你被条件卡住了。 更快是非常依赖于平台的。随时给我一个有条件执行的ARM。对于外行来说,几乎每一个手臂操作都可以是有条件的。而不是有 16 个分支命令(==0、=0、、>= 等...),您有 4 位指令字专用于为所有内容提供相同的条件。所以只有一个分支操作,你可以用一个修饰符把它变成一个Branch If Less Than Or Equal,但是同样的修饰符可以把Add操作符变成Add If Less Than Or Equal,没有任何处理开销。【参考方案3】:Bit Twiddling Hacks 页面有许多用于计算尾随零的算法。它们中的任何一个都可以通过简单地先反转你的数字来进行调整,并且可能有一些聪明的方法可以在不这样做的情况下改变现有的算法。在具有廉价浮点运算的现代 CPU 上,最好的可能是:
unsigned int v=~input; // find the number of trailing ones in input
int r; // the result goes here
float f = (float)(v & -v); // cast the least significant bit in v to a float
r = (*(uint32_t *)&f >> 23) - 0x7f;
if(r==-127) r=32;
【讨论】:
【参考方案4】:GCC 有 __builtin_ctz
并且其他编译器有自己的内在函数。只需使用#ifdef
保护它:
#ifdef __GNUC__
int trailingones( uint32_t in )
return ~ in == 0? 32 : __builtin_ctz( ~ in );
#else
// portable implementation
#endif
在 x86 上,这个内置指令将编译成一条非常快的指令。其他平台可能会慢一些,但大多数平台都有某种位计数功能,可以胜过纯 C 运算符。
【讨论】:
【参考方案5】:可能会有更好的答案,特别是如果汇编程序不是不可能的,但一种可行的解决方案是使用查找表。它将有 256 个条目,每个条目返回连续尾随 1 位的数量。将其应用于最低字节。如果是8个,申请下一个并保持计数。
【讨论】:
这不是最坏的情况吗?如果我理解正确,我会首先使用查找表检查当前字节是否有八个尾随位,然后是七个,然后是六个等等。还是你的意思是别的? 别的东西!你会查找最后一个字节,如果答案低于 8,你就完成了。如果不是,请重复下一个字节并保留总数。您最多可以对 32 位数字进行 4 次查找,每次查找后进行简单测试以确定是否继续。 这里最坏的情况是 4 次查找而不是 32 次按位运算。 要清楚,我建议的仍然是线性搜索,但它是逐字节的,而不是逐位的。它仍然是 O(n),但 k 更小。 嗯,从技术上讲,您不需要保留总数。您的索引会告诉您已扫描了多少字节。将它们乘以 8 并添加停止字节的查找。【参考方案6】:实现 Steven Sudit 的想法...
uint32_t n; // input value
uint8_t o; // number of trailing one bits in n
uint8_t trailing_ones[256] =
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 6,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 7,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 6,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4,
0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 8;
uint8_t t;
do
t=trailing_ones[n&255];
o+=t;
while(t==8 && (n>>=8))
1(最佳)到 4(最差)(平均 1.004)次(1 次查找 + 1 次比较 + 3 次算术运算)减去 1 次算术运算。
【讨论】:
这是一个非常快的实现,并且可以在任何大小的 int 的模板中工作。我想到的可能会更慢,因为它基于一个字节*和一个计数,这增加了灵活性是有代价的。如果输入类型是固定的,则可以展开循环以获得更快的速度。 另外,在所有情况下,最后的右移都是不必要的。因此,将 while 移动到循环中(通过变为 if/break)会稍微加快它的速度。不过,展开将是一个更大的胜利。我想知道展开是否可以通过模板而不是手动完成... 移动了班次,短路应该处理它,现在我在答案是 8 的倍数时保存循环迭代。【参考方案7】:此代码计算尾随零位的数量,取自here(还有一个版本取决于 IEEE 32 位浮点表示,但我不相信它,模数/除法方法看起来真的光滑 - 也值得一试):
int CountTrailingZeroBits(unsigned int v) // 32 bit
unsigned int c = 32; // c will be the number of zero bits on the right
static const unsigned int B[] = 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF, 0x0000FFFF;
static const unsigned int S[] = 1, 2, 4, 8, 16; // Our Magic Binary Numbers
for (int i = 4; i >= 0; --i) // unroll for more speed
if (v & B[i])
v <<= S[i];
c -= S[i];
if (v)
c--;
return c;
然后计算尾随的:
int CountTrailingOneBits(unsigned int v)
return CountTrailingZeroBits(~v);
【讨论】:
【参考方案8】:http://graphics.stanford.edu/~seander/bithacks.html 可能会给你一些启发。
【讨论】:
【参考方案9】:基于 Ignacio Vazquez-Abrams 的回答实现
uint8_t trailing_ones(uint32_t i)
return log2(~i & (i + 1));
log2() 的实现留给读者作为练习(参见here)
【讨论】:
【参考方案10】:采用@phkahler 的回答,您可以定义以下预处理器语句:
#define trailing_ones(x) __builtin_ctz(~x & (x + 1))
当您在所有前面的后面都剩下一个时,您可以简单地计算尾随的零。
【讨论】:
【参考方案11】:Hacker's Delight 中给出了查找尾随 0 数量的极快方法。
您可以对整数(或更一般地,单词)进行补码,以找到尾随 1 的数量。
【讨论】:
你需要补充而不是否定这个值。【参考方案12】:我有这个样品给你:
#include【讨论】:
我相信你被否决的原因是你的解决方案本质上是 OP 已经拥有并试图改进的按位线性搜索。 概念验证会显示谁的性能更好。完美不是复杂性。 我猜测一下,你的实现比 Sparr 的慢。 算法被暴露为不工作,至少我不能让它工作。 不,OP 没有说他们的算法不起作用。他们只是想要比按位搜索更好的东西。但是您提供的只是按位搜索的示例。以上是关于获取尾随 1 位的数量的主要内容,如果未能解决你的问题,请参考以下文章
csharp 编写一个程序,计算给定数字的阶乘中的尾随零的数量。