当然,您可以通过以 256 为底的长除法(或上推乘法)来做到这一点。就像你在小学学过的长除法一样,只不过用的是字节而不是数字。它涉及依次对每个字节进行级联的除法和余数。请注意,您还需要了解如何使用大数字,并且随着您使用它并且它变得更小,对范围内较大值的偏差会越来越大。例如,如果您只剩下 110 个,并且您要求一个 rnd(100),则 0-9 的值将比 10-99 的每个值高 10%。
但是,您实际上并不需要 bignum 技术,您可以使用算术编码压缩中的想法,在其中构建单个数字而无需实际处理整个事情。
如果您从读取 4 个字节到一个无符号 uint_32 缓冲区开始,它的范围是 0..4294967295,不包含最大值为 4294967296。我将这个综合值称为“结转”,而这个独占记录最大值也很重要。
[为简单起见,您可以从向缓冲区读取 3 个字节开始,最多生成 16M。这避免了处理无法保存在 32 位整数中的 4G 值。]
有 2 种使用方法,都具有准确性影响:
流下来:
做你的模数范围。模数是您的随机答案。除法结果是您的新结转,范围更小。
假设你想要 0..99,所以你以 100 为模,你的上半部分有一个最大范围 42949672 (4294967296/100),你可以将其用于下一个随机请求
我们还不能输入另一个字节...
假设你现在想要 0..9,所以你以 10 为模,现在你的上半部分的范围是 0..4294967 (42949672/100)
由于 max 小于 16M,我们现在可以引入下一个字节。将其乘以当前最大值 4294967 并将其添加到结转中。最大值也乘以 256 -> 1099511552
这种方法对小值有轻微的偏差,因为在“next max”次中为1,可用的值范围不会是全范围,因为最后一个值被截断,而是通过选择保持3-4最大的好字节,这种偏差被最小化。它只会在 1600 万次中最多出现 1 次。
该算法的计算成本是 div 乘以进位和最大值的随机范围,然后每次输入一个新字节时相乘。我假设编译器会优化模
串流:
说你想要 0..99
将你的最大值除以范围,得到下一个最大值,并将结转除以下一个最大值。现在,您的随机数在除法结果中,余数形成您结转以获得下一个随机数的值。
当 nextmax 小于 16M 时,只需将 nextmax 和您的结转乘以 256 并添加下一个字节。
如果这种方法的缺点是,根据用于生成 nextmax 的除法,最高值结果(即 99 或 9)严重偏向于,或者有时您会生成高值(100) - 这取决于您是否四舍五入或向下做第一个除法。
这里的计算成本又是 2 除,假设编译器优化器混合了 div 和 mod 操作。乘以 256 很快。
在这两种情况下,您都可以选择说,如果输入结转值在这个“高偏差范围”内,那么您将执行不同的技术。您甚至可以在这两种技术之间摇摆不定——优先使用第二种技术,但如果它产生高值,则使用第一种技术,尽管就其本身而言,两种技术可能会在结转时偏向相似的输入随机流值接近最大值。可以通过使第二种方法生成 -1 作为超出范围来减少这种偏差,但是这些修复中的每一个都增加了一个额外的乘法步骤。
请注意,在算术编码中,这个溢出区域在提取每个符号时被有效地丢弃。在解码过程中保证不会出现这些边缘值,这会导致轻微的次优压缩。