一个数组。对真的!您将没有空间开销,没有“指针追逐”开销,计算索引只需要一点点数学,处理器真的很擅长。
假设你得到一个作为mask 和bits 的部分密钥,其中mask 的位为0,表示通配符,其他位置为1,bits 为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),因此必须将它们屏蔽掉(& ~mask),然后设置回正确的值(| bits)。运算符的优先级使得它可以在很大程度上不用括号来编写。你也可以写成
key = (((key | mask) + 1) & (~mask)) | bits;
这适用于任何类型的模式。如果您只需要“最后 x 位是可变的”,您可以优化一下:
int wildcards = 0;
int invmask = ~mask;
do {
yield items[wildcards++ | bits];
} while (wildcards & invmask);
这只是从 0 到 2number-of-wildcards,然后将固定位放入顶部。
非二进制键
在最简单的非二进制情况下,密钥的部分仍然是整数位,即它们的范围从 0 到 2n-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 只从 2 位的组中减去,而这组 2 位一起至少为 1,所以随身携带永远不会离开关键部分。
当然,这种索引方式确实会浪费一些空间,这与二次幂的情况不同,因为数组中存在永远无法索引的“洞”。