
在软件开发中,我们经常需要生成随机数。一个常见的需求是生成一系列唯一的随机数。然而,许多开发者在实践中会遇到一个令人困惑的现象:即使随机数的生成范围非常大,但当生成的数量达到一定规模时,重复的数字却频繁出现,远超直观预期。
考虑以下一个Scala函数,其目标是生成1000个范围在0到9,999,999(即1千万)之间的唯一随机数:
import scala.util.Random
object RandomNumberGenerator {
private def generateOneThousandRandomNumbers(listOfNumbers: List[String] = List.empty): List[String] = {
if (listOfNumbers.size == 1000) {
listOfNumbers
} else {
val nextNumber: String = Random.nextInt(10000000).toString // 生成0到9,999,999之间的随机数
if (listOfNumbers.contains(nextNumber)) {
println("DUPLICATE NUMBER GENERATED: " + nextNumber) // 如果重复则打印
}
// 递归调用,将新生成的数字添加到列表中,即使它是重复的
generateOneThousandRandomNumbers(listOfNumbers ++ List(nextNumber))
}
}
def main(args: Array[String]): Unit = {
// 假设有测试用例检查生成列表的唯一性
// val x = generateOneThousandRandomNumbers()
// x.size shouldBe x.distinct.size // 预期会失败
}
}这段代码尝试生成1000个随机数,并检查是否已存在于列表中。开发者可能预期,在1千万的巨大范围内,生成1000个数字的重复概率会非常低,例如每1000次运行才出现一次重复。然而,实际测试结果却显示,大约50%的运行中都会检测到重复数字。这种直观感受与实际结果之间的巨大差异,正是“生日悖论”在随机数生成领域的体现。
要理解上述现象,我们需要引入概率论中的一个著名概念——生日悖论(Birthday Paradox)。生日悖论指出,在一个随机选择的人群中,只要人数达到23人,其中至少两人拥有相同生日的概率就超过50%。这听起来似乎反直觉,因为一年有365天,23人只占很小一部分。
将生日悖论推广到随机数生成场景:
在我们的例子中:
生日悖论的核心在于,我们不是在寻找某个特定数字的重复,而是在寻找任意两个数字的重复。随着生成数字数量的增加,潜在的配对数量呈平方级增长,因此发生碰撞的概率会迅速上升。
粗略的估计,当从N个可能的数值中随机抽取n个数值时,出现至少一个重复的概率近似为 1 - e^(-n^2 / (2N))。 对于我们的例子:
碰撞概率 ≈ 1 - e^(-1000^2 / (2 * 10,000,000)) ≈ 1 - e^(-1,000,000 / 20,000,000) ≈ 1 - e^(-0.05) ≈ 1 - 0.9512 ≈ 0.0488,即约 4.88%。
这个计算结果与原始问题中“约50%”的观察略有出入,但仍然远高于直观认为的“1/10,000”或“1/1,000”。原始问题中提到“50%的运行”,可能是在测试了10组1000个数字的生成后,总体的碰撞概率。如果将生成1000个数字的碰撞概率视为一次实验,那么在10次实验中至少有一次碰撞的概率会更高。
更直观的理解是,当生成的数量 n 接近随机数空间大小 N 的平方根时(即 n ≈ sqrt(N)),发生重复的概率就会变得非常高。在我们的例子中,sqrt(10,000,000) ≈ 3162。虽然我们只生成了1000个数字,但这个数量已经足以让碰撞概率显著提升,而不是微乎其微。
理解了生日悖论后,我们可以采取更有效的策略来生成唯一的随机数。简单地检查列表中的重复项并重试,在碰撞概率高时效率会非常低下,甚至可能导致无限循环。
以下是一些推荐的策略:
使用Set数据结构进行去重和收集 这是最常见且高效的方法。Set集合自动处理重复元素,只有当元素不存在时才会被添加。我们可以持续生成随机数,直到Set的大小达到目标数量。
import scala.collection.mutable
import scala.util.Random
object UniqueRandomNumberGenerator {
/**
* 生成指定数量的唯一随机数。
* @param count 需要生成的唯一随机数数量。
* @param maxRange 随机数的上限(不包含)。
* @return 包含指定数量唯一随机数的列表。
*/
def generateUniqueRandomNumbers(count: Int, maxRange: Int): List[Int] = {
if (count > maxRange) {
throw new IllegalArgumentException(s"无法从 $maxRange 范围内生成 $count 个唯一随机数。")
}
val uniqueNumbers = mutable.Set.empty[Int]
// 当Set的大小未达到目标数量时,持续生成并尝试添加
while (uniqueNumbers.size < count) {
val nextNumber = Random.nextInt(maxRange)
uniqueNumbers.add(nextNumber) // Set.add 会自动处理重复,如果已存在则不添加
}
uniqueNumbers.toList
}
def main(args: Array[String]): Unit = {
val numberOfNumbers = 1000
val range = 10000000 // 10 million
try {
val numbers = generateUniqueRandomNumbers(numberOfNumbers, range)
println(s"成功生成 ${numbers.size} 个唯一随机数。")
println(s"去重后大小: ${numbers.distinct.size}") // 验证是否唯一,应与numbers.size相同
} catch {
case e: IllegalArgumentException => println(s"错误: ${e.getMessage}")
}
}
}这种方法通过利用Set的特性,避免了手动检查重复的开销,并且在内部优化了添加操作。
预生成所有可能值并洗牌(适用于count接近maxRange或maxRange不大时) 如果需要生成的唯一随机数数量 count 接近随机数范围 maxRange,或者 maxRange 本身不大,那么生成所有可能值,然后随机打乱并取前 count 个会更有效。
import scala.util.Random
object ShuffledUniqueGenerator {
/**
* 从指定范围内生成指定数量的唯一随机数,通过洗牌实现。
* @param count 需要生成的唯一随机数数量。
* @param maxRange 随机数的上限(不包含)。
* @return 包含指定数量唯一随机数的列表。
*/
def generateUniqueRandomNumbersShuffled(count: Int, maxRange: Int): List[Int] = {
if (count > maxRange) {
throw new IllegalArgumentException(s"无法从 $maxRange 范围内生成 $count 个唯一随机数。")
}
// 生成0到maxRange-1的所有数字序列
val allNumbers = (0 until maxRange).toList
// 随机打乱序列,然后取前count个
Random.shuffle(allNumbers).take(count)
}
def main(args: Array[String]): Unit = {
val numberOfNumbers = 1000
val range = 10000000 // 10 million
// 注意:当range非常大时,生成allNumbers会消耗大量内存
// 此方法更适用于range相对较小的情况
if (range < 100000) { // 示例限制,实际根据内存情况调整
val numbers = generateUniqueRandomNumbersShuffled(numberOfNumbers, range)
println(s"洗牌法生成 ${numbers.size} 个唯一随机数。")
} else {
println("范围过大,洗牌法可能不适用,请考虑其他方法。")
}
}
}这种方法保证了绝对的唯一性,且性能稳定,但缺点是当 maxRange 很大时,生成 allNumbers 列表可能会消耗大量内存。
使用全局唯一标识符(UUID) 如果对“随机数”的定义可以扩展到字符串形式的唯一标识符,那么使用java.util.UUID是一个非常可靠的选择。UUID的碰撞概率极低,几乎可以认为是全球唯一的。
import java.util.UUID
object UUIDGenerator {
def generateUniqueUUIDs(count: Int): List[String] = {
(1 to count).map(_ => UUID.randomUUID().toString).toList
}
def main(args: Array[String]): Unit = {
val numberOfUUIDs = 1000
val uuids = generateUniqueUUIDs(numberOfUUIDs)
println(s"生成 ${uuids.size} 个唯一UUID。")
println(s"去重后大小: ${uuids.distinct.size}") // 验证是否唯一
}
}这种方法生成的不是纯数字,但对于需要唯一标识符的场景非常适用。
总之,在随机数生成中遇到意外的重复现象时,生日悖论是解释其原因的关键。通过理解这一概率原理,并选择合适的高效策略(如使用Set去重、预生成洗牌或UUID),我们可以有效地生成所需的唯一随机数,避免潜在的逻辑错误和性能问题。
以上就是理解随机数生成中的重复问题:生日悖论的启示的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号