赞
踩
讲解视频:可以在bilibili搜索“MATLAB教程新手入门篇——数学建模清风主讲”。
MATLAB教程新手入门篇(数学建模清风主讲,适合零基础同学观看)_哔哩哔哩_bilibili
在上一节中,我们学习了如何使用字符矩阵来存储多段文本。虽然字符矩阵能够有效地完成这一任务,但它也带来了一个问题:为了使所有文本长度一致,MATLAB会自动在较短文本的末尾补充空格。这种自动补齐机制在处理多个长度不一的文本时可能导致一些不便,特别是当我们需要处理的文本数量较多时,这些额外添加的空格不仅增加了数据处理的复杂性,还可能占用大量内存资源。为了解决这一问题,MATLAB提供了两种更为灵活的数据结构:字符向量元胞数组和字符串数组。
在MATLAB2016b版本发布之前,保存不同长度的文本通常使用字符向量元胞数组。元胞数组是一种特殊的数组类型,它允许我们在同一数组内存储不同大小、不同类型的数据。这意味着,即使是长度不一的字符向量,也可以被方便地存储在同一个元胞数组中。
随着2016b版本的发布,MATLAB引入了字符串数据类型,MATLAB的文本处理能力也有了重大的提升。字符串类型提供了一种更自然、更直观的方式来处理文本数据,它不仅使得文本操作更加简洁,更提高了处理文本数据的效率。
那么,为什么MATLAB要推出新的字符串类型呢?这是因为字符向量元胞数组和字符串类型在保存和读取计算机内存中的文本数据的方式有很大的区别,这直接影响了代码的运行效率。元胞数组允许不同类型的数据存储在同一个数组中,因此数组中的每个元素通常需要单独存储,这将导致MATLAB在访问元胞数组中的数据时需要更多的步骤,比如需要检查数据的类型,这增加了计算成本。相比之下,字符串类型提供了一种更为直接和高效的方式来处理文本数据,这在处理大量数据或进行复杂文本操作时的效率优势尤为明显。
下面我们将5.3节分成三个小节分别进行介绍。首先,在5.3.1节,我们会着重介绍元胞数组的基本概念,了解元胞数组的创建、引用、修改、应用函数等操作。随后,在5.3.2节,我们将转向字符向量元胞数组的实际应用,这里会涉及到一系列基础的文本处理功能,如文本的查找、替换、拆分和连接等操作。最后,在5.3.3节,我们将通过三个综合性的文本处理练习题,来巩固和应用我们学到的知识,帮助大家解决实际问题。这些准备工作将为我们后续在5.4节中学习更高效的字符串类型奠定坚实的基础。
元胞数组是MATLAB在早期版本中就引入的一种灵活且功能强大的数据类型,它的设计初衷是为了满足MATLAB中保存不同大小、不同类型数据的需求。本小节分为九个部分,将详尽探索元胞数组的多方面特性和用法。虽然本节内容与文本处理的相关性不强,但它为理解和运用下一小节将要介绍的字符向量元胞数组提供了必要的基础。通过本节的学习,我们将能够全面掌握元胞数组这个数据类型,并了解其在更广泛的数据处理场景中的应用价值。
在正式讲解元胞数组之前,我们先简单介绍下MATLAB中的数据类型。数据类型是编程语言的核心概念之一,它决定了变量所能存储的数据种类,以及能够对这些数据执行的操作。MATLAB中的数据类型有很多种,常用的数据类型有以下几种:
大家可以复制下面的代码到MATLAB中运行,代码中有许多知识点我们目前还没学过,在后续的章节中会陆续给大家讲解。
- clc;clear
- a = rand(4,5); % 双精度浮点型数值矩阵
- b = 1 + 3i; % 复数标量
- c = a > 0.5; % 逻辑数组
- d = char('abc','def'); % 字符数组
- e = {'a',[2,3]}; % 元胞数组(这一节要讲)
- f = ["abc","defg"]; % 字符串数组(下一节要讲)
- g = struct('a',1); % 结构体(后续章节会讲)
- h = categorical(f); % 分类数组(后续章节会讲)
- i = datetime(2003,08,28); % 日期和时间数据(后续章节会讲)
- j = table; % 表格(后续章节会讲)
- k = @(x) x*2; % 函数句柄(后续章节会讲)
运行结束后,我们可以在工作区查看这些变量的名称、值、大小、类型等信息(如果你只能看到名称和值,可以把鼠标放在名称、值所在的行上,然后单击鼠标右键进行勾选)。
除了在工作区查看各变量的信息外,我们还可以使用whos函数,它能显示工作区各变量的详细信息,包括变量的名称、大小、占用的内存大小和数据的属性。
另外,如果我们只想查看变量的类型,我们可以使用class函数。例如运行class(d)将返回'char',这表明d是一个字符类型。(有其他编程语言基础的同学应该都听过面向对象和面向过程的概念,MATLAB中定义的各种数据类型实际上就是面向对象程序设计语言中定义的类,这里的变量d实际上就是字符数组这个类中的一个对象。我们这门课程以入门知识为主,暂时不会深入探究这方面的内容。未来大家如果想学习MATLAB的工具箱APP设计、进阶的图形绘制等内容,就需要去深入了解相关的知识)
MATLAB中也提供了一系列判断变量是否属于某个数据类型的函数,属于就返回逻辑值1,不属于就返回逻辑值0。例如islogical函数可以判断输入变量是否为逻辑类型,当我们运行islogical(c)时就会返回逻辑值1,如果将输入变量换成a、b、d等其他值会返回逻辑值0。这些函数用法比较简单,完整的函数列表可以参考本章附录3:判断变量数据类型的函数。
此外,MATLAB提供了许多函数可以将变量从一种数据类型转换为另一种数据类型,以满足各种计算和操作的需要。例如我们本章介绍的num2str函数就能将数值类型转换为字符型。考虑到大家目前所学的知识有限,更多转换的函数我们会在后面的章节中陆续介绍,感兴趣的同学可以在MATLAB官网搜索数据类型转换。事实上,MATLAB在某些情况下会自动进行数据类型的转换以便执行计算。例如,当字符与数字进行运算时,MATLAB会自动将字符转换为对应的Unicode编码;逻辑值1或逻辑值0在涉及数值运算时,也会自动转换为数值型1或0。数据类型计算时的自动转换机制会使得代码更加灵活和高效。
在上面的内容中,我们已经对MATLAB中的数据类型进行了初步介绍。掌握如何查看和理解变量的数据类型非常重要,尤其是当我们深入学习元胞数组的操作时。元胞数组的特点在于它可以保存不同类型的数据,因此,对数据类型有一个清晰的认识对于理解和运用元胞数组至关重要。
在第三章中,我们介绍了如何使用中括号[]来创建普通的数值数组。创建元胞数组(cell array)则需要使用英文输入模式下的大括号{}(又称花括号)。在元胞数组中,同行元素之间可以用逗号或空格分隔,而行与行之间则通过分号或回车键分隔。
我们可以在元胞数组中放入任何类型的数据,例如:
上面代码中我们创建了一个3行2列的元胞数组c1:c1的第一行第一列保存的数据是一个长度为3的行向量;第一行第二列保存的数据是一个长度为4的字符向量;第二行第一列保存的数据是3行2列的一个字符矩阵;依次类推。可以看出,元胞数组中保存的数据非常灵活,数据大小和数据类型都没有限制。
另外,大家观察表格右侧的返回结果(注意:低版本的MATLAB看不到),可以看到每个元素都被大括号{}包起来了,比如第一个元素是{[1 2 3]},而不是[1 2 3],这表明MATLAB自动将每个数据保存到了独立的元胞中。那么,为什么MATLAB采用这种表示方式呢?
这一设计主要是基于元胞数组特性:允许我们在同一个数组内存储不同类型和大小的数据。例如,第一个元胞中保存的数据是数值向量 [1 2 3],如果我们随后决定将这个数据替换为一个不同大小的向量或矩阵,甚至替换为一个完全不同类型的数据,元胞数组的这种结构就能够轻松应对这样的变更。
MATLAB将元胞数组的每个元素视为一个独立的元胞,这样就可以灵活地进行修改,而不会影响数组的整体结构。这种设计大大提高了元胞数组的可操作性和灵活性,使得元胞数组成为处理各种不规则数据的理想选择。
我们可以使用cell函数来创建一个指定大小且数据全为空矩阵的元胞数组:
如果我们预先不知道元胞数组中的数据,只知道数组的大小时就能这样做,这样可以为元胞数组预先分配好空间,稍后在计算中再为数据赋值,因此可以提高代码的运行效率。
此外,元胞数组中保存的数据也可以是元胞数组,这被称为嵌套的元胞数组,例如:
c3是一个2行1列的元胞数组,其中第一个数据也是一个元胞数组,这个元胞数组中保存了两个不同类型的数据。(注意:在数值数组中,2行1列的数组通常被称为列向量,但在MATLAB的官方文档中,习惯于将元胞向量和元胞矩阵统称为元胞数组)
在数值矩阵中,我们使用小括号引用所需位置的元素,例如A是一个4行2列的数值矩阵,那么A(3, 2)就能得到A矩阵中第三行第二列位置的元素。
元胞数组有所区别,在MATLAB中,有两种方式来引用元胞数组:使用小括号()和使用大括号{}。这两种引用方式有着不同的用途和效果。
为了演示两种引用方式的区别,我们以下面这个4行2列的元胞数组C为例进行讲解:
1. 使用小括号()引用
当使用小括号()来引用元胞数组时,我们实际上是在引用元胞数组中的元胞,因此小括号引用时返回的是一个元胞数组,而不是元胞中存储的数据。
通过这些示例,我们可以看到使用小括号()引用元胞数组元素非常灵活,这种操作方法和对数值矩阵的元素进行引用时的操作方法完全相同。大家需要要记住的就是:小括号()引用返回的是对应位置的元胞数组,而不是元胞数组中存储的数据。
2. 使用大括号{}引用
使用大括号{}引用元胞数组时,我们可以直接得到对应位置的元胞数组中的数据。还是以元胞数组C为例,我们来看使用大括号{}引用的例子:
上面的例子是使用大括号{}对单个位置的数据进行引用,返回的结果就是对应位置元胞中的数据。如果使用大括号{}引用多个位置的数据,MATLAB 会依次返回这些位置元胞中的数据。例如:
如果我们需要保存所有返回的数据,那么需要给定与元胞数量相同的返回变量:
如果你给定的返回变量的数量小于引用的元胞数量,那么MATLAB只会将前面几个位置的元胞中的数据赋值给对应的变量:
上面代码中,我们使用C{1:2,:}引用了C中前两行元胞中的数据,正常情况下会返回4个数据,然而等号左侧我们只给了两个返回值,因此MATLAB会将右侧元胞中前两个位置的数据分别赋值给x和y,这里的顺序是线性索引的顺序,因此y的值为第2行第1列元胞中的数据,即y等于逻辑值1。
如果返回的每个元胞中的数据可以在水平方向上拼接为一个更大的数组,那么我们可以使用中括号[]将返回的数据括起来,这里中括号[]的作用就是在水平方向上拼接数组元素。
上面代码等价于[C{3,1}, C{3,2}],即[[5 6 7; 8 9 10], [60;70]],因此你也可以使用cat函数或者horzcat函数进行拼接:cat(2,horzcat(C{3,:}))或者horzcat(C{3,:})。
如下图所示,我们可以将元胞数组C比作一个大柜子,柜子中有4行2列共8个抽屉,每个抽屉都是独立的元胞数组, 而每个抽屉中装的物品则代表存储在元胞中的数据。
我们使用小括号()引用实际上取得的是元胞数组中的抽屉,即返回的是元胞数组类型;使用大括号{}引用得到的才是装在抽屉中的数据。
大家一定要深刻理解上图的含义,后续我们讲解对元胞数组进行修改时,也分为两种情况:对抽屉本身(单独的元胞数组)进行修改和对抽屉中保存的数据进行修改。
上面举的例子较为简单,下面我们来看稍微复杂一点的情况:
(1)链式索引
链式索引(chained indexing)是一种高级的数据访问技术,它允许我们在单个表达式中执行多个索引操作。如果元胞数组中包含数组数据,我们就能使用链式索引来访问该数组中的特定元素:先使用大括号来引用元胞中的数组;再使用小括号引用数组中的元素。
显然,链式索引的代码形式非常简洁。如果按照传统的方式,我们需要生成一个中间变量,然后再对中间变量的元素进行引用:
需要注意的是,在MATLAB中,小括号()要放到索引表达式的最后。例如,当你尝试在小括号()的后面跟上大括号{}进行索引时,MATLAB会报错:
(2)嵌套的元胞数组的引用
元胞数组中的数据可以是任意类型,因此它的数据也可以是另一个元胞数组,这被称为嵌套的元胞数组。如果存在嵌套的元胞数组,那么对元胞数组的引用会稍微复杂一点。尤其是当我们想访问内部的元胞数组时,我们需要借助多级链式索引来访问更深层次的数据。
在最后两个示例中,多级链式索引的方法展现出了其精确提取嵌套元胞数组中特定位置数据的能力。再次强调,使用MATLAB的链式索引时,我们必须遵循一项重要的语法规则:在构建索引表达式时,如果需要使用小括号(),它需要被放置在索引表达式的末尾。这种语法规则可能与其他编程语言中的链式索引实现方式存在差异。然而,一旦掌握了这种链式索引技术,我们将能够在数据分析和处理过程中更加游刃有余。
(3)套用 cat、char等函数对元胞数组中的数据进行拼接
在处理元胞数组数据时,我们经常需要将独立的元胞中的数据拼接成一个统一的数组。MATLAB提供了许多函数能帮助我们拼接数组的元素,例如我们讲过的cat和char函数。下面来看一个例子,假设我们有一个班级四名学生的姓名以及他们在语数外三门科目的成绩:
在元胞数组 student 中,每行的第一个位置的数据是保存学生姓名的字符向量,第二个位置的数据是一个包含三门科目成绩的数值向量。如果我们想要提取所有学生的名字,形成一个字符矩阵,我们可以使用 char 函数:
student{:,1}表示取出元胞数组student 第一列中所有元胞内保存的姓名数据,接下来我们套用char函数,就能将这些学生的姓名拼接到同一个字符矩阵中,MATLAB会自动在姓名的后面添加空格来保证各行的长度相同。
如果我们直接使用小括号 () 提取第一列的元素,那么返回的是一个元胞数组,这个元胞数组中所有的数据类型均为字符向量,因此它也被称为字符向量元胞数组。在下一小节中,我们会详细讲解如何使用字符向量元胞数组来处理文本数据。
接下来,如果我们想把学生的成绩合并成一个矩阵,我们可以使用 cat 函数:
通过类似的方法,我们可以有效地对元胞数组中的数据进行拼接,为进一步的数据分析打下基础。
(4)celldisp函数展示元胞数组的数据
上一章中我们介绍过disp函数,它可以在窗口(实时编辑器的窗口或命令行窗口)上显示变量的值。然而,disp函数对元胞数组的作用非常有限,它只能帮助我们快速了解元胞数组的整体结构。当我们需要详细查看每个元胞内的数据时,需要借助celldisp函数,它能提供更详细的信息。如果存在嵌套的元胞数组,它还会显示内部的元胞数组中每个元胞保存的数据。(注:另一个函数cellplot能以图形形式展示数据,但看起来不美观,大家可以自己测试)
从上面例子可以看出,celldisp 函数会按照线性索引的顺序逐个展开元胞数组。使用celldisp 函数能使我们更详细地查看元胞数组的内容,进而为后续的数据分析工作提供方便。
在数据分析任务中,有时候我们需要将不同的元胞数组拼接成为一个更大的元胞数组。MATLAB中提供了两种不同的方法来拼接元胞数组:使用中括号[]和使用大括号{}。两者拼接的结果有很大区别,我们先来看使用中括号[]拼接的例子:
可以看出,使用中括号[]对元胞数组进行拼接时,实际上是对元胞数组中的元素进行拼接,这种操作方式和对数值矩阵的拼接方式完全相同。
需要注意的是:使用中括号[]进行横向拼接时,要求参与拼接的元胞数组的行数相同;进行纵向拼接时,要求参与拼接的元胞数组的列数相同。
下面再来看使用大括号{}拼接的例子:
可以看出,使用大括号{}拼接时,我们会得到一个嵌套的元胞数组,这个大的元胞数组中的数据就是参与拼接的原始元胞数组,即将原始元胞数组整体视为独立的元素。
因此,使用大括号{}拼接对参与拼接的原始元胞数组的大小没有要求。实际上,这里大括号的作用就相当于创建了一个嵌套的元胞数组。
前文我们介绍过,可以将整个元胞数组类比为带有抽屉的柜子,每个抽屉都是独立的元胞数组,而每个抽屉中装的物品则代表存储在元胞中的数据。现在,我们将进一步学习如何修改元胞数组,这分为了两种情况:修改"抽屉"和修改"抽屉"中的数据。
(1)使用小括号()修改元胞数组的"抽屉"
当我们使用小括号()来引用元胞数组时,我们实际上是在操作整个"抽屉"。这意味着我们可以更换"抽屉",但是替换时必须使用另一个"抽屉",即更换为另一个元胞数组。例如:
上面的代码将第2行第1列的"抽屉"{'pear'}替换成了{'watermelon'}。注意,我们使用了一个元胞数组来进行替换,这样保持了"抽屉"的一致性,如果你忘记了加大括号{}就会报错:
此外,如果我们修改时指定的索引范围超出了元胞数组的大小,那么MATLAB会自动对元胞数组进行扩充,没有被指定的位置对应的数据会被填充为空向量[]:
此外,对元胞数组使用小括号()修改时,赋值等号的左右两侧元胞数组的大小不必完全一样,只需要大小兼容即可:
(2)使用大括号{}修改"抽屉"中的数据
当我们使用大括号{}时,我们是在修改"抽屉"中的具体物品,即元胞数组中保存的数据。
如果我们将赋值等号右侧的元素加上大括号,MATLAB也不会报错。它会认为我们要将这个数据替换成一个新的元胞数组,此时返回的结果就是一个嵌套的元胞数组。
从上面的例子可以看出:使用大括号{}对数据进行修改时,对新数据的类型并没有要求,这也正是元胞数组储存数据的优势所在。我们再来举个例子:
另外,我们也可以通过链式索引的方式对元胞数组中的数据进行修改,例如:
易错点:使用大括号{}修改数据时,MATLAB不支持使用简单的赋值语句对两个或两个以上的数据进行修改。例如我们想将元胞数组C中的第一列的数据全部修改为'xyz':
报错中出现了一个概念:“逗号分隔的列表”,我们稍后再来介绍。
事实上,要对两个或两个以上的数据进行修改,我们也可以使用小括号()来引用元胞数组,此时赋值等号的右侧需要是元胞数组类型,因此我们将这个数据加上大括号{}:
那么,逗号分隔的列表代表什么呢?逗号分隔的列表可以理解为一系列使用逗号分隔的数字、表达式或者变量。例如:
再比如,当我们使用大括号{}从一个元胞数组中引用多个数据时,MATLAB 返回的实际上就是一个逗号分隔列表:
MATLAB的报错信息中提示我们:“请考虑使用以逗号分隔的列表赋值”。这个操作需要用到MATLAB的内置函数:deal函数,它的作用是将输入变量分发给输出变量。
下面我们来看deal函数的例子:
通过上面两个例子可以看出,要同时对多个变量进行赋值,我们可以借助deal函数,它可以减少我们的代码量。因此,要使用大括号{}修改数据并将元胞数组C的第一列数据全部修改成'xyz'时,代码应改成:
赋值等号左侧的C{:,1}需要使用中括号[]括起来,[]里面的元素实际上就是deal函数的输出值(返回值),此处的[C{:,1}]可以看成是[C{1,1}, C{2,1}]。
下面来看一道综合性很强的题目:随机生成8名学生,并将他们的信息储存在一个大小为8行3列的元胞数组S中。其中,S的第一列是他们的姓名,假设他们的姓名由4个随机英文字母生成,首字母大写;S的第二列是他们语数外三门科目的成绩,你可以使用长度为3的数值向量保存成绩,假设成绩是区间[0,100]上的随机整数;S的第三列是他们三门科目的总分。
假设我们想将S中低于60分的成绩全部改成60分,并重新计算总分,代码应该怎样写?
请在S中添加一列,用来表示学生的等级:总分超过240分时,等级为'A';总分位于200-239时,等级为'B';总分低于200时,等级为'C'。
进一步地,请从元胞数组S中筛选出等级为'A'的学生,并将结果保存到元胞数组SS中。
在上面这段代码中,我们开始时创建一个空的元胞数组SS,它被用来存储那些等级为'A'的学生的信息。接下来,我们通过for循环来遍历S中的每一行,即遍历每个学生的信息。在循环的每一次迭代中,我们使用strcmp函数检查学生的等级是否为'A'。如果是,就将该学生的完整信息添加到SS数组的末尾。值得注意的是,在向SS数组添加新行时,我们使用了end+1作为行索引。这种语法大家可能是首次见到,它是一个非常实用的方法来动态地增加数组的大小,使用end+1可以确保新数据总是被添加到数组的末尾。这种方法既简洁又有效,是扩展数组尺寸的常用技巧。在循环完成之后,SS数组就包含了所有等级为'A'的学生信息。这样的处理不仅提高了代码的可读性,也使得数据筛选过程更加直观和高效。
进一步思考:能否不使用循环语句来解决上面这个问题?下面我们来看另一种思路:
下面来看最后一个问题,请基于成绩总分对元胞数组SS中的学生信息进行降序排列。
当然,如果你忘了sortrows这个函数,你也可以使用sort函数结合一些额外的步骤来实现相同的功能。
我们还是将元胞数组比作带有抽屉的柜子,每个抽屉都是独立的元胞数组,而每个抽屉中装的物品则代表存储在元胞中的数据。因此,删除元胞数组有两层含义:删除"抽屉"(元胞数组)和删除"抽屉"中的物品(数据)。
(1)使用小括号()删除元胞数组的"抽屉"
借助小括号(),我们可以将整个"抽屉"从数组中移除。例如,如果我们想删除元胞数组C的第二行元素,可以简单地将其赋值为空向量[]来实现删除:
(2)使用大括号{}删除"抽屉"中的数据
使用大括号{},我们可以删除"抽屉"中保存的具体数据,但这并不会移除"抽屉"本身,仅仅是将"抽屉"内的数据清空。例如我们想删除C中第二行第一列元胞中的数据:
对比小括号删除的结果不难看出,使用大括号删除后不会改变原始元胞数组的大小,删除的仅仅是指定位置的数据。
另外,如果要使用大括号{}删除两个或两个以上的数据,需要借助deal函数,直接删除会出现错误。deal函数的用法在上一小节修改元胞数组中介绍的非常详细。
前面几个小节中,我们分别介绍了元胞数组的创建、引用、拼接、修改和删除操作,下面我们来介绍元胞数组的运算。
和普通的数值数组不同,绝大多数的运算方法都不适用于元胞数组,例如加减乘除等算术运算、大于小于等关系运算、调用数学函数运算等。
返回数组大小的三个函数:size、numel和length对元胞数组仍然有效:
还有几个函数比较特殊,那就是第三章集合运算中介绍的六个函数:unique(返回数组的唯一值)、ismember(判断一个数组的元素是否在另一个数组内)、intersect(交集)、union(并集)、setdiff(差集)和setxor(对称差集)。它们只能用于字符向量元胞数组,即元胞数组中的数据全为字符向量时才可以使用。
有时候我们需要将元胞数组和其他数据类型进行转换,下面我们介绍三个常用的转换函数:num2cell、mat2cell和cell2mat :
下面我们依次介绍这三个函数的具体使用方法。
(1)num2cell函数
num2cell函数可以将数组转换为元胞数组,它有两种用法:
用法一:C = num2cell(A) 通过将 A 的每个元素放置于 C 的一个单独元胞中,来将数组 A 转换为元胞数组 C。
用法二:C = num2cell(A,dim) 将 A 的内容划分成 C 中单独的元胞,其中 dim 表示维度。dim等于1表示沿着行方向划分,dim等于2表示沿着列方向划分。
下面我们结合生活实例来讲解num2cell函数的应用。假设你是你们班的老师,需要管理学生在不同科目上的成绩。这些成绩被存储在数值矩阵score中,其中每一行代表一名学生,每一列代表一个不同的科目。现在,你想要将这个矩阵转换为元胞数组,并在元胞数组中添加上学生的姓名,这样能方便你为每名学生制定不同的学习计划。
(2)mat2cell函数
mat2cell函数是num2cell函数的进阶版,它允许我们将一个数组分割为多个大小不同的子块,并将这些子块存储在元胞数组中。这一特性在分割矩阵时尤为有用,特别是当我们需要单独操作矩阵的特定部分时。下面我们通过一个实例来展示mat2cell函数的最常用用法。
如下图所示, A 是一个大小为 6行5列的数组,我们想将其分割为六个子块:
我们来看代码:
mat2cell函数的第一个输入参数A就是我们要分割的大的数组;第二个输入参数r表示我们在行方向上分割的分布情况,上面代码中,r等于[3 2 1],它有三个元素,那么我们最终得到的元胞数组就有三行:第一行的子块矩阵有3行元素,第二行的子块矩阵有2行元素,第三行的子块矩阵有1行元素;第三个输入参数c表示我们在列方向上分割的分布情况,上面代码中c等于[1 4],它有两个元素,那么我们最终得到的元胞数组就有两列:其中第一列的子块矩阵有1列元素,第二列的子块矩阵有4列元素。因此,根据划分的关系,对r中的元素求和一定等于A的行数;对c中的元素求和一定等于A的列数。
mat2cell函数可以实现num2cell的功能:
因此,和num2cell函数相比,mat2cell函数更加灵活,它可以处理更为复杂的数组分割任务。下面我们来看一个数独的例子:
数独是一个9×9的方阵,它由九个宫格构成,每个宫格又由九个小格子构成(图中用颜色区分开的3×3的方阵)。请验证下面这个数独的盘面是否满足以下三点要求:
(1)每列包含1到9的不重复数字;(2)每行包含1到9的不重复数字;(3)每个宫格内包含1到9的不重复数字。
- sd = [1 9 4 3 8 5 7 2 6;
- 8 3 2 7 6 9 4 5 1;
- 6 5 7 4 2 1 9 3 8;
- 2 6 9 8 3 7 5 1 4;
- 5 8 3 1 9 4 6 7 2;
- 4 7 1 2 5 6 3 8 9;
- 9 1 5 6 7 2 8 4 3;
- 3 2 6 5 4 8 1 9 7;
- 7 4 8 9 1 3 2 6 5];
- % 每列是否为1到9的不重复数
- Condition_1 = all(all(sort(sd,1) == (1:9)'))
- % 每行是否为1到9的不重复数
- Condition_2 = all(all(sort(sd,2) == 1:9))
- % 每个九宫格是否为1到9的不重复数
- cc = mat2cell(sd,[3,3,3],[3,3,3]);
- Condition_3 = true;
- for ii = 1:9
- tmp = cc{ii}; % 第ii个宫格对应的3×3的方阵
- if ~all(sort(tmp(:)) == (1:9)')
- Condition_3 = false;
- break
- end
- end
- Condition_3

代码的思路如下:
验证每列的数字:首先,我们检查每列是否包含1到9的不重复数字。sort(sd,1)表示沿着行方向对sd的每一列进行排序,如果每一列排序后都是1到9的序列,sort(sd,1) == (1:9)'就会返回一个9行9列全为逻辑值1的方阵,此时使用两次all函数可以得到Condition_1为true。
验证每行的数字:其次,我们检查每行是否包含1到9的不重复数字。这类似于列的验证,但方向不同。我们使用sort(sd,2) 沿着列方向对sd的每一行进行排序,如果每一行排序后都是1到9的序列,那么Condition_2就为true。
验证每个宫格的数字:最复杂的部分就是检查每个宫格。这里mat2cell函数发挥了重要作用。我们使用mat2cell(sd,[3,3,3],[3,3,3])将数独方阵分割成九个宫格,每个宫格都是3×3的方阵,并存储在元胞数组cc中。然后,我们使用循环遍历这九个宫格,检查每个宫格是否包含1到9的不重复数字。Condition_3存储了这个条件的验证结果。
(3)cell2mat函数
cell2mat函数是mat2cell函数的逆操作,它主要用于将元胞数组转换为普通的数组。它的使用方法非常简单:
A = cell2mat(C)将元胞数组转换为普通数组。元胞数组的元素必须全都包括相同的数据类型,并且生成的数组也是该数据类型。
在处理元胞数组时,我们经常需要对数组中保存的每个数据应用相同的函数进行计算。常规的做法是使用for循环,但这并不是最方便的做法。让我们通过一个实例来看看如何改进这个过程。
假设我们有一个4行1列的元胞数组C,它记录了四名运动员今年参加的100米短跑比赛中的成绩。每一行数据都是一个向量,表示该名运动员跑100米的时间(单位秒)。我们的目标是计算这四名运动员今年比赛取得的最好成绩。使用传统的for循环方法,代码如下所示:
在上面的代码中,我们使用了for循环,在循环体内,我们通过求最小值的min函数来计算每名运动员的最好成绩。虽然这种方法是有效的,但MATLAB提供了一个更加优雅且更为灵活的方式来解决相同的问题:cellfun函数。
cellfun函数的基本用法是A = cellfun(func, C)。这里的func是一个函数句柄,表示我们希望应用的函数, C是我们要计算的元胞数组。cellfun函数会遍历C中的每个元胞,并将每个元胞中的数据作为参数传递给func。然后,它会收集func的返回值,将这些值串联起来,形成一个新的数组A。对于上面的例子,使用cellfun的代码为:best = cellfun(@min,C)。
注意:这里出现了一个新的概念“函数句柄”。在MATLAB中,函数句柄是一种引用函数的方式。它允许我们将函数作为参数传递给其他函数,或者将函数赋值给变量。函数句柄可以通过在函数名前面加上@符号来创建,例如@min就是min函数的函数句柄。在后续章节,我们会专门讲解如何创建自定义函数,到时候会详细讲解相关的知识。
回到上面的例子,如果我们希望计算这四名运动员的平均成绩,而不是最好成绩,我们可以通过更换函数句柄来轻松地实现这一要求:
在上面的代码中,我们使用cellfun函数结合@min和@mean函数句柄来分别计算四名运动员的最好成绩和平均成绩。这个例子展示了cellfun函数的灵活性和实用性,通过简单地更换函数句柄,我们能够对同一个元胞数组执行不同的操作。
需要注意的是,当我们在cellfun函数中使用的函数句柄返回的不是标量时(即把这个函数句柄应用到元胞中的数据时会返回多个元素的结果),我们需要对cellfun的使用方式作出相应的调整。例如,如果我们想对运动员的成绩进行排序,我们可以使用sort函数。然而,sort函数返回的是一个数组,而不是一个标量。在这种情况下,我们可以通过设置cellfun函数的'UniformOutput'参数为false来保存非标量返回值。
当'UniformOutput'设置为false时,cellfun会将每次函数返回的结果存储在元胞数组中,而不是尝试将它们组合成一个常规数组。
下面是使用cellfun函数和sort函数对运动员成绩排序的例子:
在这个例子中,我们使用了@sort函数句柄和'UniformOutput', false选项将每名运动员的成绩进行了排序,排序结果被存储在元胞数组sorted_scores中。
拓展:cellfun函数的运行效率高于for循环吗?
大家可以搜索cellfun函数相关的文章,有许多文章会加上这个结论:当处理大量数据时,cellfun函数的运行效率高于for循环。但是这个结论真的可靠吗?为了验证这个说法,我们可以人为构造实例来进行测试。
首先,我们生成一个包含大量数据的元胞数组C。在这个例子中,我们将C中包含的数据数量设置为10万,每个数据都是一个长度为100的随机向量:
显然,这是一个相当大的数据量。接下来,我们将比较使用mean函数作为函数句柄和使用传统for循环计算各个数据的平均值所需的时间。
我们可以在最后验证两种方法的计算结果是否相同:
这个测试结果表明,在处理这个特定的问题时,传统的for循环的运行速度快于cellfun函数。那么,如果我们换一个问题结论会有变化吗?例如我们将计算平均值改为排序:
在这个例子中,两种方法的差异并不明显。因此这提示我们,在实际应用中,选择哪种方法应根据具体任务来决定。通常,当数据量不大时,两种方法都不会花费太长时间,此时使用cellfun函数会让代码更加简单,也更为灵活。(拓展:测试中发现,上方同样的代码在MATLAB2023版本中的运行速度要比2017版本更快,这可能和新版本的算法优化有关)
注意,如果你想验证两种方法的计算结果是否相同,不能使用关系运算符==,元胞数组无法进行关系运算:
这里教大家另一个函数:isequal函数。isequal函数的基本语法为:tf = isequal(A,B),如果A和B等效,则 tf等于逻辑值 1 (true);否则tf等于逻辑值 0 (false)。
这里所说的“等效”要比“完全相同”包含的情况更多,例如字符 'A' 的ASCII码为65,那么MATLAB会认为 'A' 和65等效;再比如逻辑值1(true)和数值1也是等效的。
当然,对于一般的数值矩阵,“等效”和“完全相同”的意义一样,只有当数值矩阵的大小和对应位置的数值元素全部相同时,isequal函数才会返回逻辑值1。
对于元胞数组,只有两个元胞数组的大小相同,且对应位置的元素等效时,isequal函数才会返回逻辑值1。因此,要验证sort1和sort2两个元胞数组相同,可以使用isequal函数。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。