MATLAB 学习笔记——2. 矩阵
概述
MATLAB 对于矩阵的支持非常好,以矩阵运算为代表的基本运算功能一直是 MATLAB 引以为自豪的核心与基础。我们可以把向量和矩阵都视作矩阵进行统一的操作。在下文中我们默认讨论二维矩阵,但 MATLAB 支持多维矩阵。行向量即行数为 1 的矩阵,列向量即列数为 1 的矩阵。
在内存中,MATLAB 使用列主序进行连续存储,与 Fortran 相同,与 C 语言是反的。在下标的使用中,MATLAB 默认下标从 1 开始,与 Fortran 相同,与 C 语言等绝大部分编程语言都不同。
矩阵的尺寸信息可以通过下面的语句获取:
size(A)行数和列数,返回一个1x2的行向量;size(A,1)行数;size(A,2)列数。
还有如下函数:
numel(A)可以获取(任意维度数组)所有元素个数;length(A)可以获取向量的长度(也就是元素个数),但是扩展到高维数组的行为是非常反直觉的——返回最大维数长度,相当于max(size(A))。
注意:
- MATLAB 对于矩阵的支持是非常彻底的,甚至标量都是被视作 1 行 1 列的矩阵。
- MATLAB
的很多内置函数对于行/列向量有特殊处理,在语义上并不能自然地推广到矩阵,可能是为了用户的便利,也可能是历史兼容性。例如不建议通过
length获取尺寸信息,为了兼容行列向量的长度语义,这个函数只会返回矩阵的行数和列数之间的最大值。 - 下面默认矩阵是由浮点数组成的二维数组,MATLAB也支持更高维度的数组(但是部分矩阵运算不支持高维数组),而且元素类型除了浮点数,还可以是字符、布尔值、结构体数组、元胞数组等。
矩阵字面量
矩阵可以通过字面值创建:
- 空格或者逗号视作单行元素的分隔;
- 分号视作不同行元素的分隔,也可以输入回车进行换行。(输入分号加上回车也只是一次换行效果,并不会叠加)
例如
1 | >> [1 2 3 4] |
(如果尺寸不合法则会报错,MATLAB 不允许长短不一致的矩阵构造)
创建特殊矩阵
可以使用特殊矩阵的构造函数:(注意:单个参数时总是默认方阵)
- 全零矩阵
zeros(m,n)创造 m 行 n 列的全零矩阵;zeros(m)创造 m 行 m 列的全零方阵;zeros(size(A))创造与 A 尺寸相同的全零方阵;
- 全 1 矩阵
ones(m,n)创造 m 行 n 列的全 1 矩阵;ones(m)创造 m 行 m 列的全 1 方阵;ones(size(A))创造与 A 尺寸相同的全 1 方阵;
- 随机矩阵:(每一个元素为0-1的随机数)
rand(m,n)创造 m 行 n 列的随机矩阵;rand(m)创造 m 行 m 列的随机方阵;
- 单位矩阵
eye(m)创造 m 行 m 列的单位方阵;eye(m,n)创造 m 行 n 列的单位矩阵;(只有主对角线为 1,其余均为 0)eye(size(A))创造与 A 尺寸相同的单位矩阵;
冒号表达式用于产生等差数列(行向量),使用冒号表达式可以避免循环,计算效率更高
a:b:c首项为a,公差为b,尾项由c确定但不一定包含。
1 | >> 1:2:10 |
a:b相当于把中间的默认步长b=1省略。
与之类似的是linspace函数:
linspace(a,b,n)基于闭区间生成行向量,第一个值是 a,最后一个值是 b,中间为等差数列,一共 n 个数;- 缺省 n 时,默认取 n=100。
矩阵拼接
我们可以很方便地把两个矩阵拼起来,包括水平拼接和竖直拼接,它们分别要求两个矩阵有相同的行数或列数:
horzcat(A, B)或[A B]:水平拼接vertcat(A, B)或[A; B]:竖直拼接
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22>> A = [1 2; 3 4];
>> B = [5 6; 7 8];
>> horzcat(A, B)
ans =
1 2 5 6
3 4 7 8
>> [A B]
ans =
1 2 5 6
3 4 7 8
>> vertcat(A, B)
ans =
1 2
3 4
5 6
7 8
>> [A; B]
ans =
1 2
3 4
5 6
7 8
矩阵的基础运算
矩阵可以直接和数相加减,默认会逐个元素进行。
1 | >> zeros(2,3)+2 |
可以对矩阵执行内置函数,默认会逐个元素执行。
1 | >> a=[1 2 3;4 5 6];sin(a) |
矩阵的两种转置:共轭转置A',转置A.'。(对于实数矩阵两者一样)
矩阵乘法:A*B执行标准的矩阵乘法。
1 | >> A=[1 1;2 2];B=[1 2;3 4];A*B |
两个矩阵进行逐个元素的乘法:A.*B
1 | >> A=[1 1;2 2];B=[1 2;3 4];A.*B |
同理,还有逐个元素的乘方.^。
尤其注意,这里的运算例如+或.*对于尺寸兼容的 A
和 B 也是可以的,
对于矩阵的常用运算还包括:
inv(A)求逆rank(A)秩det(A)行列式exp(A)逐元素指数
注意:
- 这几个函数对大规模稠密矩阵不建议使用,因为数值算法的效率很低,而且效果通常并不太好,而且很多情况下并不需要。
exp(A)是对矩阵的元素逐个计算exp,矩阵的指数运算是expm(A)。
矩阵的兼容和隐式扩展
两个矩阵称为兼容的,如果它们的尺寸分别为 (m1,m2,m3) 和
(n1,n2,n3),总是可以加 1 使得描述它们尺寸的整数个数一致。
要求要么 m1=n1,要么其中一个为 1,此时维度为 1
的那个矩阵可以通过在这个维度上的简单复制达到相同的尺寸,对于
m2 与 n2,m3 与 n3
也同理。
对尺寸不同,但是兼容的两个矩阵所进行的二元运算,可能会自动触发隐式扩展,例如
1 | A=[1 2 3]; % 行向量 |
它们做逐个元素的乘法时,会自动对尺寸进行扩展,在这个例子中就是都变成尺寸
(3,4) 的矩阵,在扩展的维度上只是单纯的复制
1 | A-> |
然后进行运算,计算A+B和A.*B得到的结果分别为
1 | A+B = |
注意:
- 行向量和列向量的乘法是向量乘法,但是逐元素乘法会得到矩阵。
- 列向量和行向量的乘法因为隐式扩展,完全等效于逐元素乘法,两者都会得到矩阵。
扩充矩阵
MATLAB的矩阵尺寸不是固定的,甚至允许在赋值时直接扩充矩阵尺寸,例如扩充向量
1
2
3
4
5a = [];
a(1) = 1;
a(2) = 2;
a % [1 2]
扩充矩阵 1
2
3
4
5
6
7
8b = zeros(2,2);
b(2,:) = [2 2];
b(3,:) = [3 3];
b %
% 0 0
% 2 2
% 3 3
扩充后对于没有提供值的部分会自动补0。
更极端的情况是:即使这个变量未定义,我们也可以通过赋值扩容的方式直接创建一个矩阵,例如
1
2
3
4
5
6
7
8>> clear z
>> z
Unrecognized function or variable 'z'.
>> z(3,2)=1
z =
0 0
0 0
0 1
对这个语法糖我实在是忍不住要吐槽:编程时难免会因为一些疏漏(导致矩阵未定义就使用,或者索引越界出现错误), MATLAB 检查不出来这类问题也就算了,居然还根据越界的指标自动扩充矩阵??? MATLAB能不能出一个选项,禁止这些垃圾语法糖???
当我们尝试对矩阵进行就地扩容时,MATLAB 可能会进行空间分配并对整个数组拷贝迁移, 当然,在扩容时也可能会预留更多的空间(就像C++的vector的扩容机制一样),并不是每一次添加都意味着要拷贝整个矩阵。
更改矩阵形状
MATLAB 提供 reshape 函数用来更改矩阵的尺寸(基于数据在内存中的列优先顺序),但是并不会修改原矩阵,而是返回一个指定尺寸的新矩阵。
与扩充尺寸必然带来的内存拷贝不同,由于MATLAB采用了懒拷贝机制,如果后续不对新矩阵中的元素进行修改,那么修改尺寸可能并不会产生额外的拷贝开销,相当于 C++ 中的引用传递。
支持两种写法,第一种是直接提供完整的目标尺寸向量(必须和原始矩阵的数据长度相匹配,否则报错),例如
1
2A = 1:10;
B = reshape(A,[5,2])
可以很方便地搭配size函数使用,例如 1
2C = [1 2 3 4 5 6 7 8 9 10];
D = reshape(C,size(B))
第二种则是分别提供每一个维度的长度,此时可以使用一个[]缺省,MATLAB会自动计算缺少的维度长度,例如
1
2A = 1:10;
B = reshape(A,[],2) % size = [5,2]
注意:
- reshape 和 resize 这两个名称在不同语言中经常混淆;
- 较新版本的 MATLAB 也提供了 resize 函数,但是作用只是用来在尾部加0或移除元素,也就是只考虑实际数据长度进行截断或延长,通常不需要。
矩阵的线性索引
无论是矩阵还是退化的行向量列向量,或者高维矩阵,每一个元素都可以使用一个正整数进行索引,称为线性索引。
与之不同的是,在 C/C++ 和 Numpy 中,二维数组通常是通过一维数组的数组实现的,因此对二维数组用一个整数索引会得到一个一维数组。
线性索引从 1 开始,完全对应于矩阵在内存中列主序的排列顺序。(在同一列中,相邻的元素就在内存中相邻)
1 | >> a=[1 2 3; 4 5 6] |
支持基于线性索引的切片操作,这样会得到一个向量:(如果A是行/列向量,那么结果也是行/列向量,如果A是矩阵,那么结果始终是行向量)
A(m:n)第 m 到 n 个元素;A(m:end)第 m 个元素到最后一个元素;
例如 1
2
3
4
5
6
7
8
9
10>> a=[1 2 3; 4 5 6]
a =
1 2 3
4 5 6
>> a(1:3)
ans =
1 4 2
>> a(4:end)
ans =
5 3 6
非常奇怪的设定是,a(:)会得到一个包含所有元素的列向量,而不是一个行向量。
1 | >> a=[1 2 3; 4 5 6] |
可以利用线性索引展平得到的向量进行一些快捷操作,例如对所有元素求和
1
2
3sum(A(:))
% or
sum(A, [], 'all')
矩阵的位置索引
与线性索引不同,更直观的索引当然是直接指定元素对应的行数和列数(用逗号分隔),对于更高维的数组同理。对于行向量,位置索引和线性索引没有区别。
例如 1
2
3
4
5
6
7>> a=[1 2 3; 4 5 6]
a =
1 2 3
4 5 6
>> a(1,2)
ans =
2
可以一次性访问多个元素,例如 1
2
3
4
5
6
7>> a=[1 2 3; 4 5 6]
a =
1 2 3
4 5 6
>> a(2,[1,3])
ans =
4 6
支持基于位置索引的切片操作,可以提取指定的部分元素得到子矩阵:
A(m:n,p)第 m 到 n 行,第 p 列;A(m,:)第 m 行,所有列;A(m:end,:)第 m 到最后一行,所有列;
例如 1
2
3
4
5
6
7
8
9
10
11>> A = [1 2 3 4; 5 6 7 8; 9 10 11 12; 13 14 15 16]
A =
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
>> A(1:3,2:4)
ans =
2 3 4
6 7 8
10 11 12
矩阵的索引赋值
MATLAB 基于索引和切片对矩阵进行部分赋值的逻辑非常奇怪,有些看起来不合理的语句竟然都是可以的,有些则又会报错?有一个相关的官方文档Indexed Assignment,但是太简略了,过于灵活的语法导致会很多错误难以发现。
下面的例子均取 1
2
3
4
5
6>> A = [1 2 3 4; 5 6 7 8; 9 10 11 12; 13 14 15 16]
A =
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
理想的情况下,赋值应该在两个尺寸相符的子矩阵之间进行,例如
1
2
3
4
5
6>> A(1:2,3:4) = [10,20;30,40]
A =
1 2 10 20
5 6 30 40
9 10 11 12
13 14 15 16
之前二元运算中使用的隐式扩展在这里并不允许,例如 1
2
3
4
5
6
7
8>> A(1:2,:) = [1,1,1,1]
Unable to perform assignment because the size of the left side is 2-by-4 and the size of the right side is 1-by-4.
>> A(1:2,:) = [1,1,1,1;1,1,1,1]
A =
1 1 1 1
1 1 1 1
9 10 11 12
13 14 15 16
但是MATLAB其实也并不严格要求两者尺寸一致,例如使用行向量给列赋值是允许的
1
2
3
4
5
6>> A(:,1) = [1,1,1,1]
A =
1 2 3 4
1 6 7 8
1 10 11 12
1 14 15 16
使用列向量给行赋值也是允许的 1
2
3
4
5
6
7
8
9
10
11
12>> A = [1 2 3 4; 5 6 7 8; 9 10 11 12; 13 14 15 16]
A =
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
>> A(1,:) = [1,1,1,1]'
A =
1 1 1 1
5 6 7 8
9 10 11 12
13 14 15 16
一个常见的情景是用一个常数赋值一部分元素,此时无论赋值对象的尺寸如何,在语法上都是允许的,例如
1
2
3
4
5
6
7
8
9
10
11
12>> A = [1 2 3 4; 5 6 7 8; 9 10 11 12; 13 14 15 16]
A =
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
>> A(1:3,2:4) = 3
A =
1 3 3 3
5 3 3 3
9 3 3 3
13 14 15 16
在对切片进行赋值时,切片也可以包括当前矩阵不存在的部分,赋值完成后会自动对矩阵进行扩充,例如
1
2
3
4
5
6
7>> S=[1 2]
S =
1 2
>> S(2,:) = 1
S =
1 2
1 1
基于线性索引的部分赋值也是可以的,例如(这里换成行向量也可以,只要保证元素个数相等)
1
2
3
4
5
6>> A(1:8)=[10;20;30;40;50;60;70;80]
A =
10 50 3 4
20 60 7 8
30 70 11 12
40 80 15 16
矩阵的逻辑运算
逻辑判断
直接对矩阵使用 if 语句会发生什么? 1
2
3
4
5a = [1, 2, 3];
if a
disp('True')
end
这等价于下面的写法(也更推荐,可读性更高) 1
2
3
4
5
6a = [1, 2, 3];
% 判断所有元素是否都非零
if all(a(:))
disp('True')
end
几种建议写法如下
1 | if all(a(:)) % 所有元素非零 |
all 和 any
支持按指定维度进行运算,在判断时保留指定的维度:
all(A, dim): 判断dim方向上所有元素是否均为非零。any(A, dim): 判断dim方向上是否至少有一个非零元素。
例如 1
2
3
4
5
6
7A = [1 0 3;
4 5 6];
all(A(:)) % false
any(A(:)) % true
all(A, 1) % [true, false, true]
all(A, 2) % [false; true]
逻辑运算
MATLAB 支持对矩阵进行逐元素逻辑运算 (&,
|,
~),注意这不是通常的位运算,它只关注非零(true)和零(false),返回的是逻辑数组。
例如 1
2
3
4
5
6
7
8
9
10A = [1 0; 2 3];
B = [0 1; 1 0];
C1 = A & B % 逐元素 "AND"
C2 = A | B % 逐元素 "OR"
C3 = ~A % 逐元素 "NOT"
% C1 = [0 0; 1 0]
% C2 = [1 1; 1 1]
% C3 = [0 1; 0 0]
与之类似的短路逻辑运算 (
&&,||) 则不支持矩阵,仅能对标量使用。
逻辑索引
MATLAB 允许使用 逻辑值 作为索引来筛选数据,以这种方式对矩阵进行读写的效率很高。
例如 1
2
3
4
5
6
7
8A = [1 2 3; 4 5 6];
idx = (A > 2);
% 0 0 1
% 1 1 1
A(idx)
% [4; 5; 3; 6]
也可以直接写作 1
2A(A>2)
% [4; 5; 3; 6]
可以直接对满足条件的部分数据进行修改,只要赋值的数据尺寸满足要求(例如可以隐式扩展)
1
2A(A > 25) = 100; % 把所有大于25的元素设为100
A(A > 10 & A < 50) = 0; % 10 到 50 之间的数置为 0
补充
在编程实践中,仅仅只靠浮点数数组还是不够的,还需要其他更灵活的数据结构的支持(运行效率也会更慢),主要包括结构体数组和元胞数组。
结构体数组
虽然MATLAB官方将其称为结构体数组,但是实际上不管是从使用还是实现的角度,将其称为字典都更合适。在C++的语境中,结构体约等于自定义类型,而对于MATLAB,自定义类型和结构体数组是截然不同的。
结构体数组(struct)是 MATLAB 中一种非常灵活的具名数据类型,可以将不同类型的数据直接组合在一起,每个字段具有名称,可以包含不同类型和大小的数据,结构体数组使得组织和管理复杂数据更加方便。
结构体数组的使用非常灵活,可以使用点符号直接创建 1
2
3
4
5% 此前person未定义
% 创建一个结构体数组并添加字段
person.name = 'John Doe';
person.age = 30;
person.height = 1.75;
需要注意的是,这里的person变量尚未定义,否则上述语句只有在person已经是一个结构体数组时才会成立,此时的效果是创建或修改对应的字段。
甚至支持下面这种多层结构体的直接创建 1
2% 此前x,y,z都未定义
x.y.z = 1
那么就会自动创建结构体x,包含的唯一成员是结构体x.y,它包含的唯一成员是x.y.z。
对未定义变量直接操作的语法糖导致我们可能无意之间写出错误的语句,例如一个错误的变量名就会导致创建了一个新的结构体数组,而非对已有的结构体数组的字段进行操作,这个错误还非常隐蔽。(MATLAB的语法设计真是太合理了)
我们还可以使用struct函数创建,一次性添加多个字段以及对应的数据
1
person = struct('name', 'Jane Doe', 'age', 28, 'height', 1.65);
结构体数组并没有明确完整的创建过程,我们可以在任何时间自由地向结构体数组中添加字段,或对现有字段进行访问和修改。
每一个结构体数组都是完全独立的,对于这个结构体数组可以有(或者必须有)哪些字段没有任何要求
1
2
3disp(person.name); % 输出:John Doe
disp(person.age); % 输出:30
disp(person.height); % 输出:1.75
尝试读取不存在的字段会导致错误。
我们可以使用内置函数isfield来检查结构体数组是否具有某个字段
1
2
3% 检查字段
hasAge = isfield(person, 'age'); % 返回 true
hasHeight = isfield(person, 'height'); % 返回 false
我们可以使用内置函数rmfield来移除结构体数组的字段,需要用返回值重新赋值
1
2
3
4
5% 删除字段
person = rmfield(person, 'height');
% 尝试访问已删除的字段将会导致错误
% disp(person.height); % 错误
从上面的介绍可以看出,结构体数组实际上就是一个键为字符串的字典,语法非常灵活,易与后文中的面向对象的语法产生冲突,因此决定尽量避免使用结构体数组,转而使用更安全的面向对象的语法!
元胞数组
元胞数组是一种包含名为cell的索引数据容器的数据类型,其中的每个cell都可以包含任意类型的数据。
元胞数组可以包含文本列表、文本和数字的组合或者不同大小的矩阵等,通常用于打包一组矩阵或多组矩阵。
通过将索引括在圆括号()中可以引用cell,使用花括号{}进行索引来直接访问cell的内容,两者区别见下文。
元胞数组的创建方式如下,元胞数组通常使用二维的结构,m行n列,每一个元素是一个矩阵或其他任何对象,元胞数组也支持多维的定义和操作。
1
2
3C1 = {}; % 空的元胞数组
C2 = {1,2,3;
'text',rand(5,10,2),{11; 22; 33}} % 2*3的元胞数组
可以定义指定尺寸的,每一个元素均为空矩阵的元胞数组:
C = cell(n),返回由空矩阵构成的\(n\times n\)元胞数组。(注意这不是 \(1\times n\) 或 \(n \times 1\))C = cell(s1,...,sn)返回由空矩阵构成的 \(s_1 \times \dots \times s_n\) 元胞数组。例如,cell(2,3)返回一个 \(2\times 3\) 元胞数组。C = cell(a)返回由空矩阵构成的元胞数组,并由行向量a来定义尺寸,例如,cell([2 3])返回一个 \(2\times 3\) 元胞数组。(使用列向量是非法操作)
元胞数组和普通矩阵一样,可以在赋值的同时直接进行扩充,并且与矩阵不同,元胞数组支持不同的数据组成cell
1
2
3
4
5C = {'2017-08-16',[56 67 78]}; % size 1*2
% append
C(2,:) = {'2017-08-17',[58 69 79]}; % size 2*2
C(3,:) = {'2017-08-18',[60 68 81]}; % size 3*2
此时()和{}索引得到的结果是不同的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15>> C(1,1)
ans =
1×1 cell array
{'2017-08-16'}
>> C{1,1}
ans =
'2017-08-16'
>> C(1,2)
ans =
1×1 cell array
{[56 67 78]}
>> C{1,2}
ans =
56 67 78
前者仍然是一个 \(1\times 1\) 的元胞数组,后者得到的则是实际的矩阵或字符串。
基于花括号{}索引获取分量或矩阵之后,就可以直接赋值和修改了,甚至并不需要初始化,直接赋值就可以使用
1
2
3
4
5
6% x is undefined
x{1} = 10
x
% x 1x1 cell array
% {[10]}
元胞数组是可以嵌套的,嵌套并不等价于元胞数组的二维结构!
1
2
3D1 = {1,2,3;
'text',rand(5,10,2),{11; 22; 33}} % 2*3的元胞数组
D2 = {{1,2,3}, {'text',rand(5,10,2),{11; 22; 33}}} % 1*2的元胞数组
结构体数组和元胞数组转换
使用内置函数filedsname可以基于结构体数组的所有字段名,创建为一个\(n \times 1\)的元胞数组,例如
1
2
3
4
5
6
7>> person = struct('name', 'Jane Doe', 'age', 28, 'height', 1.65);
>> fieldnames(person);
ans =
3×1 cell array
{'name' }
{'age' }
{'height'}
与之对应的是,使用内置函数struct2cell可以基于结构体数组所有字段的值,创建为一个\(n \times 1\)的元胞数组,例如
1
2
3
4
5
6
7>> person = struct('name', 'Jane Doe', 'age', 28, 'height', 1.65);
>> struct2cell(person)
ans =
3×1 cell array
{'Jane Doe'}
{[ 28]}
{[ 1.6500]}
使用内置函数cell2struct可以通过存储字段名和值的元胞数组,创建对应的结构体数组
1
2
3
4
5
6
7cellArray = {'John Doe', 30, 1.75};
fieldNames = {'name', 'age', 'height'};
% 将元胞数组转换为结构体数组
person = cell2struct(cellArray, fieldNames, 2);
disp(person);
需要注明的是,第三个参数是创建结构体数组所使用的维度。
