这一章在干嘛?

前 7 章你接触的所有变量都是「单一类型」的数值数组 —— 学生成绩、信号采样、温度读数,它们的逻辑身份都一样。但现实场景里,一个变量经常要装不同类型的数据(比如一份学生档案要装名字字符串、ID 数字、四次小测成绩向量)。这一章教你两个 MATLAB 杀手锏:元胞数组 cell array 和结构体 struct。前者按「位置」索引(像 Python 列表),后者按「字段名」访问(像 C 的 struct 或 Python 的 dict)。学完后你就能把真实业务里的复杂记录(学生档案、商品清单、实验数据)装进 MATLAB 变量。

8.1 元胞数组 cell array

8.2 结构体 struct

8.3 进阶数据结构:分类数组与表

8.4 排序

8.5 索引向量:不排序也能按顺序遍历

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 =
     0

8.2 结构体 struct

结构体(struct)和 cell array 一样能装异类数据,但它的访问方式是按字段名而不是按位置。

类比:

  • cell array ≈ Python list,按 [0], [1], [2] 访问
  • struct ≈ Python dict 或 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.9500

8.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'