BigInteger
在找到指定范围内的有效值之前,简单的实现平均会失败 64 次。
在最坏的情况下,我的实现平均只重试 0.5 次(读作:50% 的时间它会在第一次尝试时找到结果)。
此外,与模块化算术不同,我的实现保持了一个统一的分布。
解释
我们必须在和BigInteger
之间生成一个随机数。min
max
- 如果,
min > max
我们交换min
max
- 为了简化实现,我们将范围从 转移
[min, max]
到[0, max-min]
,这样我们就不必处理符号位
- 我们计算有多少字节
max
包含 ( bytes.Length
)
- 从最高位开始,我们计算有多少位是 0 (
zeroBits
)
- 我们生成一个随机的
bytes.Length
字节序列
- 我们知道,对于我们的序列
< max
,至少zeroBits
从最高有效位开始的位必须为 0,因此我们使用 a对最高有效字节zeroBitMask
进行单个位对位&
操作来设置它们,这将节省大量时间通过减少生成超出我们范围的数字的变化
- 我们检查我们生成的数字是否是
> max
,如果是,我们再试一次
- 我们通过添加我们的结果
[0, max-min]
将范围从[min, max]
min
我们有我们的号码。
执行
public static BigInteger RandomInRange(RandomNumberGenerator rng, BigInteger min, BigInteger max)
{
if (min > max)
{
var buff = min;
min = max;
max = buff;
}
// offset to set min = 0
BigInteger offset = -min;
min = 0;
max += offset;
var value = randomInRangeFromZeroToPositive(rng, max) - offset;
return value;
}
private static BigInteger randomInRangeFromZeroToPositive(RandomNumberGenerator rng, BigInteger max)
{
BigInteger value;
var bytes = max.ToByteArray();
// count how many bits of the most significant byte are 0
// NOTE: sign bit is always 0 because `max` must always be positive
byte zeroBitsMask = 0b00000000;
var mostSignificantByte = bytes[bytes.Length - 1];
// we try to set to 0 as many bits as there are in the most significant byte, starting from the left (most significant bits first)
// NOTE: `i` starts from 7 because the sign bit is always 0
for (var i = 7; i >= 0; i--)
{
// we keep iterating until we find the most significant non-0 bit
if ((mostSignificantByte & (0b1 << i)) != 0)
{
var zeroBits = 7 - i;
zeroBitsMask = (byte)(0b11111111 >> zeroBits);
break;
}
}
do
{
rng.GetBytes(bytes);
// set most significant bits to 0 (because `value > max` if any of these bits is 1)
bytes[bytes.Length - 1] &= zeroBitsMask;
value = new BigInteger(bytes);
// `value > max` 50% of the times, in which case the fastest way to keep the distribution uniform is to try again
} while (value > max);
return value;
}
测试
using (var rng = RandomNumberGenerator.Create())
{
BigInteger min = 0;
BigInteger max = 5;
var attempts = 10000000;
var count = new int[(int)max + 1];
var sw = Stopwatch.StartNew();
for (var i = 0; i < attempts; i++)
{
var v = BigIntegerUtils.RandomInRange(rng, min, max);
count[(int)v]++;
}
var time = sw.Elapsed;
Console.WriteLine("Generated {0} big integers from {1} to {2} in {3}", attempts, min, max, time);
Console.WriteLine("On average: {0} ms/integer or {1} integers/second", time.TotalMilliseconds / attempts, attempts / time.TotalSeconds);
for (var i = 0; i <= max; i++)
Console.WriteLine("{0} generated {1}% of the times ({2} times)", i, count[i] * 100d / attempts, count[i]);
}
在我的 i7-6500U 上测试输出:
Generated 10000000 big integers from 0 to 5 in 00:00:09.5413677
On average: 0.00095413677 ms/integer or 1048067.77334449 integers/second
0 generated 16.66633% of the times (1666633 times)
1 generated 16.6717% of the times (1667170 times)
2 generated 16.66373% of the times (1666373 times)
3 generated 16.6666% of the times (1666660 times)
4 generated 16.68271% of the times (1668271 times)
5 generated 16.64893% of the times (1664893 times)
我的 i7-6500U 上的另一个测试输出
Generated 10000000 big integers from 0 to 10^100 in 00:00:17.5036570
On average: 0.0017503657 ms/integer or 571309.184132207 integers/second