假设我有一个数字x,它是2的幂,对于某些i来说意味着x = 2 ^ i。 所以x的二进制表示只有一个'1'。我需要找到那个的索引。 例如,x = 16(十进制) x = 10000(二进制) 这里索引应该是4.是否可以通过使用位操作在O(1)时间内找到索引?
答案 0 :(得分:4)
以下是对de Bruijn sequences和@Juan Lopez提供的答案的 O(1)代码中使用@njuffa背后的逻辑的解释(顺便说一句,你应该考虑支持它们)。
鉴于字母 K 和长度 n , de Bruijn序列是来自 K 包含(无特定顺序)所有排列长度为 n 的排列[1],例如,给定字母 {a,b} 和 n = 3,以下列出了长度为3的 {a,b} 的所有排列(重复):
[aaa, aab, aba, abb, baa, bab, bba, bbb]
要创建关联的de Bruijn序列,我们构造一个包含所有这些排列而不重复的最小字符串,其中一个字符串为: babbbaaa
&#34; babbbaaa&#34;是我们的字母 K = {a,b} 和n = 3的de Bruijn序列,表示这个的符号是 B(2,3)< / em>,其中2是 K 的大小,也表示为 k 。序列的大小由 k n 给出,在前面的例子中 k n = 2 3 = 8
我们如何构造这样的字符串?一种方法是构建一个有向图,其中每个节点代表一个排列,并且对于字母表中的每个字母都有一个传出边,从一个节点到一个节点的转换另一个将边缘字母添加到下一个节点的右侧并删除其最左边的字母。构建图形后,抓住Hamiltonian path上的边缘来构造序列。
上一个例子的图表是:
然后,采用哈密尔顿路径(一个只访问每个顶点一次的路径):
从节点aaa
开始并跟随每个边缘,我们最终得到:
(aaa) -> b -> a -> b -> b -> b -> a -> a -> a (aaa) = babbbaaa
我们可以从节点bbb
开始,在这种情况下,获得的序列将是&#34; aaababbb&#34;。
现在已经涵盖了de Bruijn序列,让我们用它来查找整数中前导零的数量。
要找出整数值中前导零的数量,此算法的第一步是将第一个位从右向左隔离,例如,给定848(1101010000 2 ):
isolate rightmost bit
1101010000 ---------------------------> 0000010000
执行此操作的一种方法是使用x & (~x + 1)
,您可以在Hacker's Delight book上找到有关此表达式如何工作的更多信息(第2章,第2-1节)。
这个问题表明输入是2的幂,所以最右边的位从头开始被隔离,不需要付出任何努力。
一旦该位被隔离(因此将其转换为2的幂),第二步包括使用散列表方法及其散列函数来将过滤后的输入与其对应的前导0的数量进行映射,pe,将哈希函数 h(x)应用于0000010000 2 应该返回包含值4的表上的索引。
该算法建议使用perfect hash function突出显示这些属性:
为实现这一目标,我们可以使用de Bruijn序列,其中包含二进制元素 K = {0,1} 的字母表, n = 6如果我们想要解决64位整数的问题(对于64位整数,有两个值的64个可能的功率,并且需要6个位来计算它们全部)。 B(2,6) = 64,所以我们需要找到长度为64的de Bruijn序列,其中包括长度为6的二进制数字的所有排列(重复)(000000 2 ,000001 2 ,...,111111 2 )。
使用实现上述方法的程序,您可以生成满足64位整数要求的de Bruijn序列:
0000011111101101110101011110010110011010010011100010100011000010 2 = 7EDD5E59A4E28C2 16
该算法的建议散列函数是:
h(x) = (x * deBruijn) >> (k^n - n)
x
是2的幂。对于64位内的每个可能的2的幂, h(x)返回相应的二进制排列,并且我们需要将这些排列中的每一个与前导零的数量相关联以填充该表。例如,如果x
是16 = 10000 2 ,它有4个前导零,我们有:
h(16) = (16 * 0x7EDD5E59A4E28C2) >> 58
= 9141566557743385632 >> 58
= 31 (011111b)
所以,在我们表的索引31处,我们存储了4.另一个例子,让我们使用256 = 100000000 2 ,它有8个前导零:
h(256) = (256 * 0x7EDD5E59A4E28C2) >> 58
= 17137856407927308800 (due to overflow) >> 58
= 59 (111011b)
在索引59处,我们存储8.我们在每个2的幂都重复此过程,直到我们填满表格。手动生成表是不实用的,您应该使用程序like the one found here来完成这项工作。
最后,我们最终得到下表:
int table[] = {
63, 0, 58, 1, 59, 47, 53, 2,
60, 39, 48, 27, 54, 33, 42, 3,
61, 51, 37, 40, 49, 18, 28, 20,
55, 30, 34, 11, 43, 14, 22, 4,
62, 57, 46, 52, 38, 26, 32, 41,
50, 36, 17, 19, 29, 10, 13, 21,
56, 45, 25, 31, 35, 16, 9, 12,
44, 24, 15, 8, 23, 7, 6, 5
};
计算所需值的代码:
// Assumes that x is a power of two
int numLeadingZeroes(uint64_t x) {
return table[(x * 0x7EDD5E59A4E28C2ull) >> 58];
}
有什么保证我们没有错过因碰撞导致的2的幂指数?
哈希函数基本上获得de Bruijn序列中包含的每6位置换,每次幂为2,乘以x
基本上只是向左移位(乘以2的幂是与left shifting the number)相同,然后应用右移58,逐个隔离6位组,对于x
的两个不同值(所需散列函数的第三个属性)不会出现冲突对于这个问题)感谢de Bruijn序列。
[1] De Bruijn Sequences - http://datagenetics.com/blog/october22013/index.html
[2] 使用de Bruijn序列在计算机词中索引1 - http://supertech.csail.mit.edu/papers/debruijn.pdf
[3] The Magic Bitscan - http://elemarjr.net/2011/01/09/the-magic-bitscan/
答案 1 :(得分:1)
这取决于您的定义。首先让我们假设有n
位,因为如果我们假设有一个恒定的位数,那么我们可能用它们做的所有事情将花费不变的时间,所以我们无法比较任何东西。
首先,让我们尽可能广泛地了解&#34;按位运算&#34; - 它们在位上运行但不一定是逐点运算,而且我们计算运算但不包括运算本身的复杂性。
微米。 L. Fredman和DE Willard表明,有一个O(1)运算算法来计算lambda(x)
(x
的基数2对数的底数,所以最高设置位的索引) 。它包含了相当多的乘法,所以按位调用它有点滑稽。
另一方面,有一个明显的O(log n)运算算法,不使用乘法,只是二进制搜索。但是可以做得更好,Gerth Brodal表明它可以在O(log log n)操作中完成(并且它们都不是乘法)。
我引用的所有算法都是“计算机编程艺术4A”,按位技巧和技巧。
这些都不是真正有资格在恒定时间内找到1,而且很明显你不能这样做。尽管他们声称,其他答案也没有资格。它们很酷,但它们是针对特定的常数位设计的,因此任何朴素算法也都是O(1)(通常,因为没有n
依赖)。在评论中OP说了一些暗示他实际上想要的东西,但它在技术上没有回答这个问题。
答案 2 :(得分:1)
问题的规格对我来说并不完全清楚。例如,哪些操作计为“位操作”以及有多少位构成有问题的输入?许多处理器都具有通过内部暴露的“计数前导零”或“查找第一位”指令,基本上可以直接提供所需的结果。
下面我将展示如何根据De Bruijn sequence找到32位整数的位位置。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
/* find position of 1-bit in a = 2^n, 0 <= n <= 31 */
int bit_pos (uint32_t a)
{
static int tab[32] = { 0, 1, 2, 6, 3, 11, 7, 16,
4, 14, 12, 21, 8, 23, 17, 26,
31, 5, 10, 15, 13, 20, 22, 25,
30, 9, 19, 24, 29, 18, 28, 27};
// return tab [0x04653adf * a >> 27];
return tab [(a + (a << 1) + (a << 2) + (a << 3) + (a << 4) + (a << 6) +
(a << 7) + (a << 9) + (a << 11) + (a << 12) + (a << 13) +
(a << 16) + (a << 18) + (a << 21) + (a << 22) + (a << 26))
>> 27];
}
int main (void)
{
uint32_t nbr;
int pos = 0;
while (pos < 32) {
nbr = 1U << pos;
if (bit_pos (nbr) != pos) {
printf ("!!!! error: nbr=%08x bit_pos=%d pos=%d\n",
nbr, bit_pos(nbr), pos);
EXIT_FAILURE;
}
pos++;
}
return EXIT_SUCCESS;
}
答案 3 :(得分:1)
如果允许单个内存访问,则可以在O(1)中执行此操作:
#include <iostream>
using namespace std;
int indexes[] = {
63, 0, 58, 1, 59, 47, 53, 2,
60, 39, 48, 27, 54, 33, 42, 3,
61, 51, 37, 40, 49, 18, 28, 20,
55, 30, 34, 11, 43, 14, 22, 4,
62, 57, 46, 52, 38, 26, 32, 41,
50, 36, 17, 19, 29, 10, 13, 21,
56, 45, 25, 31, 35, 16, 9, 12,
44, 24, 15, 8, 23, 7, 6, 5
};
int main() {
unsigned long long n;
while(cin >> n) {
cout << indexes[((n & (~n + 1)) * 0x07EDD5E59A4E28C2ull) >> 58] << endl;
}
}
答案 4 :(得分:0)
答案是...... ...... ......是的!
只是为了好玩,因为您在下面评论了i
最多20个答案就足够了。
(这里的乘法是零或一)
#include <iostream>
using namespace std;
int f(int n){
return
0 | !(n ^ 1048576) * 20
| !(n ^ 524288) * 19
| !(n ^ 262144) * 18
| !(n ^ 131072) * 17
| !(n ^ 65536) * 16
| !(n ^ 32768) * 15
| !(n ^ 16384) * 14
| !(n ^ 8192) * 13
| !(n ^ 4096) * 12
| !(n ^ 2048) * 11
| !(n ^ 1024) * 10
| !(n ^ 512) * 9
| !(n ^ 256) * 8
| !(n ^ 128) * 7
| !(n ^ 64) * 6
| !(n ^ 32) * 5
| !(n ^ 16) * 4
| !(n ^ 8) * 3
| !(n ^ 4) * 2
| !(n ^ 2);
}
int main() {
for (int i=1; i<1048577; i <<= 1){
cout << f(i) << " "; // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
}
}