一维和多维数组

Julia,与大多数技术计算语言一样,提供了一流的数组实现。大多数技术计算语言都非常重视其数组实现,而牺牲了其他容器。Julia 并没有以任何特殊方式处理数组。数组库几乎完全是用 Julia 本身实现的,并且像用 Julia 编写的任何其他代码一样,其性能源于编译器。因此,也可以通过继承 AbstractArray 来定义自定义数组类型。有关实现自定义数组类型的更多详细信息,请参阅 关于 AbstractArray 接口的手册部分

数组是在多维网格中存储的对象集合。允许使用零维数组,请参阅 此常见问题解答条目。在最一般的情况下,数组可能包含类型为 Any 的对象。对于大多数计算目的,数组应包含更特定类型的对象,例如 Float64Int32

通常,与许多其他技术计算语言不同,Julia 不期望程序以向量化的风格编写才能获得良好的性能。Julia 的编译器使用类型推断并为标量数组索引生成优化代码,允许程序以方便且易读的风格编写,而不会牺牲性能,并且有时使用更少的内存。

在 Julia 中,所有函数的参数都是 通过共享传递(即通过指针)。一些技术计算语言按值传递数组,虽然这可以防止被调用者意外修改调用者中的值,但它使得避免不必要的数组复制变得困难。按照约定,以 ! 结尾的函数名表示它将更改或破坏其一个或多个参数的值(例如,比较 sortsort!)。被调用者必须进行显式复制,以确保它们不会修改其不打算更改的输入。许多非变异函数都是通过在输入的显式副本上调用相同名称但末尾添加 ! 的函数来实现的,并返回该副本。

基本函数

函数描述
eltype(A)A 中包含的元素的类型
length(A)A 中元素的数量
ndims(A)A 的维度数
size(A)包含 A 维度的元组
size(A,n)A 沿维度 n 的大小
axes(A)包含 A 的有效索引的元组
axes(A,n)表示沿维度 n 的有效索引的范围
eachindex(A)用于访问 A 中每个位置的有效迭代器
stride(A,k)沿维度 k 的步长(相邻元素之间的线性索引距离)
strides(A)每个维度步长的元组

构造和初始化

提供了许多用于构造和初始化数组的函数。在以下函数列表中,带有 dims... 参数的调用可以采用维度大小的单个元组或作为可变数量的参数传递的一系列维度大小。这些函数中的大多数还接受第一个输入 T,它是数组的元素类型。如果省略类型 T,则默认为 Float64

函数描述
Array{T}(undef, dims...)一个未初始化的密集 Array
zeros(T, dims...)一个全为零的 Array
ones(T, dims...)一个全为一的 Array
trues(dims...)一个所有值为 trueBitArray
falses(dims...)一个所有值为 falseBitArray
reshape(A, dims...)一个包含与 A 相同数据但具有不同维度的数组
copy(A)复制 A
deepcopy(A)复制 A,递归复制其元素
similar(A, T, dims...)一个与 A 类型相同(密集、稀疏等)但具有指定元素类型和维度的未初始化数组。第二个和第三个参数都是可选的,如果省略,则默认为 A 的元素类型和维度。
reinterpret(T, A)一个与 A 具有相同二进制数据但元素类型为 T 的数组
rand(T, dims...)一个具有随机、独立同分布[1]且均匀分布值的 Array。对于浮点类型 T,值位于半开区间 $[0, 1)$ 中。
randn(T, dims...)一个具有随机、独立同分布且标准正态分布值的 Array
Matrix{T}(I, m, n)mn 列的单位矩阵。需要 using LinearAlgebra 才能使用 I
range(start, stop, n)startstopn 个线性间隔元素的范围
fill!(A, x)用值 x 填充数组 A
fill(x, dims...)一个填充值为 xArray。特别是,fill(x) 构造一个包含 x 的零维 Array

要查看我们可以传递维度的各种方式,请考虑以下示例

julia> zeros(Int8, 2, 3)
2×3 Matrix{Int8}:
 0  0  0
 0  0  0

julia> zeros(Int8, (2, 3))
2×3 Matrix{Int8}:
 0  0  0
 0  0  0

julia> zeros((2, 3))
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

这里,(2, 3) 是一个 Tuple,第一个参数——元素类型——是可选的,默认为 Float64

数组字面量

数组也可以使用方括号直接构造;语法[A, B, C, ...]创建一个包含逗号分隔的参数作为其元素的一维数组(即向量)。结果数组的元素类型(eltype)由括号内参数的类型自动确定。如果所有参数都是相同类型,则该类型即为其eltype。如果它们都具有共同的提升类型,则使用convert将其转换为该类型,并且该类型为数组的eltype。否则,将构造一个可以容纳任何内容的异构数组——Vector{Any};这包括未给出任何参数的字面量[]数组字面量可以被类型化,语法为T[A, B, C, ...],其中T是一个类型。

julia> [1, 2, 3] # An array of `Int`s
3-element Vector{Int64}:
 1
 2
 3

julia> promote(1, 2.3, 4//5) # This combination of Int, Float64 and Rational promotes to Float64
(1.0, 2.3, 0.8)

julia> [1, 2.3, 4//5] # Thus that's the element type of this Array
3-element Vector{Float64}:
 1.0
 2.3
 0.8

julia> Float32[1, 2.3, 4//5] # Specify element type manually
3-element Vector{Float32}:
 1.0
 2.3
 0.8

julia> []
Any[]

连接

如果方括号内的参数由单个分号 (;) 或换行符而不是逗号分隔,则它们的内容将垂直连接在一起,而不是将参数本身用作元素。

julia> [1:2, 4:5] # Has a comma, so no concatenation occurs. The ranges are themselves the elements
2-element Vector{UnitRange{Int64}}:
 1:2
 4:5

julia> [1:2; 4:5]
4-element Vector{Int64}:
 1
 2
 4
 5

julia> [1:2
        4:5
        6]
5-element Vector{Int64}:
 1
 2
 4
 5
 6

类似地,如果参数由制表符或空格或双分号分隔,则它们的内容将水平连接在一起。

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

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

julia> [1 2 3] # Numbers can also be horizontally concatenated
1×3 Matrix{Int64}:
 1  2  3

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

单个分号(或换行符)和空格(或制表符)可以组合起来同时进行水平和垂直连接。

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

julia> [zeros(Int, 2, 2) [1; 2]
        [3 4]            5]
3×3 Matrix{Int64}:
 0  0  1
 0  0  2
 3  4  5

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

空格(和制表符)比分号具有更高的优先级,首先执行任何水平连接,然后连接结果。另一方面,使用双分号进行水平连接,会在水平连接结果之前执行任何垂直连接。

julia> [zeros(Int, 2, 2) ; [3 4] ;; [1; 2] ; 5]
3×3 Matrix{Int64}:
 0  0  1
 0  0  2
 3  4  5

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

就像;;;分别在第一和第二维度进行连接一样,使用更多分号扩展了相同的通用方案。分隔符中分号的数量指定特定的维度,因此;;;在第三维度连接,;;;;在第四维度连接,依此类推。较少的分号优先级更高,因此通常首先连接较低的维度。

julia> [1; 2;; 3; 4;; 5; 6;;;
        7; 8;; 9; 10;; 11; 12]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

和之前一样,用于水平连接的空格(和制表符)比任何数量的分号都具有更高的优先级。因此,也可以通过首先指定其行,并将其元素以类似于其布局的方式文本排列来编写更高维度的数组。

julia> [1 3 5
        2 4 6;;;
        7 9 11
        8 10 12]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

julia> [1 2;;; 3 4;;;; 5 6;;; 7 8]
1×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
 1  2

[:, :, 2, 1] =
 3  4

[:, :, 1, 2] =
 5  6

[:, :, 2, 2] =
 7  8

julia> [[1 2;;; 3 4];;;; [5 6];;; [7 8]]
1×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
 1  2

[:, :, 2, 1] =
 3  4

[:, :, 1, 2] =
 5  6

[:, :, 2, 2] =
 7  8

尽管它们都表示在第二维度进行连接,但空格(或制表符)和;;不能出现在同一个数组表达式中,除非双分号只是用作“行延续”字符。这允许单个水平连接跨越多行(而不会将换行符解释为垂直连接)。

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

终止分号也可用于添加尾随长度为 1 的维度。

julia> [1;;]
1×1 Matrix{Int64}:
 1

julia> [2; 3;;;]
2×1×1 Array{Int64, 3}:
[:, :, 1] =
 2
 3

更一般地,可以通过cat函数来实现连接。这些语法是函数调用的简写,这些函数本身是便利函数。

语法函数描述
cat沿维度k连接输入数组
[A; B; C; ...]vcatcat(A...; dims=1)的简写
[A B C ...]hcatcat(A...; dims=2)的简写
[A B; C D; ...]hvcat同时进行垂直和水平连接
[A; C;; B; D;;; ...]hvncat同时进行 n 维连接,其中分号的数量指示要连接的维度

类型化数组字面量

可以使用语法T[A, B, C, ...]构造具有特定元素类型的数组。这将构造一个元素类型为T的一维数组,初始化为包含元素ABC等。例如,Any[x, y, z]构造一个可以包含任何值的异构数组。

连接语法也可以类似地以类型为前缀,以指定结果的元素类型。

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

julia> Int8[[1 2] [3 4]]
1×4 Matrix{Int8}:
 1  2  3  4

推导式

推导式提供了一种构建数组的通用且强大的方法。推导式的语法类似于数学中的集合构造符号。

A = [ F(x, y, ...) for x=rx, y=ry, ... ]

此形式的含义是F(x,y,...)使用变量xy等进行评估,这些变量在其给定的值列表中取每个值。值可以指定为任何可迭代对象,但通常是像1:n2:(n-1)这样的范围,或像[1.2, 3.4, 5.7]这样的显式值数组。结果是一个 N 维密集数组,其维度是变量范围rxry等的维度的连接,并且每个F(x,y,...)评估都返回一个标量。

以下示例计算沿一维网格的当前元素及其左右邻居的加权平均值。

julia> x = rand(8)
8-element Array{Float64,1}:
 0.843025
 0.869052
 0.365105
 0.699456
 0.977653
 0.994953
 0.41084
 0.809411

julia> [ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
6-element Array{Float64,1}:
 0.736559
 0.57468
 0.685417
 0.912429
 0.8446
 0.656511

结果数组类型取决于计算元素的类型,就像数组字面量一样。为了显式控制类型,可以在推导式前面加上类型。例如,我们可以通过编写以下内容来请求单精度结果:

Float32[ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]

生成器表达式

推导式也可以不使用包围方括号来编写,生成一个称为生成器的对象。此对象可以被迭代以按需生成值,而不是预先分配数组并将其存储起来(请参阅迭代)。例如,以下表达式对一个序列求和,而无需分配内存。

julia> sum(1/n^2 for n=1:1000)
1.6439345666815615

在参数列表中编写具有多个维度的生成器表达式时,需要使用括号将生成器与后续参数分隔开。

julia> map(tuple, 1/(i+j) for i=1:2, j=1:2, [1:4;])
ERROR: syntax: invalid iteration specification

for之后的每个逗号分隔表达式都被解释为范围。添加括号允许我们向map添加第三个参数。

julia> map(tuple, (1/(i+j) for i=1:2, j=1:2), [1 3; 2 4])
2×2 Matrix{Tuple{Float64, Int64}}:
 (0.5, 1)       (0.333333, 3)
 (0.333333, 2)  (0.25, 4)

生成器是通过内部函数实现的。就像语言其他地方使用的内部函数一样,来自封闭作用域的变量可以在内部函数中“捕获”。例如,sum(p[i] - q[i] for i=1:n)从封闭作用域捕获三个变量pqn。捕获的变量可能会带来性能挑战;请参阅性能提示

生成器和推导式中的范围可以通过编写多个for关键字来依赖于先前的范围。

julia> [(i, j) for i=1:3 for j=1:i]
6-element Vector{Tuple{Int64, Int64}}:
 (1, 1)
 (2, 1)
 (2, 2)
 (3, 1)
 (3, 2)
 (3, 3)

在这种情况下,结果始终是一维的。

可以使用if关键字过滤生成的数值。

julia> [(i, j) for i=1:3 for j=1:i if i+j == 4]
2-element Vector{Tuple{Int64, Int64}}:
 (2, 2)
 (3, 1)

索引

索引 n 维数组A的一般语法为

X = A[I_1, I_2, ..., I_n]

其中每个I_k可以是标量整数、整数数组或任何其他支持的索引。这包括Colon (:) 以选择整个维度内的所有索引、形式为a:ca:b:c的范围以选择连续或跨步的子部分,以及布尔数组以选择其true索引处的元素。

如果所有索引都是标量,则结果X是数组A中的单个元素。否则,X是一个数组,其维度数与所有索引的维度之和相同。

例如,如果所有索引I_k都是向量,则X的形状将为(length(I_1), length(I_2), ..., length(I_n)),其中Xi_1, i_2, ..., i_n位置包含值A[I_1[i_1], I_2[i_2], ..., I_n[i_n]]

示例

julia> A = reshape(collect(1:16), (2, 2, 2, 2))
2×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
 1  3
 2  4

[:, :, 2, 1] =
 5  7
 6  8

[:, :, 1, 2] =
  9  11
 10  12

[:, :, 2, 2] =
 13  15
 14  16

julia> A[1, 2, 1, 1] # all scalar indices
3

julia> A[[1, 2], [1], [1, 2], [1]] # all vector indices
2×1×2×1 Array{Int64, 4}:
[:, :, 1, 1] =
 1
 2

[:, :, 2, 1] =
 5
 6

julia> A[[1, 2], [1], [1, 2], 1] # a mix of index types
2×1×2 Array{Int64, 3}:
[:, :, 1] =
 1
 2

[:, :, 2] =
 5
 6

请注意,在最后两种情况下,结果数组的大小是如何不同的。

如果将I_1更改为二维矩阵,则X将变为形状为(size(I_1, 1), size(I_1, 2), length(I_2), ..., length(I_n))n+1维数组。矩阵添加了一个维度。

示例

julia> A = reshape(collect(1:16), (2, 2, 2, 2));

julia> A[[1 2; 1 2]]
2×2 Matrix{Int64}:
 1  2
 1  2

julia> A[[1 2; 1 2], 1, 2, 1]
2×2 Matrix{Int64}:
 5  6
 5  6

位置i_1, i_2, i_3, ..., i_{n+1}包含A[I_1[i_1, i_2], I_2[i_3], ..., I_n[i_{n+1}]]处的值。使用标量索引的所有维度都将被丢弃。例如,如果J是索引数组,则A[2, J, 3]的结果是一个大小为size(J)的数组。它的第j个元素由A[2, J[j], 3]填充。

作为此语法的特殊部分,end关键字可用于表示索引括号内每个维度的最后一个索引,由正在索引的最内部数组的大小确定。不带end关键字的索引语法等效于对getindex的调用。

X = getindex(A, I_1, I_2, ..., I_n)

示例

julia> x = reshape(1:16, 4, 4)
4×4 reshape(::UnitRange{Int64}, 4, 4) with eltype Int64:
 1  5   9  13
 2  6  10  14
 3  7  11  15
 4  8  12  16

julia> x[2:3, 2:end-1]
2×2 Matrix{Int64}:
 6  10
 7  11

julia> x[1, [2 3; 4 1]]
2×2 Matrix{Int64}:
  5  9
 13  1

索引赋值

在 n 维数组A中赋值的一般语法为

A[I_1, I_2, ..., I_n] = X

其中每个I_k可以是标量整数、整数数组或任何其他支持的索引。这包括Colon (:) 以选择整个维度内的所有索引、形式为a:ca:b:c的范围以选择连续或跨步的子部分,以及布尔数组以选择其true索引处的元素。

如果所有索引I_k都是整数,则AI_1, I_2, ..., I_n位置的值将被X的值覆盖,如果需要,将转换为Aeltype

如果任何索引I_k本身是一个数组,则右侧X也必须是一个数组,其形状与索引A[I_1, I_2, ..., I_n]的结果相同,或是一个具有相同元素数量的向量。AI_1[i_1], I_2[i_2], ..., I_n[i_n]位置的值将被X[I_1, I_2, ..., I_n]的值覆盖,如果需要,将进行转换。元素级赋值运算符.=可用于广播X到选定的位置。

A[I_1, I_2, ..., I_n] .= X

就像在索引中一样,end关键字可用于表示索引括号内每个维度的最后一个索引,由正在赋值的数组的大小确定。不带end关键字的索引赋值语法等效于对setindex!的调用。

setindex!(A, X, I_1, I_2, ..., I_n)

示例

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

julia> x[3, 3] = -9;

julia> x[1:2, 1:2] = [-1 -4; -2 -5];

julia> x
3×3 Matrix{Int64}:
 -1  -4   7
 -2  -5   8
  3   6  -9

支持的索引类型

在表达式A[I_1, I_2, ..., I_n]中,每个I_k可以是标量索引、标量索引数组或表示标量索引数组的对象,并且可以通过to_indices将其转换为这样的对象。

  1. 标量索引。默认情况下,这包括
    • 非布尔整数
    • CartesianIndex{N},其行为类似于跨越多个维度的整数的N元组(有关更多详细信息,请参见下文)
  2. 标量索引数组。这包括
    • 整数的向量和多维数组
    • 空数组,如[],它不选择任何元素,例如A[[]](不要与A[]混淆)
    • a:ca:b:c这样的范围,它从ac(含)选择连续或跨步的子部分
    • 任何作为AbstractArray子类型的自定义标量索引数组
    • CartesianIndex{N}数组(有关更多详细信息,请参见下文)
  3. 表示标量索引数组的对象,并且可以通过to_indices将其转换为这样的对象。默认情况下,这包括
    • Colon() (:),它表示整个维度或整个数组内的所有索引
    • 布尔数组,它选择其true索引处的元素(有关更多详细信息,请参见下文)

一些示例

julia> A = reshape(collect(1:2:18), (3, 3))
3×3 Matrix{Int64}:
 1   7  13
 3   9  15
 5  11  17

julia> A[4]
7

julia> A[[2, 5, 8]]
3-element Vector{Int64}:
  3
  9
 15

julia> A[[1 4; 3 8]]
2×2 Matrix{Int64}:
 1   7
 5  15

julia> A[[]]
Int64[]

julia> A[1:2:5]
3-element Vector{Int64}:
 1
 5
 9

julia> A[2, :]
3-element Vector{Int64}:
  3
  9
 15

julia> A[:, 3]
3-element Vector{Int64}:
 13
 15
 17

julia> A[:, 3:3]
3×1 Matrix{Int64}:
 13
 15
 17

笛卡尔索引

特殊的CartesianIndex{N}对象表示一个标量索引,其行为类似于跨越多个维度的整数的N元组。例如

julia> A = reshape(1:32, 4, 4, 2);

julia> A[3, 2, 1]
7

julia> A[CartesianIndex(3, 2, 1)] == A[3, 2, 1] == 7
true

单独考虑,这可能看起来微不足道;CartesianIndex 只是将多个整数收集到一个对象中,该对象表示一个多维索引。但是,当与其他产生 CartesianIndex 的索引形式和迭代器结合使用时,这可以生成非常优雅且高效的代码。请参见下面的 迭代,以及一些更高级的示例,请参见 这篇关于多维算法和迭代的博文

也支持 CartesianIndex{N} 的数组。它们表示每个跨越 N 维的标量索引的集合,从而实现了一种有时称为逐点索引的索引形式。例如,它允许从上面 A 的第一个“页面”访问对角线元素

julia> page = A[:, :, 1]
4×4 Matrix{Int64}:
 1  5   9  13
 2  6  10  14
 3  7  11  15
 4  8  12  16

julia> page[[CartesianIndex(1, 1),
             CartesianIndex(2, 2),
             CartesianIndex(3, 3),
             CartesianIndex(4, 4)]]
4-element Vector{Int64}:
  1
  6
 11
 16

这可以通过 点广播 并将其与普通整数索引结合使用(而不是将第一个 pageA 中提取为单独的步骤)来更简单地表达。它甚至可以与 : 结合使用,以同时从两个页面提取两个对角线

julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), 1]
4-element Vector{Int64}:
  1
  6
 11
 16

julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), :]
4×2 Matrix{Int64}:
  1  17
  6  22
 11  27
 16  32
警告

CartesianIndexCartesianIndex 数组与 end 关键字不兼容,后者用于表示维度最后的索引。不要在可能包含 CartesianIndex 或其数组的索引表达式中使用 end

逻辑索引

逻辑索引或使用逻辑掩码索引通常被称为,通过布尔数组进行索引会在其值为 true 的索引处选择元素。通过布尔向量 B 进行索引实际上与通过由 findall(B) 返回的整数向量进行索引相同。类似地,通过 N 维布尔数组进行索引实际上与通过其值为 trueCartesianIndex{N} 向量进行索引相同。逻辑索引必须是与它索引到的维度长度相同的向量,或者它必须是提供的唯一索引,并且与它索引到的数组的大小和维度匹配。通常,直接使用布尔数组作为索引比先调用 findall 更有效。

julia> x = reshape(1:16, 4, 4)
4×4 reshape(::UnitRange{Int64}, 4, 4) with eltype Int64:
 1  5   9  13
 2  6  10  14
 3  7  11  15
 4  8  12  16

julia> x[[false, true, true, false], :]
2×4 Matrix{Int64}:
 2  6  10  14
 3  7  11  15

julia> mask = map(ispow2, x)
4×4 Matrix{Bool}:
 1  0  0  0
 1  0  0  0
 0  0  0  0
 1  1  0  1

julia> x[mask]
5-element Vector{Int64}:
  1
  2
  4
  8
 16

索引数量

笛卡尔索引

索引 N 维数组的常规方法是使用正好 N 个索引;每个索引选择其特定维度中的位置。例如,在三维数组 A = rand(4, 3, 2) 中,A[2, 3, 1] 将选择数组第一个“页面”中第三列第二行的数字。这通常称为笛卡尔索引

线性索引

当只提供一个索引 i 时,该索引不再表示数组特定维度中的位置。相反,它使用线性跨越整个数组的列主迭代顺序选择第 i 个元素。这被称为线性索引。它基本上将数组视为已重塑为一个一维向量,使用 vec

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

julia> A[5]
7

julia> vec(A)[5]
7

数组 A 中的线性索引可以通过 CartesianIndices(A)[i] 转换为 CartesianIndex 以进行笛卡尔索引(参见 CartesianIndices),并且一组 N 个笛卡尔索引可以通过 LinearIndices(A)[i_1, i_2, ..., i_N] 转换为线性索引(参见 LinearIndices)。

julia> CartesianIndices(A)[5]
CartesianIndex(2, 2)

julia> LinearIndices(A)[2, 2]
5

需要注意的是,这些转换的性能存在非常大的不对称性。将线性索引转换为一组笛卡尔索引需要除法和取余数,而反过来只是乘法和加法。在现代处理器中,整数除法可能比乘法慢 10 到 50 倍。虽然某些数组(如 Array 本身)使用线性内存块实现并在其实现中直接使用线性索引,但其他数组(如 Diagonal)需要完整的笛卡尔索引集来进行查找(参见 IndexStyle 以了解哪种情况)。

警告

当遍历数组的所有索引时,最好遍历 eachindex(A) 而不是 1:length(A)。这不仅在 AIndexCartesian 的情况下更快,而且还支持具有自定义索引的数组,例如 OffsetArrays。如果只需要值,那么最好直接迭代数组,即 for a in A

省略和额外的索引

除了线性索引之外,N 维数组在某些情况下可以使用少于或多于 N 个索引进行索引。

如果未索引到的尾随维度长度都为 1,则可以省略索引。换句话说,只有当省略的索引在边界内索引表达式中只有一个可能的值时,才能省略尾随索引。例如,大小为 (3, 4, 2, 1) 的四维数组只能用三个索引进行索引,因为被跳过的维度(第四维)长度为 1。请注意,线性索引优先于此规则。

julia> A = reshape(1:24, 3, 4, 2, 1)
3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64:
[:, :, 1, 1] =
 1  4  7  10
 2  5  8  11
 3  6  9  12

[:, :, 2, 1] =
 13  16  19  22
 14  17  20  23
 15  18  21  24

julia> A[1, 3, 2] # Omits the fourth dimension (length 1)
19

julia> A[1, 3] # Attempts to omit dimensions 3 & 4 (lengths 2 and 1)
ERROR: BoundsError: attempt to access 3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64 at index [1, 3]

julia> A[19] # Linear indexing
19

当使用 A[] 省略所有索引时,此语义提供了一种简单的习惯用法来检索数组中的唯一元素,并同时确保只有一个元素。

类似地,如果超出数组维度的所有索引都为 1(或者更一般地,是 axes(A, d) 的第一个且唯一的元素,其中 d 是该特定维度号),则可以提供超过 N 个索引。例如,这允许向量像一列矩阵一样被索引

julia> A = [8,6,7]
3-element Vector{Int64}:
 8
 6
 7

julia> A[2,1]
6

迭代

遍历整个数组的推荐方法是

for a in A
    # Do something with the element a
end

for i in eachindex(A)
    # Do something with i and/or A[i]
end

当您需要每个元素的值而不是索引时,使用第一个结构。在第二个结构中,如果 A 是具有快速线性索引的数组类型,则 i 将是 Int;否则,它将是 CartesianIndex

julia> A = rand(4, 3);

julia> B = view(A, 1:3, 2:3);

julia> for i in eachindex(B)
           @show i
       end
i = CartesianIndex(1, 1)
i = CartesianIndex(2, 1)
i = CartesianIndex(3, 1)
i = CartesianIndex(1, 2)
i = CartesianIndex(2, 2)
i = CartesianIndex(3, 2)
注意

for i = 1:length(A) 相比,使用 eachindex 迭代提供了一种遍历任何数组类型的高效方法。此外,这也支持具有自定义索引的通用数组,例如 OffsetArrays

数组特性

如果您编写自定义 AbstractArray 类型,您可以指定它具有快速线性索引,使用

Base.IndexStyle(::Type{<:MyArray}) = IndexLinear()

此设置将导致 eachindex 遍历 MyArray 使用整数。如果您不指定此特性,则使用默认值 IndexCartesian()

数组和向量化运算符和函数

以下运算符受数组支持

  1. 一元算术运算符 – -+
  2. 二元算术运算符 – -+*/\^
  3. 比较运算符 – ==!= (isapprox)、

为了方便数学和其他运算的向量化,Julia 提供了点语法 f.(args...),例如 sin.(x)min.(x,y),用于数组或数组和标量混合的逐元素运算(广播运算);这些运算还具有在与其他点调用组合时“融合”成单个循环的额外优势,例如 sin.(cos.(x))

此外,每个二元运算符都支持可应用于数组(以及数组和标量组合)的 点版本,以便进行此类 融合广播操作,例如 z .== sin.(x .* y)

请注意,诸如 == 之类的比较运算符作用于整个数组,并给出单个布尔答案。使用点运算符(如 .==)进行逐元素比较。(对于 < 之类的比较运算符,只有逐元素 .< 版本适用于数组。)

还要注意 max.(a,b)maximum(a) 之间的区别,前者 广播 maxab 上逐元素执行,后者查找 a 中的最大值。min.(a,b)minimum(a) 之间的关系相同。

广播

有时,在不同大小的数组上执行逐元素二元运算很有用,例如将向量添加到矩阵的每一列。执行此操作的一种低效方法是将向量复制到矩阵的大小

julia> a = rand(2, 1); A = rand(2, 3);

julia> repeat(a, 1, 3) + A
2×3 Array{Float64,2}:
 1.20813  1.82068  1.25387
 1.56851  1.86401  1.67846

当维度变大时,这是浪费的,因此 Julia 提供了 broadcast,它扩展数组参数中的单例维度以匹配另一个数组中的对应维度,而无需使用额外的内存,并逐元素应用给定函数

julia> broadcast(+, a, A)
2×3 Array{Float64,2}:
 1.20813  1.82068  1.25387
 1.56851  1.86401  1.67846

julia> b = rand(1,2)
1×2 Array{Float64,2}:
 0.867535  0.00457906

julia> broadcast(+, a, b)
2×2 Array{Float64,2}:
 1.71056  0.847604
 1.73659  0.873631

点运算符(如 .+.*)等效于 broadcast 调用(除了它们会融合,如 上面所述)。还有一个 broadcast! 函数用于指定显式目标(也可以通过 .= 赋值以融合的方式访问)。实际上,f.(args...) 等效于 broadcast(f, args...),提供了一种方便的语法来广播任何函数(点语法)。嵌套的“点调用”f.(...)(包括对 .+ 等的调用)自动融合 成单个 broadcast 调用。

此外,broadcast 不限于数组(参见函数文档);它还处理标量、元组和其他集合。默认情况下,只有某些参数类型被视为标量,包括(但不限于)NumberStringSymbolTypeFunction 和一些常见的单例,如 missingnothing。所有其他参数都会逐元素迭代或索引。

julia> convert.(Float32, [1, 2])
2-element Vector{Float32}:
 1.0
 2.0

julia> ceil.(UInt8, [1.2 3.4; 5.6 6.7])
2×2 Matrix{UInt8}:
 0x02  0x04
 0x06  0x07

julia> string.(1:3, ". ", ["First", "Second", "Third"])
3-element Vector{String}:
 "1. First"
 "2. Second"
 "3. Third"

有时,您希望一个容器(如数组)通常参与广播,但要“保护”它免受广播迭代其所有元素的行为的影响。通过将其放入另一个容器(如单个元素 Tuple)中,广播将将其视为单个值。

julia> ([1, 2, 3], [4, 5, 6]) .+ ([1, 2, 3],)
([2, 4, 6], [5, 7, 9])

julia> ([1, 2, 3], [4, 5, 6]) .+ tuple([1, 2, 3])
([2, 4, 6], [5, 7, 9])

实现

Julia 中的基本数组类型是抽象类型 AbstractArray{T,N}。它由维度数 N 和元素类型 T 参数化。 AbstractVectorAbstractMatrix 是 1 维和 2 维情况的别名。AbstractArray 对象的操作是使用更高级别的运算符和函数定义的,其方式独立于底层存储。这些操作通常作为任何特定数组实现的回退而正确地工作。

AbstractArray 类型包含任何类似数组的东西,并且它的实现可能与传统数组有很大不同。例如,元素可能是在请求时计算而不是存储。但是,任何具体的 AbstractArray{T,N} 类型通常都应该至少实现 size(A)(返回一个 Int 元组)、getindex(A,i)getindex(A,i1,...,iN);可变数组还应该实现 setindex!。建议这些操作具有几乎恒定的时间复杂度,否则某些数组函数可能会意外地变慢。具体类型通常还应该提供 similar(A,T=eltype(A),dims=size(A)) 方法,该方法用于为 copy 和其他非就地操作分配类似的数组。无论 AbstractArray{T,N} 如何在内部表示,T 都是由整数索引(A[1, ..., 1],当 A 不为空时)返回的对象类型,而 N 应该是由 size 返回的元组的长度。有关定义自定义 AbstractArray 实现的更多详细信息,请参阅接口章节中的数组接口指南

DenseArrayAbstractArray 的一个抽象子类型,旨在包含所有元素以列主序连续存储的数组(请参阅性能提示中的其他说明)。Array 类型是 DenseArray 的一个特定实例;VectorMatrix 是 1 维和 2 维情况的别名。除了所有 AbstractArray 所需的操作之外,很少有操作专门为 Array 实现;数组库的大部分都是以通用方式实现的,允许所有自定义数组的行为类似。

SubArrayAbstractArray 的一个专门化,它通过与原始数组共享内存而不是复制它来执行索引。SubArray 是使用 view 函数创建的,该函数的调用方式与 getindex 相同(使用数组和一系列索引参数)。view 的结果看起来与 getindex 的结果相同,只是数据保留在原位。view 将输入索引向量存储在 SubArray 对象中,该对象稍后可用于间接索引原始数组。通过将 @views 宏放在表达式或代码块的前面,该表达式中的任何 array[...] 切片都将转换为创建 SubArray 视图。

BitArray 是空间效率高的“打包”布尔数组,每个布尔值存储一位。它们的使用方式类似于 Array{Bool} 数组(每个布尔值存储一个字节),并且可以通过 Array(bitarray)BitArray(array) 分别相互转换。

如果数组在内存中以定义良好的间距(步长)存储其元素,则该数组为“带步长的”。具有受支持元素类型的带步长数组可以通过简单地传递其 pointer 和每个维度的步长来传递到外部(非 Julia)库(如 BLAS 或 LAPACK)。stride(A, d) 是沿维度 d 的元素之间的距离。例如,由 rand(5,7,2) 返回的内置 Array 的元素按列主序连续排列。这意味着第一维的步长——同一列中元素之间的间距——是 1

julia> A = rand(5, 7, 2);

julia> stride(A, 1)
1

第二维的步长是同一行中元素之间的间距,跳过与一列中元素一样多的元素(5)。类似地,在两个“页面”(在第三维中)之间跳转需要跳过 5*7 == 35 个元素。strides 是这三个数字的元组。

julia> strides(A)
(1, 5, 35)

在这种特定情况下,内存中跳过的元素数与跳过的线性索引数相匹配。这仅适用于像 Array(和其他 DenseArray 子类型)这样的连续数组,并且通常并非如此。具有范围索引的视图是非连续带步长数组的一个很好的例子;考虑 V = @view A[1:3:4, 2:2:6, 2:-1:1]。此视图 V 指向与 A 相同的内存,但跳过并重新排列其中一些元素。V 的第一维的步长为 3,因为我们只从原始数组中选择每隔三个行。

julia> V = @view A[1:3:4, 2:2:6, 2:-1:1];

julia> stride(V, 1)
3

此视图也类似地从原始 A 中选择每隔一列——因此,在第二维的索引之间移动时,它需要跳过相当于两列五元素列。

julia> stride(V, 2)
10

第三维很有趣,因为它的顺序是反转的!因此,要从第一个“页面”到第二个“页面”,它必须在内存中向后移动,因此它在此维度的步长为负!

julia> stride(V, 3)
-35

这意味着 Vpointer 实际上指向 A 的内存块的中间,并且它引用内存中向后和向前的元素。有关定义自己的带步长数组的更多详细信息,请参阅带步长数组的接口指南StridedVectorStridedMatrix 是许多被视为带步长数组的内置数组类型的便捷别名,允许它们分派以选择专门的实现,这些实现仅使用指针和步长即可调用高度调整和优化的 BLAS 和 LAPACK 函数。

值得强调的是,步长与内存中的偏移量有关,而不是与索引有关。如果您希望在线性(单索引)索引和笛卡尔(多索引)索引之间进行转换,请参阅 LinearIndicesCartesianIndices

  • 1iid,独立同分布。