10.4 数组的拼接

我们在上一章其实已经学会了怎样利用字面量和专用符号拼接出一个数组,比如[[1;2] [3;4] [5;6]][[1 2]; [3 4]; [5 6]]。接下来我们会关注,怎样通过调用函数把多个数组拼接在一起。

为了避免混乱,我们先把前面定义的那 4 个一维数组还原成初始值:

  1. julia> a1 = [1, 3, 5]; a2 = [2, 4, 6]; a3 = [7, 9, 11]; a4 = [8, 10, 12];
  2. julia>

还记得吗?Julia 中的一维数组都是列向量。而且,我们可以使用英文分号纵向地拼接数组,并使用空格横向地拼接数组。所以,我们也可以把纵向的拼接叫做在第一个维度上的拼接,并把横向的拼接叫做在第二个维度上的拼接。下面的例子看起来会更加的形象:

  1. julia> [a1; a2]
  2. 6-element Array{Int64,1}:
  3. 1
  4. 3
  5. 5
  6. 2
  7. 4
  8. 6
  9. julia> [a1 a2]
  10. 3×2 Array{Int64,2}:
  11. 1 2
  12. 3 4
  13. 5 6
  14. julia>

这两种拼接实际上都可以通过调用cat函数来实现。我们需要做的就是,把要拼接的多个数组都传给这个函数,同时确定其关键字参数dims的值。这个关键字参数代表的就是,将要在哪一个维度上进行拼接。而且,它还是一个必选的参数。我们下面就用这个函数来实现前面的那两个拼接操作:

  1. julia> cat(a1, a2, dims=1)
  2. 6-element Array{Int64,1}:
  3. 1
  4. 3
  5. 5
  6. 2
  7. 4
  8. 6
  9. julia> cat(a1, a2, dims=2)
  10. 3×2 Array{Int64,2}:
  11. 1 2
  12. 3 4
  13. 5 6
  14. julia>

这很容易不是吗?不过,如果只是这样的话,我想这个cat函数就没有太大的存在意义了。实际上,我们还可以为它的dims参数赋予更大的正整数。例如:

  1. julia> cat(a1, a2, dims=3)
  2. 3×1×2 Array{Int64,3}:
  3. [:, :, 1] =
  4. 1
  5. 3
  6. 5
  7. [:, :, 2] =
  8. 2
  9. 4
  10. 6
  11. julia> cat(a1, a2, dims=4)
  12. 3×1×1×2 Array{Int64,4}:
  13. [:, :, 1, 1] =
  14. 1
  15. 3
  16. 5
  17. [:, :, 1, 2] =
  18. 2
  19. 4
  20. 6
  21. julia>

如上所示。当dims=3时,cat函数会把a1a2分别作为两个二维数组的唯一组成部分,然后再用这两个二维数组合成一个三维数组。从而,这个三维数组的尺寸就是3×1×2。更具体地说,之所以它在第一个维度上的长度是3,是因为a1a2的长度都是3。它在第二个维度上的长度是1,是因为a1a2都是一维的数组,并且它们分别是那两个二维数组中唯一的低维数组。至于第三个维度上的长度为2的根本原因是,我们用来做拼接的数组有两个。

dims=4时,cat函数依然会把a1a2分别作为两个二维数组的唯一组成部分。并且,它还会把这两个二维数组分别作为两个三维数组的唯一组成部分。最后,它会再用这两个三维数组合成一个四维数组。所以,这个四维数组的尺寸才会是3×1×1×2。至于更多的细节,我想你应该已经可以参照前面的描述自行解释了。

我们再来一起拼接一个更加复杂的数组。首先,我们要合成两个二维数组,如下:

  1. julia> a13 = cat(a1, a3, dims=2)
  2. 3×2 Array{Int64,2}:
  3. 1 7
  4. 3 9
  5. 5 11
  6. julia> a24 = cat(a2, a4, dims=2)
  7. 3×2 Array{Int64,2}:
  8. 2 8
  9. 4 10
  10. 6 12
  11. julia>

这两个二维数组的尺寸都是3×2,并且它们的元素值也都很有特点。现在,我们要把它们拼接成一个四维数组:

  1. julia> cat(a13, a24, dims=4)
  2. 3×2×1×2 Array{Int64,4}:
  3. [:, :, 1, 1] =
  4. 1 7
  5. 3 9
  6. 5 11
  7. [:, :, 1, 2] =
  8. 2 8
  9. 4 10
  10. 6 12
  11. julia>

这个四维数组的尺寸是‌3×2×1×2。与前面那两个3×2的数组比对一下,你是不是已经看出一些规律了呢?没错,它们在前两个维度上的尺寸完全相同。并且,最后一个维度上的长度完全取决于我们用来做拼接的数组的个数。至于第三个维度上的1,是因为我们拿来做拼接的是二维数组,它们都没有第三个维度。

一旦理解了这些,我们就可以使用一些更加便捷的函数来做拼接了。比如,vcat函数仅用于纵向的拼接,我们只把要拼接的数组传给它就可以了:

  1. julia> vcat(a1, a2)
  2. 6-element Array{Int64,1}:
  3. 1
  4. 3
  5. 5
  6. 2
  7. 4
  8. 6
  9. julia>

又比如,hcat函数仅用于横向的拼接,它的用法与vcat函数一样:

  1. julia> hcat(a1, a2)
  2. 3×2 Array{Int64,2}:
  3. 1 2
  4. 3 4
  5. 5 6
  6. julia>

另外,我们还要详细地说一下hvcat这个函数。它可以同时进行纵向和横向的拼接。在使用的时候,我们先要确定拼接后的数组的尺寸,然后再传入用于拼接的值。先看一个最简单的例子:

  1. julia> hvcat(3, a1...)
  2. 1×3 Array{Int64,2}:
  3. 1 3 5
  4. julia>

如果我们把拼接后数组的尺寸确定为一个正整数,那么就相当于在确定新数组的列数。至于新数组会有多少行,那就要看用于拼接的值有多少个了。正如上例所示,我们传给hvcat函数的第一个参数值3代表着新数组会有 3 列。如此一来,这个函数就会依次地把a1中的各个元素值分别分配给新数组中的某一列。

注意,我们在这里传给hvcat函数的第二个参数值是a1...,而不是a1。这是为什么呢?

还记得吗?符号...的作用是,把紧挨在它左边的那个值中的所有元素值都平铺开来,让它们都成为独立的参数值。所以,上述调用就相当于hvcat(3, 1, 3, 5)。如果我们把...从中去掉,那么 Julia 就会立即报错:

  1. julia> hvcat(3, a1)
  2. ERROR: ArgumentError: number of arrays 1 is not a multiple of the requested number of block columns 3
  3. # 省略了一些回显的内容。
  4. julia>

这条错误信息的大意是:既然确定了新数组会有 3 列,那么后面提供的参数值的个数就应该是 3 的倍数。否则,这个函数就无法均匀地把参数值分配到各个列上。由于我们在后面只提供了一个参数值a1,所以就引发了这个错误。

在第一个参数值是3的情况下,如果后续参数值的数量正好是 3,那么hvcat函数就会生成一个1×3的二维数组。如果这个数量是 6,那么它就会生成一个2×3的二维数组。以此类推。

原则上,除了第一个参数,我们可以把任意的值作为参数值传给hvcat函数。但如果这些参数值都是一维数组,那么该函数就会识别出它们,并依次地把它们(包含的所有元素值)分别分配给新数组中的某一列。例如:

  1. julia> hvcat(3, a1, a2, a3)
  2. 3×3 Array{Int64,2}:
  3. 1 2 7
  4. 3 4 9
  5. 5 6 11
  6. julia>

此时,新数组的行数就取决于后续参数值的长度。注意,这些后续的参数值的长度必须是相同的。

下面我们考虑第一个参数值不是正整数的情况。先来看一个很近似的例子:

  1. julia> hvcat((3), a1, a2, a3)
  2. 3×3 Array{Int64,2}:
  3. 1 2 7
  4. 3 4 9
  5. 5 6 11
  6. julia>

这次我们传给hvcat函数的第一个参数值是一个元组,并且其中只包含了一个元素值3。这个调用表达式的求值结果与前一个例子的结果完全相同。

你可能已经猜到,这个元组里的3同样代表了新数组的列数。不过,它还有另外一个含义,即:新数组进行第一轮分配时所需的后续参数值的数量。如果还有第二轮分配的话,那么就可以是下面这样:

  1. julia> hvcat((3,3), a1, a2, a3, a1, a2, a3)
  2. 6×3 Array{Int64,2}:
  3. 1 2 7
  4. 3 4 9
  5. 5 6 11
  6. 1 2 7
  7. 3 4 9
  8. 5 6 11
  9. julia> hvcat(3, a1, a2, a3, a1, a2, a3)
  10. 6×3 Array{Int64,2}:
  11. 1 2 7
  12. 3 4 9
  13. 5 6 11
  14. 1 2 7
  15. 3 4 9
  16. 5 6 11
  17. 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函数就会报错。而如果只传入一个正整数,那么只要后续参数值的实际数量不是这个正整数的倍数就会引发错误。显然,传入元组可以放宽对后续参数值的约束。

因此,这里就存在一个选择的问题。我们需要根据实际情况选择“灵活”或者“严谨”。当你在编写一个供他人使用的程序的时候,这种选择尤为重要。不过,这种选择在很多时候并不意味着非01

言归正传。虽然hvcat函数在二维数组的拼接方面很强大,但是它与vcathcat一样,都无法拼接出维数更多的数组。为了满足这样的需求,我们只能使用cat函数。当然,若我们要拼接很复杂的数组,则可以把这些函数组合起来使用。我更加推荐这种使用方式。因为这样做可以使程序的可读性更好,也更不容易出错,另外在程序的性能方面往往也不会有什么损失。