贡献者: 待更新
本文授权转载自郝林的 《Julia 编程基础》。原文链接:第 10 章 容器:数组(下)。
我们在上一章其实已经学会了怎样利用字面量和专用符号拼接出一个数组,比如 [[1;2] [3;4] [5;6]]
或 [[1 2]; [3 4]; [5 6]]
。接下来我们会关注,怎样通过调用函数把多个数组拼接在一起。
为了避免混乱,我们先把前面定义的那 4 个一维数组还原成初始值:
julia> a1 = [1, 3, 5]; a2 = [2, 4, 6]; a3 = [7, 9, 11]; a4 = [8, 10, 12];
julia>
还记得吗?Julia 中的一维数组都是列向量。而且,我们可以使用英文分号纵向地拼接数组,并使用空格横向地拼接数组。所以,我们也可以把纵向的拼接叫做在第一个维度上的拼接,并把横向的拼接叫做在第二个维度上的拼接。下面的例子看起来会更加的形象:
julia> [a1; a2]
6-element Array{Int64,1}:
1
3
5
2
4
6
julia> [a1 a2]
3×2 Array{Int64,2}:
1 2
3 4
5 6
julia>
这两种拼接实际上都可以通过调用 cat
函数来实现。我们需要做的就是,把要拼接的多个数组都传给这个函数,同时确定其关键字参数 dims
的值。这个关键字参数代表的就是,将要在哪一个维度上进行拼接。而且,它还是一个必选的参数。我们下面就用这个函数来实现前面的那两个拼接操作:
julia> cat(a1, a2, dims=1)
6-element Array{Int64,1}:
1
3
5
2
4
6
julia> cat(a1, a2, dims=2)
3×2 Array{Int64,2}:
1 2
3 4
5 6
julia>
这很容易不是吗?不过,如果只是这样的话,我想这个 cat
函数就没有太大的存在意义了。实际上,我们还可以为它的 dims
参数赋予更大的正整数。例如:
julia> cat(a1, a2, dims=3)
3×1×2 Array{Int64,3}:
[:, :, 1] =
1
3
5
[:, :, 2] =
2
4
6
julia> cat(a1, a2, dims=4)
3×1×1×2 Array{Int64,4}:
[:, :, 1, 1] =
1
3
5
[:, :, 1, 2] =
2
4
6
julia>
如上所示。当 dims=3
时,cat
函数会把 a1
和 a2
分别作为两个二维数组的唯一组成部分,然后再用这两个二维数组合成一个三维数组。从而,这个三维数组的尺寸就是 3×1×2
。更具体地说,之所以它在第一个维度上的长度是 3
,是因为 a1
和 a2
的长度都是 3
。它在第二个维度上的长度是 1
,是因为 a1
和 a2
都是一维的数组,并且它们分别是那两个二维数组中唯一的低维数组。至于第三个维度上的长度为 2
的根本原因是,我们用来做拼接的数组有两个。
当 dims=4
时,cat
函数依然会把 a1
和 a2
分别作为两个二维数组的唯一组成部分。并且,它还会把这两个二维数组分别作为两个三维数组的唯一组成部分。最后,它会再用这两个三维数组合成一个四维数组。所以,这个四维数组的尺寸才会是 3×1×1×2
。至于更多的细节,我想你应该已经可以参照前面的描述自行解释了。
我们再来一起拼接一个更加复杂的数组。首先,我们要合成两个二维数组,如下:
julia> a13 = cat(a1, a3, dims=2)
3×2 Array{Int64,2}:
1 7
3 9
5 11
julia> a24 = cat(a2, a4, dims=2)
3×2 Array{Int64,2}:
2 8
4 10
6 12
julia>
这两个二维数组的尺寸都是 3×2
,并且它们的元素值也都很有特点。现在,我们要把它们拼接成一个四维数组:
julia> cat(a13, a24, dims=4)
3×2×1×2 Array{Int64,4}:
[:, :, 1, 1] =
1 7
3 9
5 11
[:, :, 1, 2] =
2 8
4 10
6 12
julia>
这个四维数组的尺寸是 3×2×1×2
。与前面那两个 3×2
的数组比对一下,你是不是已经看出一些规律了呢?没错,它们在前两个维度上的尺寸完全相同。并且,最后一个维度上的长度完全取决于我们用来做拼接的数组的个数。至于第三个维度上的 1
,是因为我们拿来做拼接的是二维数组,它们都没有第三个维度。
一旦理解了这些,我们就可以使用一些更加便捷的函数来做拼接了。比如,vcat
函数仅用于纵向的拼接,我们只把要拼接的数组传给它就可以了:
julia> vcat(a1, a2)
6-element Array{Int64,1}:
1
3
5
2
4
6
julia>
又比如,hcat
函数仅用于横向的拼接,它的用法与 vcat
函数一样:
julia> hcat(a1, a2)
3×2 Array{Int64,2}:
1 2
3 4
5 6
julia>
另外,我们还要详细地说一下 hvcat
这个函数。它可以同时进行纵向和横向的拼接。在使用的时候,我们先要确定拼接后的数组的尺寸,然后再传入用于拼接的值。先看一个最简单的例子:
julia> hvcat(3, a1...)
1×3 Array{Int64,2}:
1 3 5
julia>
如果我们把拼接后数组的尺寸确定为一个正整数,那么就相当于在确定新数组的列数。至于新数组会有多少行,那就要看用于拼接的值有多少个了。正如上例所示,我们传给 hvcat
函数的第一个参数值 3
代表着新数组会有 3 列。如此一来,这个函数就会依次地把 a1
中的各个元素值分别分配给新数组中的某一列。
注意,我们在这里传给 hvcat
函数的第二个参数值是 a1...
,而不是 a1
。这是为什么呢?
还记得吗?符号 ...
的作用是,把紧挨在它左边的那个值中的所有元素值都平铺开来,让它们都成为独立的参数值。所以,上述调用就相当于 hvcat(3, 1, 3, 5)
。如果我们把 ...
从中去掉,那么 Julia 就会立即报错:
julia> hvcat(3, a1)
ERROR: ArgumentError: number of arrays 1
is not a multiple of the requested number of block columns 3
# 省略了一些回显的内容。
julia>
这条错误信息的大意是:既然确定了新数组会有 3 列,那么后面提供的参数值的个数就应该是 3 的倍数。否则,这个函数就无法均匀地把参数值分配到各个列上。由于我们在后面只提供了一个参数值 a1
,所以就引发了这个错误。
在第一个参数值是 3
的情况下,如果后续参数值的数量正好是 3,那么 hvcat
函数就会生成一个 1×3
的二维数组。如果这个数量是 6,那么它就会生成一个 2×3
的二维数组。以此类推。
原则上,除了第一个参数,我们可以把任意的值作为参数值传给 hvcat
函数。但如果这些参数值都是一维数组,那么该函数就会识别出它们,并依次地把它们(包含的所有元素值)分别分配给新数组中的某一列。例如:
julia> hvcat(3, a1, a2, a3)
3×3 Array{Int64,2}:
1 2 7
3 4 9
5 6 11
julia>
此时,新数组的行数就取决于后续参数值的长度。注意,这些后续的参数值的长度必须是相同的。
下面我们考虑第一个参数值不是正整数的情况。先来看一个很近似的例子:
julia> hvcat((3), a1, a2, a3)
3×3 Array{Int64,2}:
1 2 7
3 4 9
5 6 11
julia>
这次我们传给 hvcat
函数的第一个参数值是一个元组,并且其中只包含了一个元素值 3
。这个调用表达式的求值结果与前一个例子的结果完全相同。
你可能已经猜到,这个元组里的 3
同样代表了新数组的列数。不过,它还有另外一个含义,即:新数组进行第一轮分配时所需的后续参数值的数量。如果还有第二轮分配的话,那么就可以是下面这样:
julia> hvcat((3,3), a1, a2, a3, a1, a2, a3)
6×3 Array{Int64,2}:
1 2 7
3 4 9
5 6 11
1 2 7
3 4 9
5 6 11
julia> hvcat(3, a1, a2, a3, a1, a2, a3)
6×3 Array{Int64,2}:
1 2 7
3 4 9
5 6 11
1 2 7
3 4 9
5 6 11
julia>
请注意,在此例中,上面的调用表达式中的第一个参数值是 (3,3)
,而下面的第一个参数值是 3
。它们起到的作用是一样的。
可以看到,在这两个调用表达式中,hvcat
函数都会先依次地把第 1、2、3 个一维数组中的所有元素值分别分配给新数组的第 1、2、3 列。这对应于求值结果中的前三行。然后,它会再把第 4、5、6 个一维数组中的所有元素值分别分配给新数组的那三列。这对应于求值结果中的后三行。此时,新数组的行数就等于这些一维数组的长度的 2 倍。显然,这些一维数组的长度也必须是相同的。
现在我们知道了,参数值 (3,3)
中的第二个 3
的含义是:新数组进行第二轮分配时所需的后续参数值的数量。实际上,作为传递给 hvcat
函数的第一个参数值,元组中的每一个正整数都必须是相同的。如果出现了像 (3,2)
、(3,3,2)
这样的参数值,那么 hvcat
函数就会立即报错。
那为什么还要向 hvcat
函数传入元组呢?我们直接传入一个正整数不就好了吗?
与只传入一个正整数相比,传入一个元组有以下两点不同:
1. 传入元组可以确切地告诉 hvcat
函数需要为新数组分配几轮元素值。而如果只传入一个正整数,我们可能还需要计算一下才能得知真正的分配轮数。因为后续参数值的数量可以是这个正整数的任意倍数。比如,假设后续的参数值是 a5...
,而我们并不知道 a5
里有多少个元素值,所以就无法预料到分配的轮数。
2. 一旦分配的轮数由元组确定下来,后续参数值的数量就必须大于 “<元组中的首个元素值> x <元组中元素值的数量>
”,否则 hvcat
函数就会报错。而如果只传入一个正整数,那么只要后续参数值的实际数量不是这个正整数的倍数就会引发错误。显然,传入元组可以放宽对后续参数值的约束。
因此,这里就存在一个选择的问题。我们需要根据实际情况选择 “灵活” 或者 “严谨”。当你在编写一个供他人使用的程序的时候,这种选择尤为重要。不过,这种选择在很多时候并不意味着非 0
即 1
。
言归正传。虽然 hvcat
函数在二维数组的拼接方面很强大,但是它与 vcat
和 hcat
一样,都无法拼接出维数更多的数组。为了满足这样的需求,我们只能使用 cat
函数。当然,若我们要拼接很复杂的数组,则可以把这些函数组合起来使用。我更加推荐这种使用方式。因为这样做可以使程序的可读性更好,也更不容易出错,另外在程序的性能方面往往也不会有什么损失。