这一章在干嘛?
前 7 章你接触的所有变量都是「单一类型」的数值数组 —— 学生成绩、信号采样、温度读数,它们的逻辑身份都一样。但现实场景里,一个变量经常要装不同类型的数据(比如一份学生档案要装名字字符串、ID 数字、四次小测成绩向量)。这一章教你两个 MATLAB 杀手锏:元胞数组 cell array 和结构体 struct。前者按「位置」索引(像 Python 列表),后者按「字段名」访问(像 C 的
struct或 Python 的 dict)。学完后你就能把真实业务里的复杂记录(学生档案、商品清单、实验数据)装进 MATLAB 变量。
8.1 元胞数组 cell array
元胞数组(cell array)是一种可以装任意类型数据的容器:整数、字符、向量、字符串甚至另一个元胞数组都可以塞进同一个变量。它在 MATLAB 里地位特殊 —— 是少数能让你”打破矩阵元素必须同类型”的工具。
如果你熟悉 C 语言,可以这样类比:cell array ≈ 「指针数组」 —— 真正数据放在堆里,cell 里存的是指向数据的指针。这就是为什么它能装不同类型、不同大小的元素。
8.1.1 创建元胞数组
用花括号 {} 而不是方括号 [] 创建:
>> cellrowvec = {23, 'a', 1:2:9, 'hello'}
cellrowvec =
1×4 cell 数组
{[23]} {'a'} {1×5 double} {'hello'}四种不同类型(整数、字符、数值向量、字符串)共生在一个 1×4 cell 里 —— 数值矩阵绝无可能做到。
换成列向量用分号分隔:
>> cellcolvec = {23; 'a'; 1:2:9; 'hello'}
cellcolvec =
4×1 cell 数组
{[ 23]}
{'a' }
{1×5 double}
{'hello' }也支持 2 维矩阵:
>> cellmat = {23, 'a'; 1:2:9, 'hello'}
cellmat =
2×2 cell 数组
{[23]} {'a' }
{1×5 double} {'hello' }类型用 class 一查:
>> class(cellmat)
ans =
cell预分配(避免循环里反复扩容拖慢速度)用 cell 函数:
>> mycellmat = cell(2,2) % 2x2 全空 cell,元素是 0×0 占位
mycellmat =
2×2 cell 数组
{0×0 double} {0×0 double}
{0×0 double} {0×0 double}后面再逐元素赋值(参考 8.1.2)。
8.1.2 访问元胞:花括号 vs 圆括号 —— 史上最关键的区分
史上最关键的语法区分
cellmat(i)用圆括号 → 拿到「第 i 个格子」本身(还是一个 1×1 cell)cellmat{i}用花括号 → 拿到「第 i 个格子里装的内容」(原始数据)搞混这俩是初学者最常踩的雷。圆括号是「装盒子的盒子」,花括号是「盒子里装的东西」。
>> cellrowvec{2} % 花括号:内容(字符)
ans =
'a'
>> class(ans)
ans =
char
>> cellrowvec(2) % 圆括号:格子本身
ans =
1×1 cell 数组
{'a'}
>> class(ans)
ans =
cell二维访问同理:
>> cellmat{1,1} % 内容:数字 23
ans =
23
>> cellmat(1,1) % 格子:1x1 cell
ans =
1×1 cell 数组
{[23]}链路访问:cell 里装的是向量,想拿向量里第 4 个元素:
>> cellmat{2,1}(4) % 第 1 层花括号取内容,第 2 层括号取向量元素
ans =
7 % 1:2:9 的第 4 个元素批量取多个元素:
>> cellcolvec{2:3} % 取多个内容 → 逗号分隔列表
ans =
'a'
ans =
1 3 5 7 9
% 想存进多个变量:左右都用逗号分隔
>> [c1, c2] = cellcolvec{2:3}
c1 = 'a'
c2 = [1 3 5 7 9]
% 用圆括号取多个 → 新的 cell array
>> cellcolvec(2:3)
ans =
1×2 cell 数组
{'a'} {1×5 double}8.1.3 删除元素、显示、维度
删除只能用圆括号:
>> cellrowvec(2) = [] % 把第 2 个格子整个删掉
cellrowvec =
1×3 cell 数组
{[23]} {1×5 double} {'hello'}二维情况可以删整行/整列:
>> cellmat(1,:) = []
cellmat =
1×2 cell 数组
{1×5 double} {'hello'}显示有三种方式:
>> cellrowvec = {23, 'a', 1:2:9, 'hello'};
% 直接打变量名 → 简略摘要
>> cellrowvec
cellrowvec =
1×4 cell 数组
{[23]} {'a'} {1×5 double} {'hello'}
% celldisp → 展开所有内容
>> celldisp(cellrowvec)
cellrowvec{1} =
23
cellrowvec{2} =
a
cellrowvec{3} =
1 3 5 7 9
cellrowvec{4} =
hello
% cellplot → 图形化展示(high-level 视图,仅看结构)
>> cellplot(cellrowvec)常规维度函数都能用:
>> length(cellrowvec) % 元素个数
ans = 4
>> size(cellcolvec) % 行、列
ans =
4 1
>> cellrowvec{end} % 最后一个元素(花括号取内容)
ans = 'hello'8.1.4 元胞数组 vs 字符矩阵:装”长度不一的字符串”
元胞数组最经典的应用是装长度不一的字符串集合。回看第 7 章 char(...) 会自动用空格补齐短字符串:
>> char('Sue', 'Cathy', 'Xavier') % 第 1 行 'Sue ' 被补到 6 个字符
ans =
Sue
Cathy
Xavier元胞数组就不会补齐:
>> names = {'Sue', 'Cathy', 'Xavier'} % 三个字符串原样保存
names =
1×3 cell 数组
{'Sue'} {'Cathy'} {'Xavier'}
% 遍历统计每个字符串的长度
>> for i = 1:length(names)
disp(length(names{i}))
end
3
5
6互相转换:
% 字符矩阵 → cell array(自动 trim 尾部补的空格)
>> greetmat = char('Hello', 'Goodbye');
>> cellgreets = cellstr(greetmat)
cellgreets =
2×1 cell 数组
{'Hello' }
{'Goodbye'}
% cell array → 字符矩阵(又会补空格)
>> char(cellgreets)
ans =
Hello
Goodbye新工具(字符串数组):R2016b 之后 MATLAB 又推出 string 类型(双引号 "..."),但和 cell array 不是一回事。本章不展开,记住「装不等长字符串,用 cell array(更老但更通用)」就够。
8.1.5 字符串拼接与分割(cell 视角)
第 7 章学的字符串函数在 cell 上也有专门版本:
>> names = {'Sue', 'Cathy', 'Xavier'};
>> strjoin(names) % 默认空格拼接
ans =
'Sue Cathy Xavier'
>> strjoin({'August', '14', '2016'}, '-')
ans =
'August-14-2016'
>> strsplit('August-14-2016', '-') % 字符串 → cell
ans =
1×3 cell 数组
{'August'} {'14'} {'2016'}
% 判断整个 cell array 是不是「全是字符串」
>> iscellstr(names)
ans =
1
>> iscellstr({1, 'a'}) % 装了非字符串
ans =
08.2 结构体 struct
结构体(struct)和 cell array 一样能装异类数据,但它的访问方式是按字段名而不是按位置。
类比:
cell array≈ Pythonlist,按[0],[1],[2]访问struct≈ Pythondict或 C 的struct,按.字段名访问
什么时候用哪个?教材的判据很清晰:
Use cell arrays when it is desired to loop through the values or to vectorize the code. Use structures when it is desired to use names for the different values rather than indices.
8.2.1 创建与修改结构体
假设一家软件商店要存每个商品的信息:编号、成本、售价、类型码。
% 方法 1:struct 函数(最高效,一行搞定)
>> package = struct('item_no', 123, 'cost', 19.99, ...
'price', 39.95, 'code', 'g')
package =
包含以下字段的 struct:
item_no: 123
cost: 19.9900
price: 39.9500
code: 'g'% 方法 2:点运算符逐字段赋值(适合边写边加字段,但不高效)
>> package.item_no = 123;
>> package.cost = 19.99;
>> package.price = 39.95;
>> package.code = 'g';类比 C:struct('field', value, ...) ≈ C 里 MyStruct s = { .field = value };点运算符 package.cost ≈ C 里 s.cost。
查看与显示:
>> class(package)
ans =
struct % 类型是 struct
>> disp(package) % 打印整个结构体
item_no: 123
cost: 19.9900
price: 39.9500
code: 'g'
>> disp(package.cost) % 打印单个字段
19.9900
% fprintf 只能打印单个字段(不能 fprintf(package)!)
>> fprintf('%d %c\n', package.item_no, package.code)
123 g复制与修改:结构体变量赋值是拷贝整个结构体,改新变量不影响旧的:
>> newpack = package; % 整个结构体拷贝
>> newpack.item_no = 111;
>> newpack.price = 34.95
newpack =
item_no: 111
cost: 19.9900
price: 34.9500
code: 'g'删除字段用 rmfield(不修改原变量,返回新结构体):
>> rmfield(newpack, 'code') % 不赋值给 newpack,原 newpack 不变
ans =
item_no: 111
cost: 19.9900
price: 34.9500
>> newpack = rmfield(newpack, 'code')% 显式赋值才生效
newpack =
item_no: 111
cost: 19.9900
price: 34.95008.2.2 把结构体传给函数
两种风格:
% 风格 A:把整个 struct 传进去
function profit = calcprof(packstruct)
profit = packstruct.price - packstruct.cost;
end
>> calcprof(package)
ans = 19.9600
% 风格 B:只传需要的字段(两个标量)
function profit = calcprof2(oneprice, onecost)
profit = oneprice - onecost;
end
>> calcprof2(package.price, package.cost)
ans = 19.9600风格 A 适合字段经常增减的场景(调用者改一下就行),风格 B 适合只关心几个值的纯计算函数。
8.2.3 结构体相关函数
>> isstruct(package)
ans = 1
>> isfield(package, 'cost') % 字段是否存在
ans = 1
>> isfield(package, 'profit')
ans = 0
>> pack_fields = fieldnames(package) % 返回字段名(cell array)
pack_fields =
4×1 cell 数组
{'item_no'}
{'cost' }
{'price' }
{'code' }因为字段名是字符串(长度不一),所以 fieldnames 自然返回 cell array。
动态字段名(dynamic field names)—— 字段名可以是变量:
% 静态
>> package.cost
ans = 19.9900
% 动态:字段名是字符串变量
>> fname = 'cost';
>> package.(fname)
ans = 19.9900
% 配合 isfield 做"查字段"功能
>> inputfield = input('Which field would you like to see: ', 's');
Which field would you like to see: cost
>> if isfield(package, inputfield)
fprintf('The value of the %s field is: ', inputfield);
disp(package.(inputfield))
else
fprintf('Error: %s is not a valid field\n', inputfield)
end
The value of the cost field is: 19.99动态字段名 + sprintf = 反射式访问
把字段名当字符串变量处理,相当于其他语言里的「反射」。在 8.4 节的通用排序函数
generalPackSort里就能看到它的大招。
8.2.4 结构体向量:database 写法
真实业务里,结构体通常是多个的集合,叫 结构体向量(vector of structures)。这是 MATLAB 里”小型数据库”的标准范式。
商品列表 packages 是个 1×3 的结构体向量,每个元素是个含四个字段的结构体:
% 方法 1:先建第 1 个,再追加(不推荐:会动态分配)
>> packages = struct('item_no', 123, 'cost', 19.99, 'price', 39.95, 'code', 'g');
>> packages(2) = struct('item_no', 456, 'cost', 5.99, 'price', 49.99, 'code', 'l');
>> packages(3) = struct('item_no', 587, 'cost', 11.11, 'price', 33.33, 'code', 'w');
% 方法 2:先建最后一个(预分配),再补前面(推荐)
>> packages(3) = struct('item_no', 587, 'cost', 11.11, 'price', 33.33, 'code', 'w');
>> packages(1) = struct('item_no', 123, 'cost', 19.99, 'price', 39.95, 'code', 'g');
>> packages(2) = struct('item_no', 456, 'cost', 5.99, 'price', 49.99, 'code', 'l');
% 方法 3:先空 struct + repmat 复制
>> packages = repmat(struct('item_no', 123, 'cost', 19.99, ...
'price', 39.95, 'code', 'g'), 1, 3);
>> packages(2) = struct('item_no', 456, 'cost', 5.99, 'price', 49.99, 'code', 'l');
>> packages(3) = struct('item_no', 587, 'cost', 11.11, 'price', 33.33, 'code', 'w');性能警告
方法 1(不停追加)是教材明确不推荐的写法 —— 每次赋值都触发数组扩容,大数据量下极慢。任何 vector 都应该先预分配大小再填值,这是 MATLAB 的硬性性能规则。
直接打变量名只看得到「这是个 1×3 struct array,字段有 item_no、cost、price、code」:
>> packages
packages =
1×3 struct 数组,包含字段:
item_no
cost
price
code要看到具体值,必须下钻(多层访问):
>> packages(2) % 看第 2 个元素的 4 个字段
ans =
item_no: 456
cost: 5.9900
price: 49.9900
code: 'l'
>> packages(1).code % 只看第 1 个的 code 字段
ans =
'g'遍历所有元素的某个字段有两种风格:
% 编程方法(普写)
>> for i = 1:length(packages)
fprintf('%f\n', packages(i).cost)
end
19.990000
5.990000
11.110000
% MATLAB 高效方法(不写循环)
>> fprintf('%f\n', packages.cost) % 等价但更快
19.990000
5.990000
11.110000
packages.cost这种「点运算符不带索引」的写法这是 MATLAB 的一个魔法:点运算符用在 vector 上时,自动生成逗号分隔列表,让
fprintf之类能逐个处理。这就是为什么”用 struct 也能向量化”的精髓。
把整列字段收集成向量:
>> [packages.cost] % 用方括号收集所有 cost 字段
ans =
19.9900 5.9900 11.1100
>> sum([packages.cost]) % 求所有商品的成本
ans =
37.0900实用打印函数:
function printpackages(packstruct)
fprintf('\nItem # Cost Price Code\n\n')
for i = 1:length(packstruct)
fprintf('%6d %6.2f %6.2f %3c\n', ...
packstruct(i).item_no, ...
packstruct(i).cost, ...
packstruct(i).price, ...
packstruct(i).code)
end
end调用:
>> printpackages(packages)
Item # Cost Price Code
123 19.99 39.95 g
456 5.99 49.99 l
587 11.11 33.33 w
8.2.5 字段本身是向量的 struct:学生档案
如果某个字段本身也是向量,结构体能表达”一个学生有 4 次小测成绩”这种层级:
% 倒序预分配(先建第 3 个)
>> student(3) = struct('name', 'Brownnose, Violet', ...
'id_no', 332, ...
'quiz', [7.5 6 8.5 7.5]);
>> student(1) = struct('name', 'C, Joe', ...
'id_no', 999, ...
'quiz', [10 9.5 0 10]);
>> student(2) = struct('name', 'Hernandez, Pete', ...
'id_no', 784, ...
'quiz', [10 10 9 10]);这个数据结构有三层语义:
| 表达式 | 含义 |
|---|---|
student | 整个 struct 向量 |
student(1) | 第一个学生(一个 struct) |
student(1).quiz | 第一个学生的小测成绩(一个数值向量) |
student(1).quiz(2) | 第一个学生第 2 次小测成绩(一个 double) |
student(3).name(1) | 第三个学生姓名的第一个字符 'B' |
打印每个学生平均分:
function printAves(student)
fprintf('%-20s %-10s\n', 'Name', 'Average')
for i = 1:length(student)
qsum = sum([student(i).quiz]);
no_quizzes = length(student(i).quiz);
ave = qsum / no_quizzes;
fprintf('%-20s %.1f\n', student(i).name, ave)
end
end>> printAves(student)
Name Average
C, Joe 7.4
Hernandez, Pete 9.8
Brownnose, Violet 7.4
8.2.6 嵌套结构体:struct 里装 struct
更进一步:某个字段本身也是 struct,叫嵌套结构体(nested structure)。例:线段有两个端点,每个端点是 (x, y) 坐标。
% 最简洁:嵌套调用 struct
>> lineseg = struct('endpoint1', struct('x', 2, 'y', 4), ...
'endpoint2', struct('x', 1, 'y', 6))
lineseg =
包含以下字段的 struct:
endpoint1: [1×1 struct]
endpoint2: [1×1 struct]
>> lineseg.endpoint1 % 看第一个端点
ans =
x: 2
y: 4
>> lineseg.endpoint1.x % 看 x 坐标(点运算符两次)
ans =
2实战:向量 + 嵌套 struct 描述一组圆柱体
>> cyls(3) = struct('code', 'c', ...
'dimensions', struct('rad', 3, 'height', 6), ...
'weight', 9);
>> cyls(1) = struct('code', 'x', ...
'dimensions', struct('rad', 3, 'height', 6), ...
'weight', 7);
>> cyls(2) = struct('code', 'a', ...
'dimensions', struct('rad', 4, 'height', 2), ...
'weight', 5);打印每个圆柱的体积 :
function printcylvols(cyls)
for i = 1:length(cyls)
vol = cylvol(cyls(i).dimensions);
fprintf('Cylinder %c has a volume of %.1f in^3\n', ...
cyls(i).code, vol)
end
end
function cvol = cylvol(dims)
cvol = pi * dims.rad^2 * dims.height;
end>> printcylvols(cyls)
Cylinder x has a volume of 169.6 in^3
Cylinder a has a volume of 100.5 in^3
Cylinder c has a volume of 169.6 in^3
注意 cylvol 只接收 cyls(i).dimensions(端点结构体),而不是整个 cyls(i) —— 只传函数需要的最小信息,是好习惯。
8.3 进阶数据结构:分类数组与表
cell array 和 struct 是「容器型」通用解。本节再介绍两个 MATLAB 内置的专用结构:分类数组(categorical array) 和 表(table)。它们各自针对一类常见业务场景,语法更贴近自然语言。
8.3.1 分类数组(categorical array)
适合「值是有限、离散、可枚举」的数据(如血型、星期、问卷选项)。
>> icecreamfaves = categorical({'Vanilla', 'Chocolate', 'Chocolate', ...
'Rum Raisin', 'Vanilla', 'Strawberry', 'Chocolate', ...
'Rocky Road', 'Chocolate', 'Rocky Road', 'Vanilla', ...
'Chocolate', 'Strawberry', 'Chocolate'});
>> cats = categories(icecreamfaves) % 列出所有类别(按字母排序)
cats =
5×1 cell 数组
{'Chocolate' }
{'Rocky Road' }
{'Rum Raisin' }
{'Strawberry' }
{'Vanilla' }
>> countcats(icecreamfaves) % 每个类别出现次数
ans =
6 2 1 2 3 % Chocolate 6 次、Rum Raisin 1 次 ...
>> summary(icecreamfaves) % 一行摘要
Chocolate Rocky Road Rum Raisin Strawberry Vanilla
6 2 1 2 3有序分类(ordinal):当类别本身有顺序关系时,可以声明顺序:
>> daynames = {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'};
>> stepgoalsmet = {'Tue', 'Thu', 'Sat', 'Sun', 'Tue', 'Sun', ...
'Thu', 'Sat', 'Wed', 'Sat', 'Sun'};
>> ordgoalsmet = categorical(stepgoalsmet, daynames, 'Ordinal', true);
>> summary(ordgoalsmet)
Mon Tue Wed Thu Fri Sat Sun
0 2 1 2 0 3 3
% 有序分类可以直接用 < > 比较
>> ordgoalsmet(1) < ordgoalsmet(3) % Tue (序号 2) < Thu (序号 4)?
ans =
1 % 真类比 Python:categorical ≈ pandas 的 Categorical 类型,专门为离散文本数据设计,避免反复比对字符串。
8.3.2 表(table)
适合「行列都有意义的二维数据」—— 比如医院病人档案、销售明细、实验记录。
>> names = {'Harry', 'Sally', 'Jose'}; % 行名
>> weights = [185; 133; 210]; % 注意:列向量
>> heights = [74; 65.4; 72.2];
>> patients = table(weights, heights, 'RowNames', names)
patients =
weights heights
_______ ________
Harry 185 74
Sally 133 65.4
Jose 210 72.2table 比「数值矩阵」强在:
- 每列有名字(
weights、heights),不只是column 1、column 2。 - 行也可以有名字(
'Harry'等),不只靠下标 1、2、3。
两种访问方式:
% 圆括号 → 子表(还是 table)
>> patients(1:2, 1)
ans =
weights
_______
Harry 185
Sally 133
% 按行名取多个
>> patients({'Harry', 'Jose'}, :)
ans =
weights heights
_______ ________
Harry 185 74
Jose 210 72.2
% 花括号 → 提取成数值矩阵
>> mat = patients{'Harry', 'Jose'}, :)
mat =
185.0000 74.0000
210.0000 72.2000summary(patients) 会显示每列的统计摘要(min/median/max),对快速浏览数据非常方便。
cell array vs struct vs table 三件套
数据结构 类比 适用场景 cell array 异构 list 装任意东西、可遍历 struct dict / C struct 一个对象的多个命名属性 table pandas DataFrame 行/列都有名字的二维数据 categorical pandas Categorical 离散枚举文本数据
8.4 排序
排序(sorting)是把数据按某个字段大小重新组织。第 8 章讲了三种层次:数值向量排序、结构体向量按字段排序、字符串排序。
8.4.1 选择排序(理解原理)
内置 sort 函数一行搞定,但教材先讲了一个经典算法 selection sort(选择排序),帮你理解”排序到底在做什么”:
function outv = mysort(vec)
for i = 1:length(vec) - 1
indlow = i; % 假设当前位置已经是最小
for j = i+1:length(vec)
if vec(j) < vec(indlow) % 找到更小的就更新下标
indlow = j;
end
end
% 把最小值交换到位置 i
temp = vec(i);
vec(i) = vec(indlow);
vec(indlow) = temp;
end
outv = vec;
end核心思想:从前往后扫,每一轮把「未排序区」里的最小值挪到「已排序区」末尾。这是排序算法里最直观的版本。
>> vec = [85 70 100 95 80 91];
>> vec = mysort(vec)
vec =
70 80 85 91 95 100
8.4.2 内置 sort 函数
工程中几乎只用内置 sort:
>> sort(vec) % 默认升序
ans =
70 80 85 91 95 100
>> sort(vec, 'descend') % 降序
ans =
100 95 91 85 80 70对矩阵默认按列排序,要按行排加第 2 个参数:
>> mat = [4 6 2; 8 3 7; 9 7 1];
>> sort(mat) % 按列(每列单独排序)
ans =
4 3 1
8 6 2
9 7 7
>> sort(mat, 2) % 按行
ans =
2 4 6
3 7 8
1 7 98.4.3 对结构体向量按字段排序
业务上 90% 的排序都是”按某个字段排”。下面函数对 packages 按 price 排序:
function outv = mystructsort(structarr)
for i = 1:length(structarr) - 1
indlow = i;
for j = i+1:length(structarr)
if structarr(j).price < structarr(indlow).price
indlow = j;
end
end
% 交换整个结构体(不仅是 price 字段)
temp = structarr(i);
structarr(i) = structarr(indlow);
structarr(indlow) = temp;
end
outv = structarr;
end排序时交换的是 整个结构体,不是只交换被比较的字段
每个
structarr(i)是一个完整 record;只交换.price会破坏其他字段。所以排序循环里要写temp = structarr(i); structarr(i) = structarr(indlow);—— 三个赋值一起做。
进阶版:通用排序(任意字段都能排)—— 用到了 8.2.3 的动态字段名:
function outv = generalPackSort(inputarg, fname)
if isfield(inputarg, fname)
for i = 1:length(inputarg) - 1
indlow = i;
for j = i+1:length(inputarg)
% 动态字段名:字符串变量 fname 当字段名用
if inputarg(j).(fname) < inputarg(indlow).(fname)
indlow = j;
end
end
temp = inputarg(i);
inputarg(i) = inputarg(indlow);
inputarg(indlow) = temp;
end
outv = inputarg;
else
outv = []; % 字段不存在返回空
end
end调用:
>> packByPrice = generalPackSort(packages, 'price');
>> printpackages(packByPrice)
Item # Cost Price Code
587 11.11 33.33 w
123 19.99 39.95 g
456 5.99 49.99 l
>> packByCost = generalPackSort(packages, 'cost'); % 按成本排
>> generalPackSort(packages, 'profit') % 不存在的字段
ans =
[] % 空注意限制:动态字段名只对数值或字符字段有效;如果字段是字符串/向量本身,generalPackSort 会失效。这是教材明确指出的”不够 general”之处。
8.4.4 对字符串排序
字符串排序分两种场景:
字符矩阵(每行一个字符串):用 sort 按 ASCII 码逐列比,用 sortrows 按”字符串整体”比:
>> words = char('Hello', 'Howdy', 'Hi', 'Goodbye', 'Ciao');
>> sort(words) % 按 ASCII 逐列,结果混乱
ans =
Ce
Giad
Hildb
Hoolo
Howoyye
>> sortrows(words) % 按整体字符串字典序
ans =
Ciao
Goodbye
Hello
Hi
Howdysortrows 把每行作为一个整体”先比第一个字符再比第二个”,所以得到的就是字典序。对字符串排序永远用 sortrows。
字符串 cell array:直接用 sort,会自动按字典序:
>> sort({'Chemical', 'Mechanical', 'Biomedical', 'Electrical', 'Industrial'})
ans =
1×5 cell 数组
{'Biomedical' } {'Chemical' } {'Electrical' } {'Industrial' } {'Mechanical' }分类数组:对 categorical,sort 按字母序;对 ordinal categorical,按声明的顺序:
>> sort(ordgoalsmet) % 按 Mon→Sun 顺序排
ans =
1×11 categorical 数组
Tue Tue Wed Thu Thu Sat Sat Sat Sun Sun Sun8.5 索引向量:不排序也能按顺序遍历
排序会改动原数据的物理位置。有时你只是想「按某种顺序查看数据」,并不想动它本身。
索引向量(index vector)就是干这个的:它是个整数下标序列,告诉你”先看第几个、再看第几个…”。
>> grades = [85 70 100 95 80 91];
>> grade_index = [2 5 1 6 4 3]; % 第 i 小的是 grades 中的哪个位置
>> grades(grade_index) % 用索引向量「假装排好序」
ans =
70 80 85 91 95 100grade_index 的含义是:grades(grade_index(1)) = 第 1 小的 = 70;grades(grade_index(2)) = 第 2 小的 = 80;……
算法自动生成索引向量:
function indvec = createind(vec)
len = length(vec);
indvec = 1:len; % 默认索引:1,2,3,...,n
for i = 1:len - 1
indlow = i;
for j = i+1:len
% 比较的是原 vec 的值,但交换的是 indvec 里的下标
if vec(indvec(j)) < vec(indvec(indlow))
indlow = j;
end
end
temp = indvec(i);
indvec(i) = indvec(indlow);
indvec(indlow) = temp;
end
end核心思想:比较 vec 时通过 vec(indvec(j)) 间接取,但只交换 indvec 里的下标,不交换 vec 里的值。结果 vec 不动,indvec 里排好下标顺序。
>> grades = [85 70 100 95 80 91];
>> grade_index = createind(grades)
grade_index =
2 5 1 6 4 3
>> grades(grade_index) % 用索引向量打印"看似排好的版本"
ans =
70 80 85 91 95 1008.5.1 在结构体向量上用索引向量
业务里一种很常见的场景:要按多个字段分别查看数据,但不想为每个字段都排序 —— 一排序就把原数据搞乱了。
% 算出两组索引向量
cost_ind = createind_of_field(packages, 'cost'); % 按成本排的索引
price_ind = createind_of_field(packages, 'price'); % 按售价排的索引
% 同一个 packages,按两组索引分别打印
>> printpackind(packages, cost_ind)
Item # Cost Price Code
456 5.99 49.99 l
587 11.11 33.33 w
123 19.99 39.95 g
>> printpackind(packages, price_ind)
Item # Cost Price Code
587 11.11 33.33 w
123 19.99 39.95 g
456 5.99 49.99 lprintpackind 的实现就是循环里把 packstruct(indvec(i)).xxx 替进去:
function printpackind(packstruct, indvec)
fprintf('Item # Cost Price Code\n')
for i = 1:length(packstruct)
fprintf('%6d %6.2f %6.2f %3c\n', ...
packstruct(indvec(i)).item_no, ...
packstruct(indvec(i)).cost, ...
packstruct(indvec(i)).price, ...
packstruct(indvec(i)).code)
end
end排序 vs 索引向量
- 排序:会永久改变数组顺序。如果需要按不同字段分别查看,要排序多次。
- 索引向量:原数组不动,用几组就生成几组索引,按需取用。在”同一份数据库按不同字段查看”的场景几乎总是更优。
教材原话:“When it is necessary to iterate through a vector of structures in order based on several different fields, it may be more efficient to create index vectors based on these fields rather than sorting the vector of structures multiple times.”
章节配图(本章开头图)

这张图是原书 Part 2「Advanced Topics for Problem Solving with MATLAB」章节群的扉页 —— 提醒你:第 8 章起,MATLAB 内容从「基础语法」正式进入「用 MATLAB 解决真实工程问题」的进阶部分。结构化数据、数据库思维、排序与检索,这些都是工程计算的必备武器。
一图总结:第 8 章三大数据结构的取舍
flowchart TD Q{需要装多种类型数据?} Q -- 否 --> A[普通数值矩阵/向量] Q -- 是 --> B{希望按索引遍历?} B -- 是 --> C[ cell array 元胞数组] B -- 否 --> D{用名字访问字段?} D -- 是 --> E["struct 结构体(含 vector of structs / nested structs)"] D -- 否 --> F{行/列都有名字?} F -- 是 --> G["table 表"] F -- 否 --> H{有限枚举的文本?} H -- 是 --> I["categorical 分类数组"] H -- 否 --> C
8.6 三大容器数据结构的取舍详解
这一节用同一个业务问题(存一个班级的学生成绩)展示三种数据结构的不同写法,帮助你建立直觉。
业务场景:某班 30 名学生,每人有姓名(字符串)、学号(整数)、4 次小测成绩(4 个 double)。我们需要:① 录入;② 按学号查某人;③ 计算每人平均分;④ 按平均分排序。
方案 A:用普通数值矩阵
data = zeros(30, 5);
% data(:, 1) 是学号,data(:, 2:5) 是 4 次小测
% 名字放哪?放不下。问题:姓名是字符串,矩阵只能放数值。这就是「为什么需要 cell / struct」的根源 —— 当一个对象有异质属性时,纯数值矩阵搞不定。
方案 B:用元胞数组 cell array
students = cell(30, 3); % 第 1 列姓名、第 2 列学号、第 3 列成绩向量
students(:, 1) = {'张三'; '李四'; '王五'; ...}; % 字符串
students(:, 2) = {2024001; 2024002; 2024003; ...};% 整数
students(:, 3) = {[85 90 78 92]; [77 80 88 85]; ...}; % 向量
% 按学号查
idx = find([students{:, 2}] == 2024001);
disp(students{idx, 1}) % 输出姓名
% 平均分
for i = 1:30
avg(i) = mean(students{i, 3});
end
% 按平均分排序(对 cell 来说比较麻烦,要先抽出来)优点:位置索引简单直接,students{i, 3} 这种写法很自然。
缺点:语义弱 —— 你得记得第几列是姓名、第几列是成绩;新增字段(多加一列「专业」)就要重新设计。
方案 C:用结构体 struct vector(推荐)
students = struct('name', {}, 'id', {}, 'quiz', {});
for i = 30:-1:1 % 倒序预分配
students(i) = struct('name', '?', 'id', 0, 'quiz', zeros(1,4));
end
% 填值
students(1) = struct('name', '张三', 'id', 2024001, 'quiz', [85 90 78 92]);
students(2) = struct('name', '李四', 'id', 2024002, 'quiz', [77 80 88 85]);
...
% 按学号查
for i = 1:length(students)
if students(i).id == 2024001
disp(students(i).name); break
end
end
% 平均分
for i = 1:length(students)
avg(i) = mean(students(i).quiz);
end
% 按平均分排序(用 createind 索引向量,原数据不动)
[~, ord] = sort(avgs); % 或 createind(avgs)
for i = 1:length(students)
s = students(ord(i));
fprintf('%-10s %5.1f\n', s.name, mean(s.quiz))
end优点:自文档化 —— students(i).quiz 一看就懂是成绩向量;字段可以随便加(新加 students(i).major = '电子' 即可)。
缺点:不能像 cell 那样直接 students{:} 批量取字段 —— 但 students.quiz 可以拿到所有 quiz 字段组成的逗号列表。
方案 D:用 table(最现代)
names = {'张三'; '李四'; '王五'};
ids = [2024001; 2024002; 2024003];
quizzes = [85 90 78 92; 77 80 88 85; 92 88 95 90];
students = table(ids, quizzes, 'RowNames', names);
% 表头会自动是 ids、quizzes
students('张三', :) % 按行名取
mean(students.quizzes, 2) % 每行平均
sortrows(students, 'quizzes') % 按列名排序(需先转 table)table 是 R2013b 之后推出的现代写法,对二维异构数据最自然。但教材以 cell 和 struct 为主,table 留到后续章节和工程里再深入。
四个方案对比
| 维度 | 数值矩阵 | cell array | struct vector | table |
|---|---|---|---|---|
| 异构数据 | ✗ | ✓ | ✓ | ✓ |
| 字段语义 | ✗ | ✗(靠位置记) | ✓ | ✓ |
| 遍历/向量化 | ✓ | ✓ | 部分(点运算符) | ✓ |
| 增字段难度 | 改设计 | 改设计 | 一行赋值 | 加列 |
| 学习曲线 | 平 | 中 | 中 | 中(外加 table 语法) |
| 适合规模 | 千万元素数值 | 几百异构对象 | 几百异构对象 | 几百到几万行异构 |
起步选哪个?
业务里80% 的情况用 struct vector:字段稳定、有名字、规模适中。循环遍历/批量取元素用 cell array。二维表格型数据用 table(如果你的 MATLAB 版本支持)。纯数值继续用矩阵 —— 永远别把矩阵当万能容器硬塞字符串。
8.7 排序与索引向量的取舍速查
排序、索引、categorical、cell、struct 的组合用法在工程里高频出现。最后用一张速查表收尾:
| 需求 | 推荐方案 | 理由 |
|---|---|---|
| 数值向量升序 | sort(v) | 内置,最快 |
| 数值向量降序 | sort(v, 'descend') | 直接 |
| 矩阵按列排序 | sort(M) | 默认就是按列 |
| 矩阵按行排序 | sort(M, 2) | 第二参数传 2 |
| 字符矩阵按字典序 | sortrows(charM) | 关键!sort 不是字典序 |
| cell 字符串数组按字典序 | sort(cellstr) | 直接 |
| struct vector 按数值字段排序 | createind + 索引向量 | 原数据不动 |
| struct vector 按字符串字段排序 | 自定义(strcmp 比较) | 教材习题 33 |
| categorical(非 ordinal)排序 | sort(cat) | 按字母序 |
| ordinal categorical 排序 | sort(cat) | 按声明的顺序 |
| 同时按多字段展示 | 多组索引向量 | 不破坏原数据 |
一段完整示例:学生成绩管理
% 假设有 5 个学生
students(5) = struct('name', '赵六', 'id', 5, 'quiz', [70 80 90 85]);
students(1) = struct('name', '张三', 'id', 1, 'quiz', [85 90 78 92]);
students(2) = struct('name', '李四', 'id', 2, 'quiz', [77 80 88 85]);
students(3) = struct('name', '王五', 'id', 3, 'quiz', [92 88 95 90]);
students(4) = struct('name', '陈七', 'id', 4, 'quiz', [60 75 70 80]);
% 计算每人平均分
avgs = zeros(1, length(students));
for i = 1:length(students)
avgs(i) = mean(students(i).quiz);
end
% 生成按平均分升序的索引向量(用前面章节的 createind)
ord = createind(avgs);
% 用索引向量打印"按平均分排好的成绩单"
fprintf('%-10s %-6s %-8s\n', 'Name', 'ID', 'Average')
for i = 1:length(students)
s = students(ord(i));
fprintf('%-10s %-6d %-8.1f\n', s.name, s.id, mean(s.quiz))
end预期输出:
Name ID Average
陈七 4 71.3
李四 2 82.5
赵六 5 81.3
张三 1 86.3
王五 3 91.3
整个流程里没有 sort 也没有改原数据 —— 全靠索引向量。这就是在做「多次按不同字段展示」时为啥优先用 createind 的原因。
8.8 数据结构选择的 C 程序员视角
C 程序员对照这张表会更顺:
| C 的写法 | MATLAB 等价 | 备注 |
|---|---|---|
int arr[10] | arr = zeros(1,10) | 普通数值向量 |
struct S { int x; char *name; } | s = struct('x', 1, 'name', 'foo') | 按字段名访问 |
struct S arr[10] | arr(10) = struct(...); ... | struct vector |
linked list | 需用 cell 嵌套 | MATLAB 没有原生链表 |
hash table / dict | containers.Map 或 table | 第 8 章外的内容 |
enum Weekday = {MON, TUE, ...} | categorical(...) 或 ordinal categorical | 严格枚举用 categorical |
qsort(arr, n, cmp) | sort(arr) / createind(arr) | MATLAB 内置自动选算法 |
从 C 转过来的三个心智转换
- 指针思维要放下:MATLAB 大部分对象传值不传引用,赋值即拷贝。如果想”共享一份数据”,要用 handle class 或
persistent/global,不在第 8 章范围。- 不需要手动管理内存:cell / struct / table 的大小动态可变,但预分配依然是性能硬规则。
- 别再写链表:业务需求用 cell / struct 都能更紧凑地表达,链表几乎用不到。
8.9 教材习题精选解读
挑几道题详解,覆盖 cell array、struct、nested struct、索引向量几个高频考点。
习题 4:随机选一段短语打印
从一个 cell 数组里随机抽一个元素打印。
exclaimcell = {'Bravo', 'Fantastic job', 'Way to go', 'Awesome'};
n = length(exclaimcell);
idx = randi(n); % 随机选 1..n 的一个整数
disp(exclaimcell{idx}) % 花括号取内容打印要点:三步走 —— randi(n) 抽下标(MATLAB 里下标一定要从 1 开始!),exclaimcell{idx} 用花括号取 cell 内容(而不是 exclaimcell(idx),否则会打印 [1×5 char]),disp 把字符串打到屏幕。
习题 5:从三个 cell 里拼出随机句子
names、verbs、nouns三个 cell 数组里各抽一个拼成句子。
names = {'Harry', 'Xavier', 'Sue'};
verbs = {'loves', 'eats'};
nouns = {'baseballs', 'rocks', 'sushi'};
% 关键:把 1×N cell 转置成 N×1 后才能取每个元素
n = randi(length(names));
v = randi(length(verbs));
nn = randi(length(nouns));
sentence = [names{n} ' ' verbs{v} ' ' nouns{nn}];
disp(sentence)
% 输出可能是 'Harry eats sushi'要点:拼接 cell 元素用花括号取内容(names{n}),然后空格必须手写(MATLAB 拼接不会自动加空格)。这种”从三个池子各抽一个”的模式在文本生成、测试用例随机化里反复出现。
习题 17:周期表元素的 struct
用结构体存化学元素:name、atomic number、symbol、class、atomic weight、shell 电子数(7 元素向量)。
elements(3) = struct('name', 'Lithium', 'num', 3, 'sym', 'Li', ...
'class', 'alkali_metal', 'aw', 6.94, ...
'shells', [2 1 0 0 0 0 0]);
elements(1) = struct('name', 'Hydrogen', 'num', 1, 'sym', 'H', ...
'class', 'nonmetal', 'aw', 1.008, ...
'shells', [1 0 0 0 0 0 0]);
elements(2) = struct('name', 'Helium', 'num', 2, 'sym', 'He', ...
'class', 'noble_gas', 'aw', 4.003, ...
'shells', [2 0 0 0 0 0 0]);
% 查询
>> elements(1).sym
ans = 'H'
% 打印所有元素的符号
for i = 1:length(elements)
fprintf('%s (%.3f)\n', elements(i).sym, elements(i).aw)
end
% 输出:
% H (1.008)
% He (4.003)
% Li (6.940)要点:这是教材练习题 17 的微缩版 —— 真实周期表有 118 个元素,存储结构一摸一样。shells 字段是一个 向量,存 7 个电子层的电子数;嵌套结构的味道已经隐隐出现了。
习题 25:kit 嵌套结构体的下钻
下面这道题考查 nested struct 的访问层级,是初学者最容易犯迷糊的地方。
kit(2).sub.id = 123;
kit(2).sub.wt = 4.4;
kit(2).sub.code = 'a';
kit(2).name = 'xyz';
kit(2).lens = [47];
kit(1).name = 'rst';
kit(1).lens = 5:6;
kit(1).sub.id = 33;
kit(1).sub.wt = 11.11;
kit(1).sub.code = 'q';| 表达式 | 合不合法 | 结果 |
|---|---|---|
kit(1).sub | ✓ | 一个 1×1 struct,含 id=33、wt=11.11、code=‘q’ |
kit(2).lens(1) | ✓ | 47(kit(2).lens 是 [47]) |
kit(1).code | ✗ | 字段不存在 —— code 是 kit(1).sub 的字段 |
kit(2).sub.id == kit(1).sub.id | ✓ | 0(123 ≠ 33) |
strfind(kit(1).name, 's') | ✓ | [2](‘rst’ 中 ‘s’ 在第 2 个位置) |
核心:每加一个 . 就下钻一层;缺字段、缺层级都会报错。debug 这种嵌套结构时建议把每一级都 disp 一下看清楚结构。
习题 31:matsort —— 把矩阵所有元素整体排序
把矩阵所有元素从小到大排,按列重新铺回去。
function out = matsort(mat)
v = sort(mat(:)); % 第一步:列优先展平成向量并排序
out = reshape(v, size(mat)); % 第二步:按原矩阵形状重塑
end要点:MATLAB 的 A(:) 默认是按列展开(先第一列全部,再第二列……),reshape 也按这个顺序。所以这两步配合,结果就是「整体排序、按列重铺」。这个技巧很常被误以为是按行排序(结果不一定),但只要 sort + reshape 都按默认列优先走,结果就是确定的。
>> mat = [4 5 2; 1 3 6; 7 8 4; 9 1 5];
>> matsort(mat)
ans =
1 4 6
1 4 7
2 5 8
3 5 9习题 35:返回升降序两个索引向量
function [ascind, dscind] = createbothind(vec)
ascind = createind(vec); % 升序索引(教材里的函数)
len = length(vec);
dscind = len + 1 - ascind; % 降序 = 升序反过来
end要点:降序索引不需要重新跑排序算法 —— 「整体反向」就等于降序。这是数学上的小聪明:ascind(1) 是最小值的下标,len + 1 - ascind(1) 是最大值的下标。生产代码里这种”小聪明”能省一半的 sort 调用。
习题 26:printhts —— 含嵌套结构的实验数据
一个向量,每个元素是「被试」结构体,其中 height 字段又是一个含 feet / inches 的嵌套结构。
function printhts(experiments)
for i = 1:length(experiments)
% howhigh 返回总英寸数(外部函数)
inches = howhigh(experiments(i).height);
fprintf('%-10s height = %d inches\n', ...
experiments(i).name, inches);
end
end
function total = howhigh(h)
total = h.feet * 12 + h.inches;
end
% 数据准备
experiments(2) = struct('num', 11, 'name', 'Sally', ...
'weights', [111.45 111.11], ...
'height', struct('feet', 7, 'inches', 2));
experiments(1) = struct('num', 33, 'name', 'Joe', ...
'weights', [200.34 202.45], ...
'height', struct('feet', 5, 'inches', 6));要点:当一个被试有多个维度(编号、姓名、体重向量、身高嵌套结构),struct vector + nested struct 是最自然的表达。experiments(i).height 先取出身高结构体,再传给 howhigh,函数内部再用 .feet 和 .inches 取子字段 —— 三层点运算符链路很经典。
习题 27:洪水数据的多文件结构化分析
文件里每行
code rainfall duration,要算每场风暴的强度(rainfall/duration),找最强风暴。
这是把第 8 章的工具全部串起来:先设计 struct、向量化读文件、计算强度、找最值、按强度排序打印。
% 第 1 步:建数据文件(手动或脚本)
% stormdata.dat 内容:
% 321 2.4 1.5
% 111 3.3 12.1
% 第 2 步:脚本
load stormdata.dat % 数值矩阵
nrows = size(stormdata, 1);
% 预分配 struct vector(先建最后一个)
storms(nrows) = struct('code', '', 'rainfall', 0, 'duration', 0, 'intensity', 0);
for i = 1:nrows
storms(i).code = stormdata(i, 1);
storms(i).rainfall = stormdata(i, 2);
storms(i).duration = stormdata(i, 3);
storms(i).intensity = stormdata(i, 2) / stormdata(i, 3);
end
% 第 3 步:打印(嵌套函数 printStormInfo 自己写)
printStormInfo(storms);
% 第 4 步:找最强风暴(用 max + 索引向量思路)
[~, idx] = max([storms.intensity]); % max 返回值 + 位置
fprintf('\n最强风暴: code=%d, intensity=%.2f in/hr\n', ...
storms(idx).code, storms(idx).intensity);要点:这种「读文件 → struct vector → 计算派生字段 → max/sort」的四步流程是工程数据处理的标准模板。第 8 章所有工具(struct、repmat/倒序预分配、点运算符、索引向量)在这里都用上了一遍。
本章通关标准
- 能用一句话解释 cell array 和 struct 的本质区别,并举出各自最合适的应用场景。
- 能写出
cellrowvec(i)、cellrowvec{i}、cellrowvec{i}(k)三种访问方式,并解释每层访问到的对象类型。- 能用
struct('field', value, ...)创建一个含多个字段的结构体,掌握「预分配结构体向量」的两种写法(倒序、repmat)。- 能用
createind函数生成索引向量,理解它和直接排序的取舍。- 理解
sort、sortrows、动态字段名、isfield、fieldnames这些常用工具的典型用法。
1.
cellrowvec{2}和cellrowvec(2)分别返回什么?类型呢?
cellrowvec{2}用花括号,返回第 2 个格子里装的内容(如果装的是字符串就是char);cellrowvec(2)用圆括号,返回第 2 个格子本身,是个 的 cell(class是cell)。圆括号永远返回 cell,花括号才”拆盒子”。
2. 什么时候选 cell array,什么时候选 struct?
教材判据:cell array 适合需要遍历/向量化的场景(比如装一组不等长的名字),struct 适合字段有自然名字、且不打算循环访问的场景(比如一个学生的多个属性)。如果数据是规整的二维结构,行列都有名字,table 才是更现代的选择。
3. struct 字段名能是变量吗?
可以,叫动态字段名:
package.(fname)其中fname是字符串变量。常用于”isfield验证 → 动态访问”的反射式查询场景。如果字段名是固定的,直接package.cost就行。
4. 排序 struct vector 时为什么要交换整个结构体?只交换被比较的字段不行吗?
因为排序会把元素挪到新位置 —— 如果只挪
price,其它字段(item_no、cost、code)还留在原位置,整个 record 就乱了。教材明确:exchange elements in the index vector或者交换temp = structarr(i); structarr(i) = structarr(indlow); structarr(indlow) = temp;三步走,保持 record 完整。
5. 什么场景下用索引向量(index vector)优于直接排序?
当同一份数据需要按不同字段多次遍历展示时。排序会永久改变原顺序,要按 5 个字段展示就得排 5 次;索引向量不动原数据,每个字段一组下标就行。代价是每次访问多一层下标引用,可读性略差。在数据量大、要排序多次的场景,索引向量几乎总是赢家。