3.4.4.1 逻辑运算函数
在上一节介绍关系运算时,我们提到过: MATLAB 中使用逻辑值表示布尔变量,逻辑值 1 代表真(true),逻辑值 0 代表假(false)。而逻辑运算就是对逻辑值进行的运算。大部分的编 程语言都会涵盖四个最基本的逻辑运算方法: 逻辑与、逻辑或、逻辑非和逻辑异或。
下表给出了 MATLAB 中的定义(注意:下表中的 1 和 0 是逻辑值 1 和 0):
注意, 上表中函数名和对应的运算符可以执行相同的功能, 除了“逻辑异或”没有相应的 运算符外,剩下三个运算方法都有对应的运算符。
(1)&,|,~和logical函数、true函数、false函数
这四个运算方法的使用方法较为相似, 以“逻辑与”为例, 大家可以查看“逻辑与”的帮 助文档: A & B 对数组 A 和 B 执行逻辑 AND 操作,并返回包含设置为逻辑值 1 (true) 或 逻辑值 0 (false) 的元素的数组。如果 A 和 B 在相同的数组位置都包含非零元素,则返回 的数组中对应位置的元素将设置为逻辑值 1 (true)。如果不是,则将数组元素设置为 0。 and(A,B) 是执行 A & B 的替代方法, 但很少使用。(注: 该帮助文档来自于 MATLAB 2023 之前 的版本,新版本增加对表格类型数据的计算)
从帮助文档可以得知:(1)MATLAB 推荐大家直接使用运算符进行计算, 因此& 、|和~这 三个符号的功能大家要牢记,我们主要使用这三个符号而不是对应的函数进行计算。(2)“逻 辑与&”是对数组 A 和 B 进行计算的, 计算时会比较 A 和 B 对应位置的元素。数组 A 和 B 的 大小不一定要完全相同, 只需要符合算术运算中介绍的五种兼容模式即可。(3)“逻辑与&”不 仅可以作用在逻辑值 0 和 1 上,还可以用于普通的数值上,这时候, MATLAB 会将非零数值 视为逻辑 1,将数值零视为逻辑 0 进行运算。 例如: 3&5 返回逻辑值 1 ,-4&0 返回逻辑值 0。 我们来看两个例子:
有同学可能会有疑问, 为什么 MATLAB 会将非零数值视为逻辑 1,将数值零视为逻辑 0? 这是因为 MATLAB 在进行逻辑运算之前, 在计算机内部自动将数值转换成了逻辑值。我们也 可以使用 logical 函数手动进行转换:
L = logical(A) 将 A 转换为一个逻辑值数组。 A 中的任意非零元素都将转换为逻辑值 1 (true),零则转换为逻辑值 0 (false)。复数值和 NaN 不能转换为逻辑值。
举个例子,我们随机生成一个 4 行 3 列的矩阵,将矩阵中非 0 位置的元素转换成逻辑值 1,等于 0 的位置的元素转换成逻辑值 0:
特别地, 我们还可以使用 true和 false 函数分别创建全为逻辑 1 和逻辑 0 的逻辑矩阵。以 true 函数为例, 它的主要用法有两种:(1)true(n)可以生成一个 n 行 n 列全为逻辑值 1 的方阵, 特别地, 当 n 等于 1 时可以简写为 true,此时表示一个常量;(2)true(m,n)可以生成一个 m 行 n 列全为逻辑值 1 的矩阵。
除了“逻辑与&”外,剩下的三个逻辑运算函数也可以用于普通的数值上, MATLAB 也会
将非零数值视为逻辑 1,将数值零视为逻辑 0 进行运算,下面我们举几个例子:
另外,我们有时候也会使用“逻辑与 &”和“逻辑或 | ”进行连续运算,例如 1 & 2 & 3
和 0 | 3 | 0 的结果都是逻辑 1,下面我们再看几个例子:
下面我们再次谈谈运算优先级的问题, MATLAB 中的运算符有不同的计算优先级,优先 级高的先进行计算, 例如 3+4*2 等于 11 而不是 14,这是因为乘法的优先级高于加法。类似的, 关系运算符(例如> 、== 、~=)的优先级要高于逻辑运算符&和|,例如 3 > 4 & 2 > -1 的返回结果 是逻辑 0。大家可以去 MATLAB 官网查看运算符优先级的帮助文档,但没有必要刻意去记, 我们只需要养成一个好的习惯: 使用小括号来指定计算的先后顺序,例如我们可以将上面的代 码改成(3 > 4) & (2 >-1),这样计算的先后顺序看起来会清楚很多。另外有一个特殊的优先级顺 序需要大家了解, & 运算符的优先级要高于 | 运算符。尽管 MATLAB 通常按从左到右的顺 序计算表达式, 但表达式 a|b&c 按 a|(b&c) 形式计算,因此,对于同时包含 & 和 | 的语句, 比较好的做法是使用小括号显式地指定期望的语句优先级。
我们来做一个小练习:随机生成一个具有 20 个元素的向量,用来表示 20 名同学的成绩 (假设成绩为满分 100 分的整数制)。请通过代码计算得到一个具有 20 个元素的逻辑向量, 向 量中对应位置的元素为逻辑值 1 时表示该同学的成绩在区间[60,80)内,为逻辑值 0 时表示成绩 在区间[60,80)外。
答案如下:
A = randi([0,100],1,20)
res = (A >= 60) & (A < 80)
一定要注意: 不能写成: 60 <= A < 80 哦!
如果要找出[0 , 60) ∪ [80 , 100]分的同学呢?
答案如下(下面两种方法都可以):
方法 1:(A < 60) | (A >= 80)
方法 2:~((A >= 60) & (A < 80))
(2)&&和||
下面我们再来介绍 MATLAB 中另外两个使用频率很高的逻辑运算符: &&和||.
这两个运算符和“逻辑与&”和“逻辑或|”作用相同,但它们有两个非常重要的区别:
(1)&&和||只能对标量(只有一个元素) 进行逻辑运算,不能对有多个元素的向量或者矩阵进行运算,而&和|可以。比如我们上面那个练习题,你只能使用&和|进行运算。
(2)&&和||进行逻辑运算时具有短路功能,可以提高运行效率:
- 计算 A && B 时,如果 A 为逻辑 0,则 B 不会被判断, 因为最后的结果一定是逻辑 0;
- 计算 A || B 时,如果 A 为逻辑 1,则 B 不会被判断,因为最后的结果一定是逻辑 1。
举个例子: 假设 a 等于 10,b 等于 3,现在要计算: (a+b < 10) && (a/b > 1),那么 MATLAB 首先会判断前面一项: (a+b < 10),因为这一项计算的结果为逻辑 0,所以后面的(a/b > 1)这一 项不会被计算, MATLAB 会直接返回逻辑 0;如果你使用的是: (a+b < 10) & (a/b > 1),那么这两项都会被计算, 这样的话效率会低一点。在下一章中, 我们会介绍 if 判断语句和 while 循环 语句,和& 、|相比, &&和||在 if 和 while 语句中使用频率更高。
思考题: 前面我们介绍过 logical 函数, 它能将数值转换为逻辑值, 但如果输入的是 NaN , 那么 MATLAB 会报错,请大家思考:分别运行(10 > 3) | logical(NaN)和(10 > 3) || logical(NaN) 的结果是什么?
答案如下:
运行 (10 > 3) | logical(NaN) 会报错,因为前后两项都会被计算,所以当 MATLAB 运行到 后面的 logical(NaN)时会报错;运行 (10 > 3) || logical(NaN) 会返回逻辑 1,因为前面一项(10>3) 返回逻辑 1,此时会触发||的短路机制,这时候就会直接返回逻辑 1。
3.4.4.2 利用逻辑值引用矩阵的元素
前面我们学过 MATLAB 中的向量和矩阵, 并介绍过如何通过元素所处的位置索引来引用 (提取)所需的元素。例如:若 a 是一个向量,则 a(1:2:end)表示提取 a 中所有奇数位置的元 素;若 A 是一个矩阵,则 A(1:2:end, :)表示提取 A 中所有奇数行的元素。
本小节我们介绍引用矩阵的元素的另一种方式:利用逻辑值。
假设有一个 m 行 n 列的矩阵 A,我们要提取其指定位置的元素, 那么我们可以生成一个和 A 同样大小的逻辑矩阵 L,L 中的元素要么是逻辑值 1,要么是逻辑值 0,其中:等于逻辑值 1 的元素所处的位置是我们所需要的。接着我们只需要使用命令 A(L),就能够在 A 中提取出指 定位置的元素。通常逻辑矩阵 L 是由一系列的逻辑运算或者条件运算得来的。
举个例子,我们要提取 A 中所有小于等于 3 的元素,这里的 A 是我随机生成的一个矩阵:
这里的 L 就是通过关系运算得到的一个逻辑矩阵,它的大小和 A 相同, L 中为逻辑值 1 的位置就是矩阵 A 中要引用的位置。
注意:命令 A(L)得到的结果是一个列向量,有同学会问:为什么 A(L)返回的列向量中的 元素顺序是 3, 1,2,而不是其他的顺序?这是因为 MATLAB 会按照线性索引的顺序来返回提取 的元素,就类似于 A(:)命令可以将 A 中的所有元素按照线性索引的顺序返回一个列向量。
以后大家熟练的话,可以直接写成 A(A<=3)来提取 A 中所有小于等于 3 的元素。再举个 例子,如果要提取 A 中大于等于 5 且小于 8 的元素,我们可以使用 A( A>=5 & A < 8),千万不能写成 A(5 <= A < 8),也不能使用&&运算符:A( A>=5 && A < 8) ,&&运算符只能用于标量的逻辑运算。
注意,这里有一个易错点: L 必须是逻辑矩阵, 即里面的 0 和 1 都必须是逻辑值, 不能是 由数字 0 和 1 构成的数值矩阵。 如果 L 是数值矩阵, 可以使用 logical 函数进行转换。
练习题:清风班上有 20 名同学, 这 20 名同学的编号为数字 1 至 20 。现在清风老师要随机请一些同学去吃饭, 每名同学被抽中的概率都是 50%,请帮助清风老师随机抽取这些同学 (学过概率论的同学应该能够看出来, 每名同学是否能被选中均服从一个伯努利分布,就类似 于独立的投 20 次篮,每次投中的概率都是 0.5)。
答案如下:
A = 1:20; % 这 20 名同学的编号为 1,2, ……,20
x = randi([0,1],1,20); % 随机生成 20 个由 0 和 1 构成的随机数
L = logical(x); % 转换成逻辑值
A(L) % 使用逻辑向量 L 引用 A 中的元素
为什么上面的答案可行?第二行代码随机生成了一个包含 20 个元素的行向量 x ,向量 x 中的每个元素都是随机生成的 0 或 1,其中 0 代表同学没有被选中, 1 代表同学被选中。 由于 randi 函数用来生成随机的均匀分布的整数,因此出现 0 和 1 的概率相同,这就满足了每名同 学被抽中的概率都是 50%。第三行代码使用 logical 函数将 x 这个数值向量转换成了逻辑向量, 这样才能通过逻辑值对向量元素引用。大家可以自己尝试,如果不使用 logical 函数进行转换 的话, MATLAB 就会报错:数组索引必须为正整数或逻辑值。
除了以上方法外, 还有一种更为通用的方法, 不妨假设每名同学被抽中的概率都是 p,其 中 0≤p≤1 ,那么我们可以借助下面的代码来抽取这些同学:
更通用的答案:
p = 0.5; % 每名同学被抽中的概率
A = 1:20; % 这 20 名同学的编号为 1,2, ……,20
r = rand(1,20); % 随机生成 20 个在 0 至 1 区间上均匀分布的随机数
L = (r < p); % r 和 p 比较大小
A(L) % 使用逻辑向量 L 引用 A 中的元素
在更通用的答案中, 我们通过比较由 rand(1,20)生成的、位于[0, 1]区间上的 20 个随机数 r 与每名同学被抽中的概率 p 来构建逻辑数组 L。对于每一个元素 r(i),如果 r(i) < p,则 L(i)为 逻辑值 1,表示第 i 个同学被抽中;反之,如果 r(i) >= p,则 L(i)为逻辑值 0,表示第 i 个同学 未被抽中。最终,逻辑数组 L 的逻辑值 1 和逻辑值 0 会被用来从原始数组 A 中提取元素,即 A(L)会返回所有被抽中同学的编号。
下面我们再来看一个练习题:随机生成 200 名同学的考试得分(0-100 整数),统计得分大 于等于 60 分的同学人数,并计算得分位于区间[60,80]内的所有同学的平均分。
答案如下:
A = randi([0,100],200,1); % 随机生成 200 名同学的分数
sum(A>=60) % 大于等于 60 分的人数
tmp = A(A<=80 & A >=60); % 区间[60,80]内的所有分数
mean(tmp) % 计算平均值
3.4.4.3 使用逻辑值修改或删除矩阵元素
(1)普通元素的修改和删除
上一小节我们介绍了利用逻辑值引用矩阵的元素, 我们也可以对引用的元素进行修改或删 除。下面我们直接看例子:
从上表最后一个例子可以看出,使用逻辑索引删除矩阵中的元素后, MATLAB 会将矩阵 中剩下的元素按照线性索引的顺序放入到一个行向量中。
(2)缺失值的识别和填补(用到isnan()函数)
我们再来看一个有趣的问题: 缺失值的识别和填补。
举个例子: 假设清风老师要连续一周测量早上 6 点室外的温度, 结果清风老师周二和周五 睡过了头,那两天的温度没有测量,剩下五天的温度分别是 10°、 5°、 2°、 8°和 5 。,周 二和周五的温度成了缺失值。现在清风老师想利用有数据的剩余五天的平均气温来代替周二和 周五这两天的温度, 于是他计算出这五天的平均气温为(10+5+2+8+5)/5=6°, 这时候就完成 了对缺失值的填补。
那么我们怎样在 MATLAB 中实现这个过程呢?我们可以先定义一个向量A 用来保存这一 周的温度: A = [10 NaN 5 2 NaN 8 5],其中第二个元素和第五个元素为 NaN,代表周二和周五 的温度数据是缺失的。现在需要大家将 A 中所有的 NaN 值替换成所有非缺失值的平均值。
答案只需要一行代码: A(isnan(A)) = mean(A(~isnan(A)))。这里用到了 isnan 函数, 它可以 判断数组中的元素是否为不定值 NaN,并返回一个和输入的数组大小相同的逻辑数组。
例如,这里的 isnan(A)返回的结果就是[0 1 0 0 1 0 0]这个逻辑向量。
有同学会想:为什么不直接用命令 A==NaN 来找 A 中的缺失值? 这是因为在 MATLAB 中, NaN 相互之间不相等,运行 NaN ==NaN 会输出逻辑值 0。
那么,如何找出 A 中所有非缺失值的元素呢?我们可以对 isnan(A)的结果进行“逻辑非” 运算,即~isnan(A),然后再利用这个逻辑向量对 A 进行索引: A(~isnan(A))。
详细的介绍大家可以看本书的配套视频。后续章节中我们会更系统地介绍缺失值的知识点, 现在只是小试牛刀。
3.4.4.4 all 、any 和 find 函数
下面我们介绍三个非常重要的函数,它们的作用请看下表:
函数名 | 作用 |
all | 判断数组元素是否全为非零值(可指定沿什么维度判断), 全为非零值时返回逻辑 1,否则返回逻辑 0 |
any | 判断数组元素中是否存在至少一个非零值(可指定沿什么维 度判断),是的话返回逻辑 1,否则返回逻辑 0 |
find | 查找数组中的非零元素,并返回其索引 |
(1)all函数
其中, all 函数和 any 函数的用法类似,以 all 函数为例,它的用法如下:
(1)如果 A 是一个向量,那么当所有元素均为非零值时, all(A)返回逻辑值 1 (true),当 存在一个或多个元素为零时,返回逻辑值 0 (false)。
(2)如果 A 是一个矩阵,那么 all(A,dim) 沿着 dim 维来判断元素是否全为非零值, dim 等于 1 时沿着行方向来判断每一列是否全为非零值,并将结果返回为一个全为逻辑值的行向 量; dim 等于 2 时表示沿着列方向判断每一行是否全为非零值, 并将结果返回为一个全为逻辑 值的列向量。特别地,当 dim 等于 1 时,可以直接简写成 all(A)。
(事实上, all 函数和 any 函数的用法和我们之前讲解的 sum 函数非常像)
可以看出, all 函数相当于对向量或者矩阵的元素进行‘逻辑与&’运算, 只有全为非零值 时才返回逻辑值 1。而 any 函数则相当于对元素进行‘逻辑或|’运算,存在至少一个非零值时 就会返回逻辑值 1。
(2)any 函数
我们来看 any 函数的例子:
事实上, all 函数和 any 函数很少直接运用在数值矩阵上,它常常配合逻辑矩阵来实现特 定的功能。我们来看下面的练习题:
(1)请随机生成一个 100 行 3 列的矩阵,用来记录学生的考试成绩:矩阵每一行代表一 名同学,每一列代表一门科目的成绩,矩阵中的每个元素都是区间[50,100]内的随机整数。
score = randi([50,100],100,3)
(2)三门科目的成绩都不低于 85 分的同学可以获得奖学金评选的资格, 请指出哪些同学 可以获得资格。要求返回一个包含 100 个元素的逻辑向量, 元素为逻辑值 1 的位置对应的同学 有评选资格。
all(score >= 85,2)
(3)请指出哪些同学挂科了,至少有一门科目没过 60 分就算挂科。要求返回一个包含 100 个元素的逻辑向量,元素为逻辑值 1 的位置对应的同学挂科了。
any(score < 60,2)
(4)这三门科目中是否存在科目没有人挂科(所有同学的这一门科目的成绩都高于 60 分)。 要求返回一个包含 3 个元素的逻辑向量,元素为逻辑 1 的位置对应的科目表示没有人挂科。
all(score >= 60)
(3)find 函数
大家应该注意到了, 上面问题的答案有一点冗余。例如第二问我们关心的是哪些同学可以 获得评选资格, 但是答案返回的结果是一个长度为 100 的逻辑向量, 向量中也包含了没有获得 评选资格的同学, 他们用逻辑值 0 表示。那么有没有一种方法能够找到这个向量中所有非零元素呢? find 函数可以帮助我们实现!下面是 MATLAB 官方文档对于 find 函数的介绍:
默认情况下, find 函数会返回所有非零元素的索引, 如果只给 find 函数一个返回值, 那么 会返回所有非零元素的线性索引; 如果给两个返回值, 那么会返回非零元素对应的行和列下标; 如果给三个返回值, 那么还会返回非零元素构成的向量。另外, 大家也可以指定返回前 n 个非零元素的索引, 只需要给定第二个输入参数 n ,此时会返回前 n 个非零元素的索引, 如果要返 回后 n 个非零元素的索引,那么需要使用 find(X,n,'last')。
回到上一页练习题的第二个问题, 我们可以使用下面的命令对代码进行改进, 这样就可以 返回获得资格的同学的索引:
tmp = all(score >= 85,2);
ind = find(tmp)
练习题(接着上题来):
(5)找出恰好挂了两门科目的同学的编号。
tmp = sum(score < 60,2); % 每位同学挂科的数目 find(tmp == 2)
(6)找到总分超过 260 分的同学的编号。
total_score = sum(score,2); % 计算每位同学的总分 find(total_score > 260)