编写最有效的代码来获取“1”位的索引。
最有效的代码是以某种方式将ch 的值映射到它的位索引,即:
0x01 -> 0
0x02 -> 1
0x04 -> 2
0x08 -> 3
...
朴素映射表
最简单和天真的解决方案需要在映射表中查找所有可能的值ch。对于 8 位数字 (char),我们需要一个包含 28= 256 个元素的表格:
char naive_table[256];
naive_table[0x01] = 0;
naive_table[0x02] = 1;
naive_table[0x04] = 2;
naive_table[0x08] = 3;
naive_table[0x10] = 4;
naive_table[0x20] = 5;
naive_table[0x40] = 6;
naive_table[0x80] = 7;
在这张表中查找也很简单:
index = naive_table[ch];
哈希函数+映射表
之前的解决方案简单快速,但是naive_table的大部分元素都被浪费了。考虑到 ch 是 2 的幂,对于任何 n 位数,只有 n 可能的索引。
因此,我们可以使用一个只有 8 个元素的表和一个哈希函数来将 ch 的值映射到一个唯一索引映射表。
这种散列函数的完美候选者是使用 de Bruijn 序列的函数。有一篇论文"Using de Bruijn Sequences to Index a 1 in a Computer Word" 指出:
length-n de Bruijn 序列,其中n 是 2 的精确幂,是一个由 n 个 0 和 1 组成的循环序列,因此每个长度为 lg n 的 0-1 序列作为一个连续子串恰好出现一次.
例如,一个长度为 8 的 de Bruijn 序列是 00011101。每个 3 位数字作为一个连续的子串恰好出现一次:从最左边的 3 位开始,一次将 3 位窗口向右移动一位,我们有 000, 001, 011, 111, 110, 101, 010(环绕),100(也环绕)。
哈希函数的计算公式为:h(x)=(x * deBruijn)>>(n - lg n)
那么,让我们试试这个散列函数来在我们的紧凑查找表中获得一个唯一索引:
h(ch) = ((ch * 00011101b) >> (8 - 3)) & 0x7
h(ch) = ((ch * 29) >> 5) & 0x7
让我们计算ch 的所有值的哈希值,并确保哈希函数按预期工作,即所有哈希值都是唯一的:
ch h(ch)
0x01 ((1 * 29) >> 5) & 0x7 = 0
0x02 ((2 * 29) >> 5) & 0x7 = 1
0x04 ((4 * 29) >> 5) & 0x7 = 3
0x08 ((8 * 29) >> 5) & 0x7 = 7
0x10 ((16 * 29) >> 5) & 0x7 = 6
0x20 ((32 * 29) >> 5) & 0x7 = 5
0x40 ((64 * 29) >> 5) & 0x7 = 2
0x80 ((64 * 29) >> 5) & 0x7 = 4
因此散列函数工作正常,并为ch 的两个值的每个幂生成唯一的散列。
现在让我们使用上表中的哈希值创建一个紧凑的映射表:
char compact_table[8];
compact_table[0] = 0;
compact_table[1] = 1;
compact_table[3] = 2;
compact_table[7] = 3;
compact_table[6] = 4;
compact_table[5] = 5;
compact_table[2] = 6;
compact_table[4] = 7;
现在我们使用哈希值作为索引进行查找:
h = ((ch * 29) >> 5) & 0x7;
index = compact_table[h];
哈希函数+位串
之前的版本近乎完美:映射表中不再有浪费的元素。但由于所有索引都在 0-7 之间(即只有 3 位值),因此仍有改进的空间。让我们使用位串代替映射表,这样就不会浪费每个元素的最高位。
首先,让我们使用ch 的所有值和之前版本的哈希值创建这样一个位串:
ch h(sh) index
0x01 0 0 (000b)
0x02 1 1 (001b)
0x04 3 2 (010b)
0x08 7 3 (011b)
0x10 6 4 (100b)
0x20 5 5 (101b)
0x40 2 6 (110b)
0x80 4 7 (111b)
现在让我们按哈希值排序这个表:
ch h(sh) index
0x01 0 0 (000b)
0x02 1 1 (001b)
0x40 2 6 (110b)
0x04 3 2 (010b)
0x80 4 7 (111b)
0x20 5 5 (101b)
0x10 6 4 (100b)
0x08 7 3 (011b)
所以位串将是这些 3 位索引的反向串联:
011 100 101 111 010 110 001 000 = 0x72f588
现在让我们像以前一样在这个位串中查找。请注意,我们的索引是 3 位的,因此我们需要将哈希值乘以 3:
h = ((ch * 29) >> 5) & 0x7; // just like before
bit_string = 0x72f588;
index = (bit_string >> (h * 3)) & 0x7;
简而言之:
index = (0x72f588 >> ((((ch * 29) >> 5) & 0x7) * 3)) & 0x7;
代码中没有除法/模数/条件,因此它应该在任何 CPU 上都能快速执行。
概念验证代码:
unsigned char ch;
for (ch = 1; ch; ch <<= 1) {
int index = (0x72f588 >> ((((ch * 29) >> 5) & 7) * 3)) & 7;
printf("ch = 0x%02x index = %d\n", ch, index);
}
return 0;