计算机软件在纸面上通常是确定性的:如果您使用相同的输入运行相同的程序两次,您应该得到相同的输出。实际上,现代计算的复杂性使得您不可能运行两次相同的程序并获得完全相同的结果,甚至执行时间完全相同。例如,现代操作系统将内存地址随机化作为安全预防措施:一种称为地址空间布局随机化的技术。因此,如果你运行一个程序两次,你不能保证内存存储在相同的内存地址。在 Go 中,您可以使用%p
指令打印指针的地址。下面的程序将分配一个小的整数数组,并使用指向第一个值的指针打印相应的地址。如果多次运行该程序,您可能会得到不同的地址。
package main import ( "fmt" ) func main() { x := make([]int, 3) fmt.Printf("Hello %p", &x[0]) }
因此,从某种意义上说,无论我们喜欢与否,软件程序都已经是随机的。随机化可以使编程更具挑战性。例如,一个糟糕的程序可能在大多数时间都运行正常,只是间歇性地失败。这种不可预测的行为对程序员来说是一个挑战。
尽管如此,我们可以使用随机化来生成更好的软件:例如通过使用随机输入测试我们的代码。此外,随机性是安全例程的关键要素。
尽管随机性是一个直观的概念,但定义它需要更加小心。随机性通常与信息的缺乏有关。例如,它可以通过我们无法预测结果来衡量。也许你正在生成数字,每秒一个,在查看了你生成的最后几个数字之后,我仍然无法预测你将生成的下一个数字。这并不意味着您用来生成数字的方法很神奇。也许您正在应用完全可预测的数学例程。因此,随机性与观察者及其知识有关。
在软件中,我们区分伪随机性和随机性。如果我运行一个数学例程,生成看似随机的数字,但这些数字是完全确定的,我会说它们是“伪随机”。随机查找的含义是主观的,伪随机性的概念同样是主观的。
在计算机上,有可能产生程序员无法预测的数字。例如,您可以在处理器中使用温度传感器来捕获可用作随机输入的物理“噪声”。您可以使用程序启动时的时间作为随机输入。我们经常称这些值是随机的(而不是伪随机的)。我们认为它们是随机的,因为即使在原则上,软件也不可能预测它们:它们是由软件系统外部的过程产生的。 ### 散列 散列是我们设计一个函数的过程,该函数接受各种输入(例如可变长度字符串)并输出一个方便的值(通常是整数值)。因为哈希涉及一个函数,给定相同的输入,我们总是得到相同的输出。通常,散列函数产生固定数量的位:例如,32位、64位等等。
散列的一种应用与安全性有关:给定从网络恢复的文件,您可以从中计算散列值。然后您可以将其与服务器提供的哈希值进行比较。如果两个哈希值匹配,则您恢复的文件很可能与服务器上的文件匹配。诸如 git 之类的系统依赖于这种策略。
散列还可以用于构造有用的数据结构。例如,您可以创建一个哈希表:给定一组键值,您可以计算表示数组中索引的哈希值。然后,您可以将键和相应的值存储在给定索引处或附近。当提供一个键时,您可以对其进行散列,在数组中查找地址并找到匹配的值。如果散列看起来是随机的,那么您应该能够将 N 个对象散列到一个包含 M 个元素的数组中,其中 M 略大于 N,以便将一些对象散列到数组中的同一位置。很难确保没有两个对象映射到同一个数组元素:它要求 M 远大于 N。对于 M 远大于 N,碰撞的概率约为1 - exp(-N*N/(2*M))
。尽管随着 M 变大,该概率会降至零,但需要M
远大于N
才能使其实际上为零。求解1 - exp(-N*N/(2*M)) = p
p
的 p ,我们得到M = -1/2 N*N / ln(1-p)
。也就是说,为了维持概率p
,则M
必须关于N
呈二次方增长(与N*N
成比例)。因此,我们应该预期即使哈希函数看起来是随机的,哈希表中也会存在冲突。我们可以通过多种方式处理碰撞。例如,您可以使用链接:数组中的每个元素存储对可能包含多个键的动态容器的引用。您还可以使用线性探测:如果数组位置已被占用,则找到下一个未占用的值并将键存储在那里。在线性探测下搜索键时,首先访问哈希值指示的元素,如果它被不同的键占用,则移动到下一个元素,依此类推,直到访问完整个数组,或者直到找到未被占用的元素。同一主题(线性探测)有许多变体。
理想的哈希函数可能会采用每个可能的输入并将其分配给 Oracle 给出的纯随机值。不幸的是,这样的哈希函数通常是不切实际的。它们需要存储输入值和匹配随机值的大型表。在实践中,我们的目标是生成哈希函数,其行为就像纯粹随机的一样,同时仍然易于高效实现。
哈希非零整数值的一个合理示例是 murmur 函数。杂音函数由两个乘法和三个移位/异或运算组成。以下 Go 程序将使用 murmur 函数显示看起来随机的 64 位整数:
package main import ( "fmt" "math/bits" ) func murmur64(h uint64) uint64 { h ^= h >> 33 h *= 0xff51afd7ed558ccd h ^= h >> 33 h *= 0xc4ceb9fe1a85ec53 h ^= h >> 33 return h } func main() { for i := 0; i < 10; i++ { fmt.Println(i, murmur64(uint64(i))) } }
这是一个相当快的函数。 murmur64
函数的一个缺点是零映射到零,因此需要小心。
实际上,您的值可能不是整数。如果你想对字符串进行哈希处理,你可以使用递归函数。您逐个字符地处理字符串。对于每个字符,您将字符值与迄今为止计算的哈希值相结合,生成一个新的哈希值。函数完成后,您可以将 murmur 应用于结果:
package main import ( "fmt" ) func murmur64(h uint64) uint64 { h ^= h >> 33 h *= 0xff51afd7ed558ccd h ^= h >> 33 h *= 0xc4ceb9fe1a85ec53 h ^= h >> 33 return h } func hash(s string) (v uint64) { v = uint64(0) for _, c := range s { v = uint64(c) + 31*v } return murmur64(v) } func main() { fmt.Print(hash("la vie"), hash("Daniel")) }
有更好更快的哈希函数,但使用 murmur 终结器进行递归哈希的结果是合理的。
重要的是,生成两个散列为相同值的字符串(即创建冲突)相当容易。例如,您可以验证字符串"Ace"
、 "BDe"
、 "AdF"
、 "BEF"
是否都具有相同的哈希值:
fmt.Print(hash("Ace"), hash("BDe"), hash("AdF"), hash("BEF"))
当散列任意长的字符串时,总是可能发生冲突。但是,我们可以使用更复杂(且计算成本更高)的哈希函数来降低遇到问题的概率。
所提供的murmur64
函数的一个有趣特征是它是可逆的。如果考虑这些步骤,就会发现两次乘以奇数。与奇数的乘法始终是可逆的:作为 64 位无符号整数,0xff51afd7ed558ccd 的乘法逆数为 0x4f74430c22a54005,0xc4ceb9fe1a85ec53 的乘法逆数为 0x9cb4b2f8129337db。 h ^= h >> 33
是可逆的,这一点可能不太明显。但如果h
是 64 位整数,则通过检查我们可以发现h
和h ^ (h >> 33)
在最高有效 33 位中是相同的。因此,如果给定z = h ^ (h >> 33)
,则z >> (64-33) == h >> (64-33)
。也就是说,我们从h ^ (h >> 33)
中识别出h
的最高有效 33 位。扩展这个推理,我们在下面的代码中得到g
是f
的逆,即g(f(i)) == i
。
func f(h uint64) uint64 { return h ^ (h >> 33) } func g(z uint64) uint64 { h := z & 0xffffffff80000000 h = (h >> 33) ^ z return h }
我们经常需要哈希值适合从零开始的区间。例如,您可能想获取[0,max)
中的哈希值,您可以使用以下函数:
func toIntervalBias(random uint64, max uint64) uint64 { hi,_ := bits.Mul64(random, max) return hi }
此函数使用单个乘法输出[0,max)
中的值。有诸如random % max
之类的替代方案,但整数余数运算可以编译为除法指令,并且除法通常比乘法更昂贵。当性能是一个因素时,您应该尽可能避免除法指令。
重要的是, toIntervalBias
函数引入了轻微的偏差:我们从2 64 个不同的值开始,并将它们映射到N
不同的值。这意味着在2 64 个原始值中,大约有2 64 / N个值对应于每个输出值。设⌈ x ⌉为不小于x 的最小整数, ⌊ x ⌋为不大于x的较大整数。当2 64 / N不是整数时,某些输出值与 ⌈2 64 / N ⌉原始值匹配,而其他输出值与⌊2 64 / N ⌋原始值匹配。当N很小时,它可能可以忽略不计,但随着N的增大,偏差相对更重要。从某种意义上说,如果我们从均匀分布在一组2 64 个可能值上的原始值开始,那么它是最小的可能偏差。
将它们放在一起,以下程序将把一个字符串哈希为区间[0,10)
中的值。
package main import ( "fmt" "math/bits" ) func murmur64(h uint64) uint64 { h ^= h >> 33 h *= 0xff51afd7ed558ccd h ^= h >> 33 h *= 0xc4ceb9fe1a85ec53 h ^= h >> 33 return h } func hash(s string) (v uint64) { v = uint64(0) for _, c := range s { v = uint64(c) + 31*v } return murmur64(v) } func toIntervalBias(random uint64, max uint64) uint64 { hi,_ := bits.Mul64(random, max) return hi } func main() { fmt.Print(toIntervalBias(hash("la vie"),10)) }
虽然toIntervalBias
函数通常很有效,但当范围是 2 的幂时,它的成本就不必要了。如果max
是 2 的幂(例如 32),则random % max == random & (max-1)
。通常,与最大值递减的按位与甚至比乘法还要快。因此以下功能是优选的。
func toIntervalPowerOfTwo(random uint64, max uint64) uint64 { return random & (max-1) }
估计基数
散列的一种用例是估计数组或值流中值的基数。假设您的软件收到数十亿个标识符,那么有多少个不同的标识符?您可以构建所有标识符的数据库,但它可能会使用大量内存并且相对昂贵。有时,您只想要一个粗略的近似值,但希望快速计算它。
有许多技术可以使用散列来估计基数:概率计数、LOGLOG 概率计数等等。我们可以解释核心思想,甚至可以产生一个有用的函数,而无需任何高等数学。如果您散列所有值(例如,标识符),并且散列函数具有良好的质量,您会期望所有不同的值都将散列为所有值集合内的随机值。
不同哈希值之间的间距应约为2 64 /( N +1),其中N是不同值的数量。如果我们找到小的哈希值m ,那么我们应该大约有m = 2 64 /( N +1)或N = 2 64 / m − 1 。当N远大于1但远小于2 64时,这近似为N = (2 64 -1)/ m 。以下函数应用此公式来估计基数:
// estimateCardinality estimates the number of distinct values func estimateCardinality(values []uint64) int { if len(values) < 2 { return len(values) } mi1 := murmur64(values[0]) for i := 1; i < len(values); i++ { t := murmur64(values[i]) if t < mi1 { mi1 = t } } return int(math.MaxUint64 / mi1) }
我们可以在下面的程序中应用这个函数。该近似值相当粗略,但在某些实际情况下已经足够好了。
package main import ( "fmt" "math" ) func mu(h uint64, step uint64) uint64 { return h * step } func murmur64(h uint64) uint64 { h ^= h >> 33 h *= 0xff51afd7ed558ccd h ^= h >> 33 h *= 0xc4ceb9fe1a85ec53 h ^= h >> 33 return h } // fillArray fills the array with up to howmany distinct values func fillArray(arr []uint64, howmany int) { for i := 0; i < len(arr); i++ { // careful not to include zero because murmur64(0) == 0 arr[i] = 1 + uint64(i%howmany) } } // estimateCardinality estimates the number of distinct values func estimateCardinality(values []uint64) int { if len(values) < 2 { return len(values) } m := murmur64(values[0]) for i := 1; i < len(values); i++ { t := murmur64(values[i]) if t < mi1 { m = t } } return int(math.MaxUint64 / m) } func main() { values := make([]uint64, 5000000) // 50 M distinct := 2200000 // 1.2 M fillArray(values, distinct) fmt.Println(estimateCardinality(values), distinct) }
整数
生成随机整数的方法有很多,但一种特别简单的方法是依赖哈希。例如,我们可以从一个整数(例如,10)开始,返回随机整数murmur64(10)
,然后递增该整数(例如,到 11),然后返回整数murmur64(10)
。
斯蒂尔等人。 (2014)提出了一种类似的策略,他们称之为 SplitMix:它是 Java 标准库的一部分。它的工作原理与我们刚刚描述的非常相似,但不是将计数器递增 1,而是将其递增一个大的奇整数。他们还使用与murmur64
版本略有不同的版本。以下函数遵循 SplitMix 公式打印 10 个不同的随机值:
package main import "fmt" func splitmix64(seed *uint64) uint64 { *seed += 0x9E3779B97F4A7C15 z := *seed z = (z ^ (z >> 30)) z *= (0xBF58476D1CE4E5B9) z = (z ^ (z >> 27)) z *= (0x94D049BB133111EB) return z ^ (z >> 31) } func main() { seed := uint64(1234) for z := 0; z < 10; z++ { r := splitmix64(&seed) fmt.Println(r) } }
每次调用splitmix64
函数时,隐藏的seed
变量都会前移一个常量 ( 0x9E3779B97F4A7C15
)。如果从相同的种子开始,您总是会得到相同的随机值。
然后该函数对 z 执行一系列按位运算。首先,它在 z 和 z 右移 30 位之间执行 XOR 运算。然后将结果乘以常量值 0xBF58476D1CE4E5B9。接下来,它在结果与右移 27 位的结果之间执行另一次异或运算。最后,将结果乘以常量值0x94D049BB133111EB,并返回右移31位的异或结果。
它使用完整的 64 位范围生成整数。如果需要一个区间内的随机整数(例如, [0,N)
),则需要做更多的工作。如果间隔的大小是 2 的幂(例如, [0,32)
),那么我们可以简单地使用与散列相同的技术:
// randomInPowerOfTwo -> [0,max) func randomInPowerOfTwo(seed *uint64, max uint64) uint64 { r := splitmix64(seed) return r & (max-1) }
然而,当界限是任意的(不是 2 的幂)并且我们想要避免偏差时,就需要稍微复杂的算法。事实上,如果我们假设 64 位整数是真正随机的,那么所有值的可能性都是相同的。但是,如果我们不小心,在将 64 位整数转换为[0,N)
中的值时可能会引入偏差。当N
是 2 的幂时,这不是一个问题,但当N
是任意的时,它就成为一个问题。 Lemire (2019)描述了一个快速例程来解决这个问题:
func toIntervalUnbiased(seed *uint64, max uint64) uint64 { x := splitmix64(seed) hi, lo := bits.Mul64(x, max) if lo < max { t := (-max) % max // division!!! for lo < t { x := splitmix64(seed) hi, lo = bits.Mul64(x, max) } } return hi }
toIntervalUnbiased 函数采用两个参数:一个指向 64 位无符号整数种子的指针和一个 64 位无符号整数最大值。它返回一个 64 位无符号整数。该函数首先以种子指针为参数调用 splitmix64 函数,生成随机的 64 位无符号整数 x。然后,它使用 bits.Mul64 函数将 x 与 max 相乘,该函数将两个 64 位无符号整数的乘积返回为两个 64 位无符号整数。乘积的高 64 位存储在变量 hi 中,低 64 位存储在变量 lo 中。如果 lo 小于 max,则该函数进入一个循环,使用 splitmix64 生成新的随机数,并重新计算 x 和 max 的乘积,直到 lo 大于或等于 -max % max。这样做是为了确保随机数的分布是无偏的。
该函数使用的一般策略称为拒绝方法:我们反复尝试生成随机整数,直到可以产生无偏差结果。然而,当间隔远小于2 64 (常见情况)时,我们不太可能使用拒绝方法,甚至不需要计算整数余数。大多数时候,该函数永远不会进入拒绝循环。
测试随机生成器是否随机出现是具有挑战性的。我们可以使用多种测试策略,并且每种测试策略都可以或多或少地广泛。值得庆幸的是,不难想到我们可以应用一些测试。例如,我们希望值的分布是均匀的:生成任何一个值的概率除以可能值的数量应为 1。当生成 2 到 64 个可能值时,测试均匀性在技术上具有挑战性。但是,我们可以使用toInterval
等函数方便地限制输出的大小。
以下程序根据 1 亿个值计算频率直方图的相对标准偏差。相对标准偏差远小于 1% (0.05655%),这表明分布是均匀的。
package main import ( "fmt" "math" "math/bits" ) func splitmix64(seed *uint64) uint64 { *seed += 0x9E3779B97F4A7C15 z := *seed z = (z ^ (z >> 30)) z *= (0xBF58476D1CE4E5B9) z = (z ^ (z >> 27)) z *= (0x94D049BB133111EB) return z ^ (z >> 31) } func toIntervalUnbiased(seed *uint64, max uint64) uint64 { x := splitmix64(seed) hi, lo := bits.Mul64(x, max) if lo < max { t := (-max) % max // division!!! for lo < t { x := splitmix64(seed) hi, lo = bits.Mul64(x, max) } } return hi } func meanAndStdDev(arr []int) (float64, float64) { var sum, sumSq float64 for _, val := range arr { sum += float64(val) sumSq += math.Pow(float64(val), 2) } n := float64(len(arr)) mean := sum / n stdDev := math.Sqrt((sumSq / n) - math.Pow(mean, 2)) return mean, stdDev } func main() { seed := uint64(1234) const window = 30 var counter [window]int for z := 0; z < 100000000; z++ { counter[toIntervalUnbiased(&seed, window)] += 1 } moyenne, ecart := meanAndStdDev(counter[:]) fmt.Println("relative std ", ecart/moyenne*100, "%") }
随机洗牌
有时,您会得到一个要随机洗牌的数组。 Knuth 描述的优雅算法是标准方法。该算法的工作原理是从最后一个元素到第一个元素迭代数组。在每次迭代中,它会选择 0 和当前索引(含)之间的随机索引,并将当前索引处的元素与随机生成的索引处的元素交换。
以下程序根据种子随机打乱数组。更改种子会更改数组的顺序。对于大型数组,可能的排列数量可能超过可能的种子数量:这意味着使用简单的固定长度种子的算法并非所有可能的排列都是可能的。
package main import ( "fmt" "math/bits" ) func splitmix64(seed *uint64) uint64 { *seed += 0x9E3779B97F4A7C15 z := *seed z = (z ^ (z >> 30)) z *= (0xBF58476D1CE4E5B9) z = (z ^ (z >> 27)) z *= (0x94D049BB133111EB) return z ^ (z >> 31) } func toIntervalUnbiased(seed *uint64, max uint64) uint64 { x := splitmix64(seed) hi, lo := bits.Mul64(x, max) if lo < max { t := (-max) % max // division!!! for lo < t { x := splitmix64(seed) hi, lo = bits.Mul64(x, max) } } return hi } func shuffle(seed *uint64, arr []int) { for i := len(arr)-1; i >= 1; i-- { j := toIntervalUnbiased(seed, uint64(i+1)) arr[i], arr[j] = arr[j], arr[i] } } func main() { seed := uint64(1234) numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} shuffle(&seed, numbers) fmt.Println(numbers) }
花车
通常需要生成随机浮点数。软件系统通常使用 IEEE 754 浮点数。
要生成[0,1)
区间内的 32 位浮点数,我们似乎可以生成一个 32 位整数(在[0, 2 32 )中)并将其除以2 32以获得随机浮点数– [0, 1)中的点值。这当然是“大致正确”,但我们在这样做时犯了一个错误。误差有多大?
浮点(普通*数字表示为符号位、尾数和指数,如下所示:
- 有一个符号位。因为我们只关心正数,所以该位是固定的,可以忽略。
- 32 位浮点数的尾数为 23 位。它前面隐式地带有数字 1。
- 有八位专用于指数。对于普通数,指数范围为 -126 到 127。要表示零,您需要指数值 -127 和零尾数。
那么0到1之间有多少个正常的非零数呢?负指数范围为 -1 到 -126。在每种情况下,我们都有2 23 个不同的浮点数,因为尾数由 23 位组成。所以我们在[0,1)
中有 126 x 2 23 个普通浮点数。如果您手边没有计算器,那就是 1,056,964,608。如果我们要将数字 0 和 1 相加,则为126 × 2 23 + 2,略高于 10 亿个不同的值。有2 32 32 位字或略多于 40 亿个字,因此其中大约四分之一位于区间[0,1]
中。在计算机可以表示的所有浮点数中,四分之一位于[0,1]
中。通过扩展,一半的浮点数位于区间[-1,1]
中。
数字2 32不能被126 × 2 23 + 2整除,因此我们不能将一个 32 位非负整数除以2 32并希望这会生成[0,1]
或[0,1)
以一种公正的方式。
我们可以利用尾数使用 23 位这一事实。这特别意味着您选择[0, 2 24 )中的任何整数,并将其除以2 24 ,然后您可以通过将结果再次乘以2 24来恢复原始整数。这适用于2 24 ,但不适用于2 25或任何其他更大的数字。对于 64 位浮点数,您可以获得更高的精度,因为您可以用53替换24 。
因此,您可以在[0, 2 24 )中选择一个随机整数,将其除以2 24 ,您将得到[0,1)
中的一个无偏差随机数,这意味着对于[0,2^{24})
中的每个整数[0,2^{24})
, [0,1)
中只有一个数字。此外,分布是均匀的,因为可能的浮点数是均匀间隔的(它们之间的距离是平坦的2 -24 )。
因此,即使单精度浮点数使用 32 位字,并且即使您的计算机可以在[0, 1)中表示大约 230 个不同的正常浮点数,您的随机生成器也很可能只产生2 24区间[0, 1)中的不同 32 位浮点数,以及只有2 53 个不同的 64 位浮点数。
生成区间[0,N)
内的随机整数的常见方法是首先生成随机浮点数[0,1)
,然后将结果乘以N
。如果N
超过2 24 (或2 53 ),那么您将无法生成区间[0,N)
中的所有整数。类似地,要生成[a,b)
中的数字,您需要生成一个随机浮点数[0,1)
,然后将结果乘以ba
并加上a
。总体而言,结果可能并不理想。
以下程序生成随机浮点数:
package main import ( "fmt" ) func splitmix64(seed *uint64) uint64 { *seed += 0x9E3779B97F4A7C15 z := *seed z = (z ^ (z >> 30)) z *= (0xBF58476D1CE4E5B9) z = (z ^ (z >> 27)) z *= (0x94D049BB133111EB) return z ^ (z >> 31) } // toFloat32 -> [0,1) func toFloat32(seed *uint64) float32 { x := splitmix64(seed) x &= 0xffffff // %2**24 return float32(x)/float32(0xffffff) } // toFloat64 -> [0,1) func toFloat64(seed *uint64) float64 { x := splitmix64(seed) x &= 0x1fffffffffffff // %2**53 return float64(x)/float64(0x1fffffffffffff) } func main() { seed := uint64(1231114) fmt.Println(toFloat32(&seed)) fmt.Println(toFloat64(&seed)) }
浮点的一个有趣的应用是估计 pi 的值。如果我们在[0, 1), [0, 1)中生成两个浮点数x , y ,然后在 1 的面积(单位正方形)之外,则面积为x*x+y*y <= 1
应该是 pi/4。以下程序打印 pi 值的估计值。
package main import ( "fmt" ) func splitmix64(seed *uint64) uint64 { *seed += 0x9E3779B97F4A7C15 z := *seed z = (z ^ (z >> 30)) z *= (0xBF58476D1CE4E5B9) z = (z ^ (z >> 27)) z *= (0x94D049BB133111EB) return z ^ (z >> 31) } // toFloat64 -> [0,1) func toFloat64(seed *uint64) float64 { x := splitmix64(seed) x &= 0x1fffffffffffff // %2**53 return float64(x) / float64(0x1fffffffffffff) } func main() { seed := uint64(1231114) N := 100000000 circle := 0 for i := 0; i < N; i++ { x := toFloat64(&seed) y := toFloat64(&seed) if x*x+y*y <= 1 { circle += 1 } } fmt.Println(4 * float64(circle)/float64(N)) }
当然,实际算法可能需要其他分布,例如正态分布。我们可以使用 Ziggurat 方法Marsaglia & Tsang, 2000高速生成高质量的正态分布浮点值。实现起来并不困难,但有技术含量。特别是,它需要一个预先计算的表。通常,我们生成平均值为零、标准差为一的正态分布值:我们通常将结果乘以所需标准差的平方根,然后加上所需平均值。
离散分布
有时我们会得到一组可能值,每个值都有相应的概率。例如,我们可以随机选择三种颜色(红色、蓝色、绿色)中的一种,并具有相应的概率(20%、40%、40%)。如果此类值很少(例如三个),则标准方法是轮盘赌选择。我们将 0 到 1 的区间分为三个不同的分量,每种颜色一个:从 0 到 0.2,我们选择红色,从 0.2 到 0.6,我们选择蓝色,从 0.6 到 1.0,我们选择绿色。
以下程序说明了该算法:
package main import ( "fmt" "math/rand" "time" ) func splitmix64(seed *uint64) uint64 { *seed += 0x9E3779B97F4A7C15 z := *seed z = (z ^ (z >> 30)) z *= (0xBF58476D1CE4E5B9) z = (z ^ (z >> 27)) z *= (0x94D049BB133111EB) return z ^ (z >> 31) } func toFloat64(seed *uint64) float64 { x := splitmix64(seed) x &= 0x1fffffffffffff // %2**53 return float64(x) / float64(0x1fffffffffffff) } func rouletteWheelSelection(seed *uint64, colors []string, probabilities []float64) string { rand.Seed(time.Now().UnixNano()) // Create a slice of cumulative probabilities cumulativeProbabilities := make([]float64, len(probabilities)) cumulativeProbabilities[0] = probabilities[0] for i := 1; i < len(probabilities); i++ { cumulativeProbabilities[i] = cumulativeProbabilities[i-1] + probabilities[i] } // Generate a random number between 0 and 1 randomNumber := toFloat64(seed) // Select the color based on the random number and cumulative probabilities if randomNumber < cumulativeProbabilities[0] { return colors[0] } for i := 1; i < len(cumulativeProbabilities); i++ { if randomNumber >= cumulativeProbabilities[i-1] && randomNumber < cumulativeProbabilities[i] { return colors[i] } } return colors[len(colors)-1] } func main() { seed := uint64(1231114) colors := []string{"red", "blue", "green"} probabilities := []float64{0.2, 0.4, 0.4} fmt.Println(rouletteWheelSelection(&seed, colors, probabilities)) }
如果您必须从一个大集合中选择一个值,则轮盘赌选择方法可能会变得低效。在这种情况下,我们可以使用别名方法。
加密散列和随机数生成
我们通常不会重新实现加密函数。最好使用经过良好测试的实现。它们通常保留用于需要考虑安全性的情况,因为它们通常使用更多资源。
字符串的加密散列设计使得很难找到两个冲突的字符串(具有相同的散列值)。因此,如果您收到一条消息,并且提前获得了其哈希值,并且您检查哈希值和消息是否对应,则很有可能该消息没有被损坏。攻击者很难(但并非不可能)生成与您给出的哈希值相匹配的消息。要以加密方式对 Go 中的字符串进行哈希处理,您可以使用以下代码:
package main import ( "crypto/sha256" "fmt" ) func main() { message := "Hello, world!" hash := sha256.Sum256([]byte(message)) fmt.Printf("Message: %s\nHash: %x\n", message, hash) }
同样,您可能希望以加密方式生成随机数:在这种情况下,生成的随机数很难预测。即使我给你最后十个数字,也很难预测下一个数字。如果您要为在线赌场实现软件,您可能应该使用加密随机数。
package main import ( "crypto/rand" "fmt" "math/big" ) func main() { nBig, err := rand.Int(rand.Reader, big.NewInt(100)) if err != nil { panic(err) } n := nBig.Int64() fmt.Printf("Here is a random %T between 0 and 99: %d\n", n, n) }
原文: https://lemire.me/blog/2023/10/17/randomness-in-programming-with-go-code/