轨道
/
Julia
Julia
/
练习
/
船长日志
船长日志

船长日志

学习练习

简介

许多程序需要用(看似)随机的值来模拟现实世界的事件。

常见且熟悉的例子包括:

  • 抛硬币:从('H', 'T')中随机取一个值。
  • 掷骰子:从 1 到 6 中随机取一个整数。
  • 洗一副牌:得到牌列表的一种随机排列。

用计算机生成真正随机的值是一项出人意料地困难的技术挑战,所以这些结果有时被称为“伪随机”。

重要:本概念_不_涉及加密安全的随机数,那是一项困难得多的挑战。

不过,设计良好的库,比如 Julia 标准库中的Random模块,速度快、灵活,给出的结果对建模、仿真和游戏中的大多数应用来说都足够好。

Julia 把随机功能分散在好几个地方:

  • Base里只有几个基础但非常通用的函数,随时可用。
  • Random模块里有更丰富的选项。
  • 更专门的功能在需要先安装才能使用的包里(这些包在 Exercism 上不可用)。

Random是标准库的一部分,很可能已经预装,但你需要在程序顶部加上using Random,才能把它里面的内容引入命名空间。

rand()函数

这个函数做什么,取决于你传给它的实参。 可选的方式有_很多_。

不带实参时,它会生成一个 0(含)到 1 之间的浮点数。 这是一个uniform分布,所有值出现的概率都相同,正如后文“处理分布”一节所讨论的。

传入一个整数实参,会生成一个该长度的向量。

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

要得到不同的范围,只需对结果做适当的平移和缩放。

下面这个例子用广播来做减法,这一点在“向量运算”概念中有介绍。 其中的.-只是把这个运算应用到每个向量元素上。

# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
 -0.5303906759076336
  0.9635682226775855
 -0.048823697086981754
  0.465842804648374
  0.9880834344780736

如果只把类型作为实参,rand会用typemin和typemax作为上下限。 这大概不是你想要的!

要生成随机整数,我们可以传入一个范围,还可以选择性地指定生成多少个值。

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

在上面rand(1:10, 5)这个例子里,注意有些值(碰巧)重复了,因为每次抽取都是独立的。 这就是“有放回抽样”,下文会更详细地讨论。

要在某个范围内生成浮点值,通常需要指定步长。 否则步长会默认取 1.0,这很少有用。

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

或者,传入一个数组或元组,rand会随机返回其中一个元素:

julia> rand([4, 9, 16, 25])
16

# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
 1
  "name"

有放回与无放回抽样

假设有一个袋子,里面有 3 个红球和 4 个绿球,我们随机从袋子里摸出一个球。 要再摸第二个球,有两种可能:

  1. 把第一个球放回袋子,充分摇匀,再摸一个。 此时球的数量和之前一样(7 个),而且_红球与绿球的比例也不变_。
  2. 把第一个球放到桌上,再摸第二个。 此时袋子里只剩 6 个球,红绿比例取决于第一个球的颜色。

情形 1 是有放回,情形 2 是无放回,两者得到的结果不同。

在 Julia 中模拟无放回抽样,有几种选择。

最简单的方法(在 Exercism 上也是唯一的方法)是用Random.shuffle()把各元素随机排序,然后取前n个元素。 对于小问题这没问题,但可能无法很好地扩展到大型集合:即使你只要其中一小部分,shuffle也需要生成整个数组。

要“正规地”做有放回抽样,请安装StatsBase.jl包。 它提供sample()函数,选项一应俱全。

我们可以合理地期待,未来某个版本会把类似功能加入Random,让它成为标准库的一部分(本文档中的代码示例是在 Julia 1.11 上测试的)。

处理分布

到目前为止,我们关注的都是所有结果概率相同的情形。 例如,rand(1:100)给出 1 到 100 之间任意整数的概率都相同。

许多现实世界的情况远没有这么简单。 因此,统计学家创造了各种各样的distributions,用数学来描述“现实世界”的结果。

均匀分布

上文介绍的rand()函数用于所有概率都相等的情况。 这叫作uniform分布。

高斯分布

也叫“正态”分布或“钟形”曲线,这是描述测量值不精确性的一种非常常见的方式。

例如,假设你工作的工厂刚买了 10,000 个本应完全相同的螺栓。 你想设置工厂机器人来处理它们,于是称量了 100 个作为样本,发现它们的平均(或mean)重量是 4.731g。 这极不可能意味着它们每个都恰好重 4.731g。 也许你发现值分布在 4.627 到 4.794g 之间,但聚集在 4.731g 附近。

这就是Gaussian distribution,其概率在平均值处达到峰值,并向两侧对称地逐渐减小(因此是“钟形”)。 要在软件中模拟它,我们需要某种方式来指定曲线的宽度(通常来说,昂贵的螺栓会比便宜的螺栓更紧密地聚集在平均值附近!)。

按照惯例,这用standard deviation来表示:值小则曲线又尖又窄,值大则曲线又低又宽。 数学家喜欢希腊字母,所以我们用μ('mu')表示平均值,用σ('sigma')表示标准差。 因此,如果你读到“95% 的值都在 μ 的 2σ 范围内”或“希格斯玻色子以 5 西格玛的置信度被探测到”,这类说法都和标准差有关。

关于这一点,Statistics概念里会有更多介绍。

randn()函数

它是“random normal”的缩写,和rand()的浮点版本类似,只是值服从平均值为 0、标准差为 1 的高斯分布。

同样地,你可能想对randn的原始输出按标准差进行缩放,并按平均值进行平移。 下面这个例子把它转换成平均值为 30、标准差为 5。

julia> raw = randn(5)
5-element Vector{Float64}:
  3.0762588867281475
  1.5101100620253902
 -0.5914858221637778
  0.684175554069735
 -0.8416433926114673

julia> raw * 5 .+ 30
5-element Vector{Float64}:
 45.38129443364074
 37.55055031012695
 27.04257088918111
 33.420877770348675
 25.791783036942665

光看输出,很难看出原始输出比均匀分布更聚集在 0 附近。 如果你不信,就生成 1000 个或更多并画出来,这样会更明显。

Random模块

这个模块包含下一层的功能,这些功能没有放进Base,是为了帮助把 Julia 默认配置的体积降到最小。

Random为Base中的rand和randn补充了会修改原值的版本:rand!和randn!。

一个有用的补充是randstring,它生成指定长度的字符串。 默认情况下,它使用大小写字母加上 0 到 9 的数字,但也可以指定其他选项。

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

此外,还有一个bitrand函数,用来生成指定长度的随机BitArray。

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

洗牌与排列

要随机打乱Vector中的元素,可以用shuffle;还有shuffle!,可以就地修改输入向量。

julia> v = ['A', '1', '2', 'J', 'Q', 'K'];

julia> shuffle(v)
6-element Vector{Char}:
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)

# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

有时改用打乱后的下标会更有用。 这时可以用randperm(n),其中 n 是序列的长度。

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

实际上,上面这个例子得到的结果和shuffle(1:6)一样。

说明

Mary 是电视剧 Star Trek: The Next Generation 的忠实粉丝。她经常玩纸上角色扮演游戏,在游戏里,她和朋友们假扮 Starship Enterprise 的船员。Mary 扮演的角色是 Picard 舰长,所以她得负责记录舰长日志。她很喜欢游戏里需要发挥创意的部分,但不喜欢临场编造随机数据。

帮 Mary 写一些随机生成器,用来生成舰长日志中常见的数据吧。

1. 生成随机行星

Starship Enterprise 在航行途中会遇到许多行星。星际迷航宇宙中的行星按照各自的特性被划分成不同类别。比如,地球是一颗 M 级行星。所有可能的行星级别有:D、H、J、K、L、M、N、R、T 和 Y。

实现 random_planet 函数。它应该随机返回其中一种行星级别。

julia> random_planet_class()
'K'

2. 生成随机的星舰注册号

Enterprise(注册号 NCC-1701)可不是唯一在太空中飞行的星舰!当它与其他星舰会合时,Mary 需要记录下那艘星舰的注册号。

注册号以“NCC-”为前缀,后面是 1000 到 9999 之间的数字(含两端)。

实现 random_ship_registry_number 函数,返回一个随机的星舰注册号。

julia> random_ship_registry_number()
"NCC-1947"

3. 生成随机的星历

日志要是不包含日期,那还有什么用?

星历是一个浮点数。Starship Enterprise 在 The Next Generation 第一季中的冒险,发生在星历 41000.0 到 42000.0 之间。其中的“4”代表 24 世纪,“1”代表第一季。

实现 random_stardate 函数,返回一个介于 41000.0(含)和 42000.0 之间的浮点数。

julia> random_stardate()
41458.15721310934

4. 生成四舍五入后的星历

有人抱怨星历的小数点太多了,于是 Mary 决定把日志里的星历四舍五入到一位小数。

虽然可以给 random_stardate() 返回的值做四舍五入,但你发现还有一种简单得多的做法。

实现 random_stardate_v2 函数,返回一个介于 41000.0 和 42000.0 之间(含两端)、保留一位小数的浮点数。

julia> random_stardate_v2()
41732.6

5. 从列表中随机挑选几艘星舰

游戏有一个版本需要与其他星舰互动,Mary 得挑选几艘出来。

这里有一份所有可能星舰的完整列表,但每局游戏只需要其中几艘,而且应该随机挑选。

实现 pick_starships(starships, number_needed) 函数,它接收一个由不重复的星舰注册号组成的向量,并返回一个长度为 number_needed 的向量。结果中的值应当取自 starships,且不能重复。

julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
 "NCC-1228"
 "NCC-5011"

5a. 可选:思考其他参数类型

我们把 starships 实参限制成了向量。如果想让 pick_starships() 也能处理其他可迭代对象,比如元组或集合,你会怎么实现?

这部分没有测试。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 船长日志 了吗?

注册 Exercism,借助 35 个概念128 个练习 和真人导师指导,学习并掌握 Julia,全部免费。