轨道
/
Julia
Julia
/
练习
/
Exercism 矩阵
Exercism 矩阵

Exercism 矩阵

学习练习

简介

早在 Vectors 概念里,我们就指出过:“数组的尺寸可以任意大(只受硬件内存的限制),维数也可以任意多。”

从那以后,为了保持简单,我们基本都避开了多维数组。如果你试着去读 Julia 的参考文档,感受一下它们那完整的复杂度,就会更明白这个决定。

不过,高维数组在科学计算中_非常非常_重要,所以我们必须理解它们。

术语说明: 沿袭几百年来数学上的惯例,我们把一维数组称为 Vectors,把二维数组称为 Matrices。

本文中的例子大多会是矩阵。处理 3 维或更多维度时,语法几乎完全相同,但在二维屏幕上,输出既难读又容易让人困惑。

构造数组

我们已经通过把逗号分隔的列表放进方括号,创建过很多向量。分号也可以用作分隔符。

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> w = [1; 2; 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v == w
true

如果用空格(或制表符)作分隔符,结果就不一样了。

julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

Julia 现在会把它定义成一个 1×3 的矩阵(在其他语境里,我们会称它为 row vector)。

一般来说,空格把元素横向拼在一起,分号(或换行)把元素纵向拼在一起。

这里说的“东西”是刻意含糊的,因为不管你给它什么,Julia 都会试着处理。

julia> [v 2v]
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> [v; 2v]
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

函数 hcat() 和 vcat() 做的事情是一样的,只是更明确地表明这是横向和纵向的拼接。更高维的推广则是 cat() 函数。

jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> vcat(v, 2v)
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

直接输入矩阵时,按行主序来写很方便,因为这样符合人的直觉(对使用横排文字的文化来说,看着更舒服):

julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

不过要注意,Julia(和 Fortran、R、Matlab 一样,但_不同于_ C/C++ 或 NumPy)是以列主序存储 N 维数组的,如果你要遍历元素,这会带来巨大的性能差异。善待你的 CPU 缓存,它也会善待你!

# put these integers in 2 rows and 3 columns

julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
 1  3  5
 2  4  6

上面的例子把整数 1 到 6 按列填入一个 2×3 的矩阵。

还有很多实用函数可以构造常见的数组类型(均匀的或随机的)。

julia> zeros(2, 3)  # see also ones()
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> falses(2, 2)  # booleans, see also trues()
2×2 BitMatrix:
 0  0
 0  0

julia> rand(Float32, 2, 3)  # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
 0.768823  0.169633  0.632565
 0.388451  0.109176  0.850381

下标访问

对于二维数组,我们通常按 [row, col] 的顺序使用两个下标。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[1, 2] # row 1, col 2
2

# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]

julia> m[3]
2

最后一个例子可能有点出乎意料:用_单个_下标不会报错,而是返回单个元素。

原因还是要回到前面关于列主序的说明:Julia 会先走完第 1 列,再走第 2 列,直到在_内存中_找到第 3 个元素。

小心:这在编写通用库时有些用处,但更可能让你困惑!

查询数组大小时也有类似的问题。length() 给出元素总数,size() 给出一个包含 ndims() 个元素的元组,每个元素对应一个维度的长度。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> length(m)
6

julia> size(m)  # 2 rows, 3 cols
(2, 3)

julia> ndims(m)  # how many dimensions? Like `m |> size |> length`
2

用区间和数组进行下标访问

我们可以很方便地复制子矩阵。

在下面的例子里,reshape() 函数会把数组按列填充,强制转换成指定的维度,然后我们对它做切片。

julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
 1  5   9
 2  6  10
 3  7  11
 4  8  12

julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
 2  6
 3  7
 4  8

# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
 2  6  10
 3  7  11
 4  8  12

单独一个 : 表示“复制这个维度里的所有内容”。

对于不连续的行或列,可以用向量:

julia> m12[[1, 3], :]  # rows 1 and 3
2×3 Matrix{Int64}:
 1  5   9
 3  7  11

对数组应用函数

我们之前见过把 sum() 和 maximum() 这类聚合函数用在了一维集合上,它们会对所有元素进行操作,并返回一个标量结果。

这在更高维的情况下同样适用。不过,我们有时只想让函数沿某一个维度作用,比如向下求和或横向求和,从而返回一个大小为 1 的 singleton dimension 的数组。

为此,可以使用一个可选的 dims 关键字。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> sum(m)  # sum everything
21

julia> sum(m; dims=1)  # sum down
1×3 Matrix{Int64}:
 5  7  9

julia> sum(m; dims=2)  # sum across
2×1 Matrix{Int64}:
  6
 15

# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
 3
 6

dims 的值就是_变成单例的那个维度_,所以在上面几个例子里,dims=1 => 1×3,dims=2 => 2×1。

推而广之,如果把 dims 设为一个数组或区间,高维数组就能沿多个维度做归约(不过,要读懂输出可能还得费点脑筋!)。

编写能感知维度的函数

dims 关键字在 sum() 这类内置函数里很常见,但我们要怎么在自己的代码里写出等价的功能呢?

一个不错的答案是使用 reduce() 这类高阶函数,这会在后面的概念里详细展开。

另一种办法是,Julia 提供了一些函数,让我们可以把 N 维数组当作嵌套的“向量的向量”来处理。

对于矩阵,eachrow() 和 eachcol() 用起来很方便,但更通用的函数是能处理任意维度的 eachslice()。

# m is as in the previous examples

julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
 [1, 2, 3]
 [4, 5, 6]

julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
 [1, 4]
 [2, 5]
 [3, 6]

这个类型看着有点吓人,但这只是因为它是原数组的一个_视图_,让我们不用复制就能直接操作它。Julia 数组的大小可能达到 TB 级别,所以复制可能是性能上的噩梦!

这样的视图可以用于循环、广播,或者我们到目前为止在教学大纲里见过的其他各种操作。

另外,推导式在处理数组输入时可以非常强大、非常灵活。Loops 概念里提到过一些简单的用法,后面还会有更详细的讨论。

多维广播

一维的情况我们在 Vector Operations 概念里讨论过。

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v .- 0.5
3-element Vector{Float64}:
 0.7
 1.0
 1.2

带点的运算符 .- 会把单个的 0.5 通过 broadcasting 扩展到匹配的维度,即等同于 [0.5, 0.5, 0.5],然后再逐元素做减法。

把它扩展到更高维,其实也是同样的道理。

例如,把一个 2x1 的行向量 [1.0 1.5 2.0] 与 2x3 的矩阵 [1 2 3; 4 5 6] 的乘法进行广播时,就相当于先把 [1.0 1.5 2.0] 广播成 [1.0 1.5 2.0; 1.0 1.5 2.0],再逐元素相乘。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m .* [1.0 1.5 2.0]  # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0]  # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m ./ [1, 2]  # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

julia> m ./ [1 1 1; 2 2 2]  # 2x3 elementwise division
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

注意: 进行广播时,非单例维度的大小应当一致(例如 2x3 Matrix .* 2x1 Matrix)。

此外,函数也可以像对 Vector 那样,逐元素广播到 Matrix 的每个元素上。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> (x -> x^2).(m)  # broadcast a function to all elements
2×3 Matrix{Int64}:
  1   4   9
 16  25  36

一开始你可能会觉得,程序员犯迷糊的空间会随着维数指数级增长,但多练习就会好很多。另外,如果你熟悉 NumPy 数组,那些经验也能很好地迁移到 Julia,尽管语法不太一样。

说明

点阵是由点和空白组成的二维图像。 点在空白的背景上构成想要的图案。

点阵图像可以作为二维的Matrix存储在内存中并进行操作。 在下文中,矩阵里的“空白”用0表示,“点”则用非零值表示。

1. 定义 Exercism 标志Matrix

该Matrix如下所示,其中0表示空白,1表示点:

[
    0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
    0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
    0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
    0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
    0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]

把它“渲染”出来,长这样:

  XX          XX  
 X              X 
 X   X      X   X 
 X  X X    X X  X 
 X              X 
X                X
 X    X    X    X 
 X     X  X     X 
 X      XX      X 
 X              X 
  XX          XX

2. 定义让标志皱眉的函数

定义frown!()和frown()函数,它们接收 Exercism 标志Matrix。 返回一个Matrix,其中的微笑嘴巴改成了皱眉嘴巴。

得到的Matrix渲染出来会是这样:

  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X      XX      X
 X     X  X     X
 X    X    X    X
 X              X
  XX          XX

3. 拼出一面贴纸墙

定义stickerwall()函数,它以 Exercism 矩阵为输入。 返回一个点阵的Matrix,渲染出来如下:

  XX          XX    XX          XX
 X              X  X              X
 X   X      X   X  X   X      X   X
 X  X X    X X  X  X  X X    X X  X
 X              X  X              X
X                XX                X
 X    X    X    X  X      XX      X
 X     X  X     X  X     X  X     X
 X      XX      X  X    X    X    X
 X              X  X              X
  XX          XX    XX          XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  XX          XX    XX          XX
 X              X  X              X
 X   X      X   X  X   X      X   X
 X  X X    X X  X  X  X X    X X  X
 X              X  X              X
X                XX                X
 X      XX      X  X    X    X    X
 X     X  X     X  X     X  X     X
 X    X    X    X  X      XX      X
 X              X  X              X
  XX          XX    XX          XX

4. 把点改为每列的像素数

我们并不局限于只用1表示点,所以如果需要,还可以编码其他有用的信息。 定义colpixelcount()函数,它以任何用1作为点的点阵为输入。 返回一个同样大小的点阵,其中每一列的点变成该列中点的个数。

以 Exercism 标志Matrix作为输入时,输出如下所示的Matrix:

[
    0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
    0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
    0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
    0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
    0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]

5. 渲染点阵

与其只是谈论怎么渲染我们的作品,不如真正把它做出来,方便查看。 定义render()函数,它以点阵为输入。 返回一个字符串,其中点渲染为'X',0渲染为' ',每一行之间用换行符连接。

julia> render(E)
"  XX          XX  \n X              X \n X   X      X   X \n X  X X    X X  X \n X              X \nX                X\n X    X    X    X \n X     X  X     X \n X      XX      X \n X              X \n  XX          XX  "

打印出来时,它应该能按预期渲染出 ExercismMatrix。

julia> print(render(E))
  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X    X    X    X
 X     X  X     X
 X      XX      X
 X              X
  XX          XX

对于不是1的“点”,这也应该同样有效:

julia> print(render(colpixelcount(E)))
  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X    X    X    X
 X     X  X     X
 X      XX      X
 X              X
  XX          XX
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 Exercism 矩阵 了吗?

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