数组。对真的!您将没有空间开销,没有“指针追逐”开销,计算索引只需要一点点数学,处理器真的很擅长。
假设你得到一个作为 a 的部分密钥mask
,bits
其中mask
0 代表通配符位,其他位置 1 位,通配符bits
为 0,非通配符位为 0。
收集具有与该模式匹配的键的所有项目的算法是:
int key = bits;
do {
yield items[key];
key = (key | mask) + 1 & ~mask | bits;
} while (key != bits);
那key = (key | mask) + 1 & ~mask | bits
部分看起来很有趣,这是它的工作原理。
(|
按位或)使所有非通配符为 1。这确保增量继续通过非通配符的位。添加之后,应该“固定”的位被破坏(如果进位通过它们,则为 0,否则为 1),因此必须将它们屏蔽掉(the & ~mask
),然后将其设置回正确的值(| bits
)。运算符的优先级使得它可以在很大程度上不用括号来编写。你也可以写成
key = (((key | mask) + 1) & (~mask)) | bits;
这适用于任何类型的模式。如果您只需要“最后 x 位是可变的”,您可以优化一下:
int wildcards = 0;
int invmask = ~mask;
do {
yield items[wildcards++ | bits];
} while (wildcards & invmask);
这只是从 0 到 2个通配符数量,然后放入顶部的固定位。
非二进制密钥
在最简单的非二进制情况下,密钥的部分仍然是整数位,即它们的范围从 0 到 2 n -1。在这种情况下,您可以使用完全相同的代码,但对掩码的解释是不同的:通配符没有单个 0 位,非通配符只有一个 1 位,而是有一些其他位数(对应于关键部分的宽度(以位为单位)。
对于非二的幂,它需要一些更多的技巧。问题是必须尽快生成进位,以满足关键部分小于某个值的约束。
例如,如果所有关键部分都可以是 0、1 或 2(但不是 3),您可以这样做(未测试):
int key = bits;
int increment = (0x55555555 & ~mask) + 1;
do {
yield items[key];
int temp = (key | mask) + increment & ~mask;
int fix = (temp | (temp >> 1)) & 0x55555555;
key = temp - fix | bits;
} while (key != bits);
额外increment
是 1 加上“最近的 2 次幂与键部分的最大值之差”的掩码,在这种情况下,每个键部分为 1,因此每个“槽”中都有一个 1(插槽是 2 位宽,这是在这种情况下它们可以是最窄的)。它仅在通配符的位置具有那些“偏移量”。
偏移关键部分,以便它们的最高允许值映射到“全1”,确保进位通过它们传播。但是,这意味着它们通常处于无效状态(除非它接收到进位并变为零)。那么烦人的部分就来了:偏移量必须仅针对未回绕为零的关键部分撤消。
所以fix
进来了。它计算一个不为零的关键部分的掩码。如果关键部分更宽,那就更烦人了,如果它们的关键部分大小不一样,那就太糟糕了。
然后最后一部分,key = temp - fix | bits
撤消偏移并将非通配符放回。那个减法永远不会破坏任何东西,因为只有 1 仅从至少为 1 的 2 位组中减去,因此进位永远不会留下键-部分。
当然,这种索引方式确实会浪费一些空间,这与二次幂的情况不同,因为数组中存在永远无法索引的“洞”。