随机播放所需的分布
0..(n-1)
在域中应用一些双射来稍微洗牌。如果是素数,这将特别容易n
,因为在这种情况下,您可以将模算术视为一个字段,并执行各种漂亮的数学函数。可能会根据您的需要均匀地分配数字的一件事可能是乘以一个固定的数字c
,然后是模数:
a ↦ (c*a) mod n
你必须选择c
它与n
, 即互质gcd(c,n)=1
。如果n
是素数,那么只要 是微不足道的c≠0
,如果n
是 2 的幂,那么任何奇数仍然足够。这种共质性条件确保了另一个数的存在,d
它是 的倒数,c
即它满足c*d ≡ 1 (mod n)
乘以d
将取消乘以 的效果c
。例如,您可以BigInteger.modInverse
在 Java 或Wolfram Alpha中使用来计算这个数字。
如果您n
是 2 的幂,那么您可以避免模运算(以及所需的时间),而是执行简单的位掩码操作。但即使对于 的其他值n
,您有时也可以提出避免通用除法运算的方案。当您选择c
(并d
使用它)时,您可以这样做,c
并且d
只有很少的非零位。那么乘法很可能用位移和加法来表示。只要您确保这些数字是编译时常量,您的优化编译器就应该为您解决这个问题。
这是一个使此优化明确的示例。请注意,不必以这种方式编写代码:通常编写诸如(25*a)&1023
.
// n = 1024
// c = 25 = 16+8+1
// d = 41 = 32+8+1
static unsigned shuffle(unsigned a) {
return (a + (a << 3) + (a << 4)) & 1023;
}
static unsigned unshuffle(unsigned a) {
return (a + (a << 3) + (a << 5)) & 1023;
}
另一种适用于二的幂的情况的改组方法n
是使用位移、掩码和异或的某些组合来修改值。这可以与上述乘法方法相结合,在乘法之前或之后进行位旋转,甚至两者兼而有之。做出选择很大程度上取决于价值的实际分布。
拆分和存储
结果值仍然在范围内0..(n-1)
,可以分成两个值:一个在范围内0..(k-1)
并将被调用lo
,另一个在0..(ceil(n/k)-1)
我将调用的范围内hi
。
lo = a mod k
hi = floor(a/k)
如果k
是 2 的幂,则可以lo
使用位掩码和hi
位移位来获得。然后,您可以使用hi
来表示哈希存储桶,并lo
表示要存储在该存储桶中的值。所有具有相同hi
值的值都会发生冲突,但它们的lo
部分将有助于检索实际存储的值。
如果您想识别散列映射中未占用的插槽,则应确保lo
在每个插槽中为此目的保留一个特定值(例如零)。如果您无法在原始值集中实现此保留,那么您可能希望选择k
2 减一的幂,以便您可以存储k
自身的值以表示空单元格。或者您可以交换 and 的含义hi
,lo
这样您就可以调整 的值n
以省略一些值。我将在下面的示例中使用它。
倒置
要反转这整个过程,您需要获取 keyhi
和存储的 value lo
,将它们组合成a=k*hi+lo
range 中的一个值0..(n-1)
,然后撤消初始洗牌以返回原始值。
例子
这个例子旨在避免所有的乘法和除法。它在插槽上分配n=4032
值,每个插槽可能k=64
具有n/k=63
不同的值加上一个特殊的空值。它使用c=577
and进行洗牌d=1153
。
unsigned char bitseq[50] = { 0 };
int store(unsigned a) {
unsigned b, lo, hi, bitpos, byteno, cur;
assert(a < 4032); // a has range 0 .. 0xfbf
// shuffle
b = (a << 9) + (a << 6) + a + 64; // range 0x40 ..0x237dbf
b = (b & 0xfff) + ((b & 0xfff000) >> 6); // range 0x40 .. 0x9d7f
b = (b & 0xfff) + ((b & 0xfff000) >> 6); // range 0x40 .. 0x11ff
b = (b & 0xfff) + ((b & 0xfff000) >> 6); // range 0x40 .. 0xfff
b -= 64; // range 0x00 .. 0xfbf
// split
lo = b & 63; // range 0x00 .. 0x3f
hi = b >> 6; // range 0x00 .. 0x3e
// access bit sequence
bitpos = (lo << 2) + (lo << 1); // range 0x00 .. 0x17a
byteno = (bitpos >> 3); // range 0x00 .. 0x30
bitpos &= 7; // range 0x00 .. 0x7
cur = (((bitseq[byteno + 1] << 8) | bitseq[byteno]) >> bitpos) & 0xff;
if (cur != 0) return 1; // slot already occupied.
cur = hi + 1; // range 0x01 .. 0x3f means occupied
bitseq[byteno] |= (cur << bitpos) & 0xff;
bitseq[byteno + 1] |= ((cur << bitpos) & 0xff00) >> 8;
return 0; // slot was free, value stored
}
void list_all() {
unsigned b, lo, hi, bitpos, byteno, cur;
for (lo = 0; lo != 64; ++lo) {
// access bit sequence
bitpos = (lo << 2) + (lo << 1);
byteno = (bitpos >> 3);
bitpos &= 7;
cur = (((bitseq[byteno + 1] << 8) | bitseq[byteno]) >> bitpos) & 0x3f;
if (cur == 0) continue;
// recombine
hi = cur - 1;
b = (hi << 6) | lo;
// unshuffle
b = (b << 10) + (b << 7) + b + 64;
b = (b & 0xfff) + ((b & 0xfff000) >> 6);
b = (b & 0xfff) + ((b & 0xfff000) >> 6);
b = (b & 0xfff) + ((b & 0xfff000) >> 6);
b -= 64;
// report
printf("%4d was stored in slot %2d using value %2d.\n", b, lo, cur);
}
}
如您所见,可以避免所有乘法和除法运算,以及所有显式模调用。生成的代码是否比每次调用使用单个模调用的代码具有更高的性能仍有待测试。您需要最多三个归约步骤来避免单个模数,这一事实使得这相当昂贵。
您可以观看上述代码的演示运行。