3.4 增加或删除矩阵的行或列
严格来说,矩阵的长度和维度是固定的,因此不能增加或删除行或列。但是可以给矩阵重新赋值,这样可以得到和增加或删除一样的效果。
3.4.1 改变矩阵的大小
回忆之前通过重新赋值改变向量大小的方法:
第一个例子里,x原来长度为5,通过拼接和重新赋值,将其长度变为6。事实上我们没有改变x的长度,而是生成一个新的向量,然后赋值给x。
注意 重新赋值的过程可能会在用户看不见的情况下进行,在14章我们将会介绍。例如,即使是x[2]<-12这种小操作事实上都是一个重新赋值的过程。
类似的操作可以用来改变矩阵的大小。例如,函数rbind()(代表row bind,按行组合)和函数cbind()(代表column bind,按列组合)可以给矩阵增加行或列。
这里,cbind()把一列由1组成的向量和z组合在一起,创建了一个新矩阵。上面我们只是直接输出了结果,实际上也可以把这个新的矩阵赋值给z(或其他变量),如下所示:
不过,请谨慎使用cbind()!和创建向量一样,创建一个新的矩阵是很耗时间的(毕竟矩阵也属于向量)。在下面的代码中,cbind()创建了一个新矩阵:
新的矩阵正好被赋值给z,也就是说我们给这个新矩阵取名为z,与原来的矩阵同名,而原来的矩阵被覆盖了)。问题是创建新矩阵会减低程序速度,如果在循环中重复创建矩阵,将浪费大量的时间。
因此在循环中每次往矩阵中添加一行(列),最后矩阵会变成一个大矩阵,这种做法是不可取的,最好一开始就定义好一个大矩阵。这个事先定义的矩阵是空的,但是在循环过程中逐行或列进行赋值,这种做法避免了循环过程中每次进行耗时的矩阵内存分配。
也可以通过重新赋值来删除矩阵的行或列:
3.4.2 扩展案例:找到图中距离最近的一对端点
计算图中多个端点之间距离是计算机或统计学中常见的例子。这类问题在聚类算法和基因问题中经常出现。
我们以计算城市之间的距离为例,这比计算DNA链间距离更直观。
假设有一个距离矩阵,其第i行第j列的元素代表城市i和城市j间的距离。我们需要写一个函数,输入城市距离矩阵,输出城市间最短的距离,以及对应的两个城市。代码如下:
以下是一个调用该函数的例子:
最小值是6,位于在第3行第4列。可以看到apply()函数在这里起重要作用。
我们的任务很简单:找到矩阵中最小的非零元素。首先找到每行中的最小值—仅一个命令apply()即可对所有行达到此目的—然后找到这些最小值中最小的那一个。但如你所见,这段代码的逻辑还是略微有些复杂。
注意一个很关键的地方,这个矩阵是对称的,因为城市i到城市j的距离与从城市j到城市i的距离相等。因此在找第i行最小值时,只需要在第i+1,i+2,……n等元素中搜索(n是矩阵的总列数)。因此在调用apply()时,矩阵d的最后一行是可以跳过不用管的。
由于矩阵可能很大,如果有1000个城市的话,那么矩阵就有100万个元素。所以我们要利用矩阵的对称性来节省时间。但是这样也带来一个问题:在计算每行最小值时,我们需要知道该行在原矩阵中的行号, 而apply()函数不能直接提供给它所调用的函数。所以在代码的第6行,我们给原矩阵增加了一列,为相应的行号,目的是让apply()函数所调用的函数能够识别出行号。
apply()所调用的函数是imin()函数,它的定义开始于代码第15行,该函数寻找形式参数x所代表的这一行的最小元素及其所在位置的索引。例如,对例子中矩阵q的第一行调用函数imin(),可求出最小值是8,出现在该行第4列的位置。为求得最小元素的位置,第18行使用了函数which.min(),这是R语言中非常方便的函数。
请注意第19行。之前我们利用矩阵的对称性,在寻找最小值的时候跳过了每行的前半部分,这一点可从第18行表达式中的(i+1):(lx-1)看出。但是这也意味着which.min()返回的最小值的位置索引是相对于范围(i+1): (lx-1)的。例如在q的第三行,尽管第4个元素最小,但是which.min()返回的是1。因此我们需要在which.min()的结果上增加i,如第19行所示。
最后,要正确使用apply()的输出结果需要费点儿功夫。对于上面例子中的矩阵q,apply()函数将会返回一个矩阵wmins:
这个矩阵的第二行包含d矩阵的上三角部分各行最小值,第一行则是这些值的索引。例如wmins的第一列表明,q的第一行中最小值是8,位于第4列。
第9行代码选出整个矩阵最小值所在的行号i,在矩阵q的例子中结果为6。第10行则求出该行最小值在第j列,在矩阵q的例子中结果为4。就是说整个矩阵的最小值在第i行第j列,这个信息在11行将会被用到。
同时,apply()输出结果的第一行是各行最小值所在的列。这样我们就可以找出距离最近的城市分别是什么了。我们已经知道城市3是两个城市之一,而wmins第一行第三列对应的是4,因此城市3和城市4是距离最近的,程序第9行和第10行表示了这一推理过程。
如果该矩阵中最小值是唯一的,那么就有更简单的方法:
这段代码直接找到d的最小元素的所在位置。其中参数arr.ind=TRUE指定,which()函数返回的坐标必须是矩阵下标—也就是行号和列号,而不是一个单一的向量索引。如果没有这个参数,d就会被当作向量来处理。
前面提到过,这段新代码只有当最小值唯一时才有用。如果此条件不成立,which()函数会返回多个行/列数对,与我们的目标相悖。如果我们用原来那版代码,当d有多个最小值时,只会返回其中一个。
另外一个问题是效率。新的代码实质上包含两个(隐性的)循环:一个是计算最小值smallest,另一个是调用which()。因此新的代码会比原来那版更慢。
在这两种方法中,如果特别关注运行速度,并且可能有多个最小值,那么最好选择原来那版代码,否则就选用另一个。而后者的简洁性使之更容易阅读和维护。