<-- Home |--matlab

Indexing_in_Matlab中的数组索引

数组相关函数

Matlab中,最基础的数据类型,其实都可以作为数组存在。这是Matlab的名字和实力。

前面我们已经介绍了关于向量数组的一些基本约定,已经涉及了一些数组创建和操作的规则。

函数作用备注
size返回数组的大小
length返回数组的最大维度
ndims返回数组的维度数
numel返回数组的元素个数
zeros创建全零数组
ones创建全一数组
rand创建随机数组
randn创建服从正态分布的随机数组
linspace创建等差数列
logspace创建等比数列

特别还有操作符:,用于生成等差数列,如1:2:10,产生行向量[1,3,5,7,9]

还有一些没有介绍的创建函数,也很常用。

函数作用备注
eye创建单位矩阵
magic创建魔方阵
true创建逻辑1
false创建逻辑0

还有几个函数,是提取部分元素或者组合元素形成新的数组。

函数作用备注
blkdiag创建对角矩阵
diag提取对角线元素
tril提取下三角矩阵
triu提取上三角矩阵
flip翻转数组
fliplr水平翻转数组
flipud垂直翻转数组
cat拼接数组
repmat复制数组
horzcat水平拼接数组
vertcat垂直拼接数组
reshape重塑数组
squeeze去除维度为1的维度
repelem重复数组元素

这里不对这些命令进行详细介绍,需要查看帮助的,请在Matlab命令行中输入help命令,如help eyehelp magic等。要查看详细文档,可以在Matlab命令行中输入doc命令,如doc eyedoc magic等。

这里对数组元素的访问,也称为数组索引,进行一个专门的介绍。

数组索引为什么要用括号

很多程序员和初学者在使用Matlab时,会有一个疑问:为什么要用括号来索引数组元素,而不是用方括号?毕竟在C、Python等语言中,我们都是用方括号来索引数组元素的。所以在第一门课程里面都是按方括号来来学习的,这是第一印象。

我在仔细调研这个问题之前就形成了一个观点:那就是Matlab把数组(矩阵)假装成一个对象,这个对象自己是一个函数,索引就是这个函数的意义。

下面,请听我的狡辩。

在Matlab中,函数的调用是用括号的,如sin(x)size(A)plot(x,y)等等。这是Matlab的基本约定,括号是函数调用的标志。那么索引数组的元素呢?也是用括号,如A(1,2)A(1:3)A(1:2:end)等等。是不是很相似?

其次,等后面我会专门写一个函数的约定,里面会讲到,Matlab里面有一个数据构造叫做cell,跟别的语言的元组非常类似,实际上Matlab函数的输入参数和输出值都是cell

比如有个函数size,他可以返回数组的大小,或者返回数组的特定维的大小。

1A = magic(3); % 3 x 3
2sz = size(A); % [3,3]
3sz1 = size(A,1); % 3
4
5args = {A, 1};
6sz2 = size(args{:}); % 3

我们再来看高维数组的索引,看看是不是有什么很一样的地方。

1A = magic(3); % 3 x 3
2
3A(1,2) % 索引单个元素
4
5
6A(2:3,1:2) % 索引子矩阵
7
8A([1,2], [1, 3]) % 索引特定元素

那么你们猜猜,是否能够采用上面的cell的方式来索引数组呢?

 1A = magic(3); % 3 x 3
 2
 3idx = {1,2};
 4A(idx{:}) % 索引单个元素
 5
 6idx = {2:3,1:2};
 7A(idx{:}) % 索引子矩阵
 8
 9idx = {[1,2], [1, 3]};
10A(idx{:}) % 索引特定元素

很可能你们会发现,这个cell的方式和数组索引的方式是一样的。

一个东西,它看起来像函数,走起来像函数,叫起来也像函数,那么它就(可能)是函数。

以上,就是我的狡辩。实际上,只要接受了这个设定,你会发现Matlab的数组索引是非常方便的,而且非常强大。

数组索引的种类

Matlab中的数组索引可以按两种方式进行:

  1. 位置索引
  2. 逻辑索引

位置索引很简单,就是通过前面给出的cell的方式,指定数组的位置,如A(1,2)A(1:3)A(1:2:end)等等,你就想象成函数调用,按照低维至高维来制定下标范围,每个维度的下标范围必须是一个整数、整数列表、colon列表(:)。这个下标范围就是位置索引,每个索引的值都应该在$[1, \text{size}(A,i)]$之间(请思考这句话是不是一定对。)。值得注意的是,这个位置索引的数量可以超过维数(ndims),但是超过维数的数值只能是1。(请思考为什么?

还有一个可以注意的事情是,可以降维索引,也就是不给足ndims个下标,此时,最后一个下标就会成为当前维到最高的非1的维的乘积。这个特性在后面的还会专门提到。

逻辑索引也非常简单,逻辑索引就是调用这个数组函数时,给的参数不是整数下标,而是逻辑值或者逻辑值组成的数组,这个数组索引的所有参数构成的cell元组给定的位置,true对应选中,false对应不选中。

看看上面的分析就知道,这两个索引概念跟降维索引交叉起来会形成四种索引方式:

  1. 线性(降维)位置索引
  2. 线性(降维)逻辑索引
  3. 多维位置索引
  4. 多维逻辑索引

值得注意的是,帮助文档只给出线性位置索引和多维逻辑索引的例子,但是实际上上面这几种复杂的情况都是存在的、合法的索引方式(数组函数调用方式)。

那么?我们提到的前面的一个左值和右值个数numel必须相等的约定,以及左值设定右值的对应关系应该如何取得呢?(请思考什么是对应关系

1A = B
2% numel(A) == numel(B)
3% 请思考如何赋值?

Matlab必须以某种具备一致性的方式,访问A 和B的元素,考虑到上面我们已经搞出了这么多索引方式,这个对应关系应该如何取得呢?

数组索引的规则

对于A(idx)这个表达式,这里的idx可以是任何合法的索引方式,那么这个表达式的值应该是什么呢?

这个问题的答案非常简单,我们已知的信息

  1. size(A)
  2. idx

那么我们选择一个什么样的方式最为合情合理呢?有没有一种索引是很容易在字面上是唯一的呢?这个问题随便拍脑袋就知道,那就是:

11:numel(A)

这个列向量及其中的部分元素构成的数组,可以唯一的确定A中元素的子集。按照A(:)这个方式,任何N维数组都可以被展开成一个列向量,这个列向量的元素就是原数组的元素,这个列向量的索引就是原数组的索引。

Matlab提供了一组函数,用于把任何一种索引方式转换成线性位置索引,这个函数就是sub2ind,这个函数的调用方式是:

1ind = sub2ind(size(A), idx1, idx2, ..., idxN)

这跟下面的调用是一模一样的,我们在函数的讲解中会详细介绍。

1ind = sub2ind(size(A), idx{:})

这里的idx1, idx2, ..., idxN就是idx的元素,可以是上面若干中索引的组合,当然必须确保idxsize(A)的范围是合法的(包括降维后的合法性)。

有了这个函数之后,就可以尽情地发掘Matlab的索引方式了。最终访问数字的元素,都是依靠这个函数返回值的唯一性来保证,通过这个函数(类似的逻辑),也能够保证左值和右值的对应关系。

相应地,还有一个作用相反地函数ind2sub,这个函数的调用方式是:

1[idx1, idx2, ..., idxN] = ind2sub(size(A), ind)

这里左值的个数自然数个,当个数少于ndims时,最后一个下标就会成为当前维索引与所有高维维数的乘积(请思考为什么);当个数多于ndims时,多出来的下标全部都是1(这在语义上是合理的,因为这些下标对应的维度是1)。

这里举一个例子。

 1A = rand(3, 4, 5); % 3 x 4 x 5
 2s = size(A); % [3, 4, 5]
 3
 4ind = sub2ind(s, 2, 3, 4); % 44  如何计算得到这个值?
 5
 6[i1, j1, k1] = ind2sub(s, ind); % 2, 3, 4
 7
 8[i2, j2] = ind2sub(s, ind); % 2, 15
 9
10[i3] = ind2sub(s, ind); % 44

这里的ind是如何计算得到的呢?这个问题是一个很好的思考题,可以帮助你理解sub2ind函数的作用。

$$ \text{ind} = (4-1)\times 3 \times 4 + (3-1) \times 3 + 2 = 44 $$

我们的计算有两个要点:

  1. 从高维开始算,每个维度的下标都是从1开始的,所以要减去1;
  2. 不足一个高维的,递归计算下去。

此外ind2sub中下标可以是一个向量,这样就可以一次性计算多个位置的下标。此时,每个位置的向量长度必须相等,否则会报错。

1A = rand(3, 4, 5); % 3 x 4 x 5
2s = size(A); % [3, 4, 5]
3
4ind = [2, 3, 4]; 
5
6[i1, j1, k1] = ind2sub(s, ind); % i1 = [2,3,1], j2=[1,1,2], k1=[1,1,1]
7
8
9ind = sub2ind(s, i1, j1, k1); % [2,3,4]

逻辑索引

逻辑索引一般而言,会采用一个跟被索引矩阵大小相同的逻辑矩阵,这个逻辑矩阵的元素值为true或者false,true对应的位置会被选中,false对应的位置会被忽略。

那么,跟上面的位置索引一样,如果逻辑索引降维了呢?如果逻辑索引的每个维长度小于被索引矩阵的对应维度呢?

 1A = rand(3, 4, 5); % 3 x 4 x 5
 2s = size(A); % [3, 4, 5]
 3
 4idx = [true, false, true]; % 1 x 2
 5
 6B = A(idx); % 1 x 2,对应的元素是A(1,1), A(1,3)
 7
 8
 9idx2 = {[true, false, true], [true, false, true]}; % 2 x 2
10
11C = A(idx2{:}) % 2 x 2,对应的元素是A(1,1), A(1,3), A(3,1), A(3,3)

这里降维和索引是怎么样的呢?这个问题是一个很好的思考题。

其实这个问题还可以变得更复杂:

1idx2 = {[true, false, true], [true, false, true, false, true, true]}; 
2
3A(idx2{:})

这样又会是什么结果呢?先让我们回到逻辑数组索引的基本概念和基本约定,本文最后,我们给出上面思考题的答案。

逻辑数组索引是一种非常灵活的索引方式,它的基本概念是用一个逻辑数组来选择数组中的元素。这个逻辑数组的长度必须和被索引的数组的长度相同(或者小于被索引数组),这个逻辑数组的元素是true或者falsetrue表示选择这个位置的元素,false表示不选择这个位置的元素。

我们先看基本的例子:

 1A = magic(3);
 2
 3B = magic(3)';
 4
 5inx = A < B;
 6
 7A(inx)
 8% ans = [4; 1; 7]
 9
10B(inx)
11% ans = [6; 3; 9]

这里通过逻辑操作符<得到了一个逻辑数组inx,然后通过逻辑数组索引,得到了AB中对应位置的元素。最终返回的结果是AB中对应位置的元素,并构成一个列向量。

这也符合前面所说的约定,计算得到的是列向量。

逻辑索引数组生成

说到这里,就必须要看看Matlab中的逻辑值到底是什么。Matlab的逻辑值是logical类型,它是一种特殊的数值类型,只有两个值:1和0,分别代表truefalse,它的大小就只有1Bytes。对应操作逻辑值的四个基本函数:

  • logical:将数值转换为逻辑值,跟输入矩阵相同的size
  • islogical:判断是否是逻辑值;
  • true:返回逻辑值true,可以是矩阵;
  • false:返回逻辑值false,可以是矩阵。

这几个函数的具体调用方式和作用这里不赘述,参考helpdoc即可。

通常,产生逻辑数组的方式有:

  • 逻辑操作符:<<=>>===~=
  • 逻辑计算符号:&|~
  • 逻辑函数:logicaltruefalse
  • 判断型的函数:isxxxxx,包括islogicalisnumericischar等等。
  • 自己定义返回逻辑值的函数。

在Matlab中,针对矩阵运算有一个特殊约定。

针对单个元素的计算可以扩展到矩阵的每个元素。

像上面的逻辑运算符,可以作用到单个元素,也可以作用到矩阵的每个元素。这个特性在逻辑数组索引中非常重要。例如,矩阵中所有大于5的元素:

1A = magic(3);
2% A = [8, 1, 6; 3, 5, 7; 4, 9, 2]
3
4inx = A > 5;
5% inx = [0, 0, 1; 1, 1, 0; 0, 1, 1]

比如自己定义返回逻辑值的函数:

1function y = isodd(x)
2    y = mod(x, 2) == 1;
3end

这样就可以得到一个逻辑数组:

1A = magic(3);
2
3inx = isodd(A);
4% inx = [1, 0, 1; 1, 0, 1; 0, 1, 0]

值得注意的是,矩阵的逻辑索引可以作为左值,也可以作为右值。这在Matlab里面是常规操作。

1A = magic(3);
2
3inx = A > 5;
4
5A(inx) = 0;
6A(~inx) = 1;
7
8A % [0, 1, 0; 1, 1, 0; 1, 0, 1]

逻辑索引的应用

当我们常规使用逻辑数组索引时,我们通常约定:

逻辑数组索引由被索引的数组计算得到,逻辑数组的大小和被索引的数组的大小相同。

这一点对于我们把逻辑数组索引的结果作为左值和右值时都能很好的保证语义的一致性。在实际进行工程计算时,这也是一个很好的约定。

通过遵守上述预定来使用逻辑数组索引,我们能够很好的把循环+分支的程序改成更加简洁的表示形式,更好地把Matalb当做一个计算器使用。

前面那个例子把矩阵中大于5的元素置为0,小于等于5的元素置为1,使用常规的程序设计语言,我们需要按照如下的方式来实现:

 1A = magic(3);
 2
 3for i = 1:3
 4    for j = 1:3
 5        if A(i, j) > 5
 6            A(i, j) = 0;
 7        else
 8            A(i, j) = 1;
 9        end
10    end
11end

两层循环、判断分支,对于程序员而言,这样的程序是很常见的。但是对于Matlab而言,这样的程序是不够简洁的。我们可以并且习惯通过逻辑数组索引来实现。

这个计算也跟Matlab中大多数操作符、函数能够对矩阵的每个元素进行操作的特性是一致的。

对于某些同时对于矩阵和矩阵元素有意义的操作符和运算,Matlab约定了采用.来表示对矩阵的每个元素进行操作。

1A = magic(3);
2
31 ./ A % [0.125, 1, 0.1667; 0.3333, 0.2, 0.1429; 0.25, 0.1111, 0.5]
4
5A .^ 2 % [64, 1, 36; 9, 25, 49; 16, 81, 4]

除法、乘法、乘方,对于矩阵和矩阵元素的计算有不同含义。

此外,逻辑数组索引还有一个约定。

逻辑数组索引的结果是列向量。

这在Matlab中也是很有一致性的约定。这个约定也是为了保证逻辑数组索引的结果能够作为左值和右值使用时的一致性。

其他不太常见的逻辑数组索引

现在可以回到前面的思考题,这是一种不常见也不常用的逻辑数组索引方式。我们把它写到二维数组,即矩阵,的例子中,更容易看到这种逻辑数组索引的特性。

1A = magic(3);
2
3idx = {[true, true], [true, false, true]};

从结果可以看到:

>> A, A(idx{:})

A =

    8     1     6
    3     5     7
    4     9     2


ans =

    8     6
    3     7

这个结果是很有意思的。实际上,这个计算的结果大概是这样的。

首先,我们把idx展开,得到:

1idx{1}(:) & idx{2}
2% [1, 0, 1; 1, 0, 1]

这个情况下,最终索引的结果是把展开的逻辑数组和被索引的数组在左上角对齐,然后按照逻辑数组的truefalse来选择元素,构成一个数组(并非是列向量)。

而我们如果把idx展开成idx{1} & idx{2}(:)来进行索引,结果是不一样的。

1A(idx{1}(:) & idx{2})
2% [8, 3, 5, 9]

此时,最终索引结果是按照列向量的方式来选择元素,这个语义是具备一致意义的。也就是前面我们给出的约定,索引在转换为列向量的基础上具备统一性。哪怕是索引逻辑数组与被索引数组的长度、维数不一致,也是按照列向量的方式来选择元素。

但是,多维逻辑数组索引A(idx{:})在形状不一致时的索引不具备语义的一致性。建议不要在实际的工程计算中使用这种索引方式,因为这种索引方式的结果是不确定的,准备给MathWorks提出一个建议,对这种索引方式进行限制或者更改为更加具备一致性的索引方式。

总结

  1. Matlab的数组索引看起来非常像是函数调用,也确实可以当作函数调用来理解;
  2. 可以利用位置索引和逻辑索引来访问数组的元素;
  3. 索引的一致性可以通过sub2ind函数来验证。
  4. Matlab的逻辑值是一种特殊的数值类型,只有两个值:1和0,分别代表truefalse,它的大小就只有1Bytes。
  5. 逻辑数组索引是一种非常灵活的索引方式,它的基本概念是用一个逻辑数组来选择数组中的元素。
  6. 逻辑数组索引的产生方式有:逻辑操作符,逻辑计算符号,逻辑函数,判断型的函数,自己定义返回逻辑值的函数。
  7. 工程应用约定:逻辑数组索引由被索引的数组计算得到,逻辑数组的大小和被索引的数组的大小相同。最好一直采用这种方式来对数组进行索引。

文章标签

|-->matlab |-->index |-->索引 |-->入门


GitHub