写这篇MATLAB矩阵横向拼接的详解,是因为我最近在处理一批多通道实验数据时,又把老问题碰了一遍:几十个通道按列存放,要合并成一个大矩阵,说起来不过是横向拼接,但数据类型、批量方式、版本习惯不一样,踩坑的点完全不同。很多教程只会讲到[A B]这种基础语法就收尾了,真正到批量列数据合并的工程现场,维度不匹配、类型报错、越拼越慢的问题会一个个冒出来。这篇我把三种基础写法、维度规则、类型差异、批量合并方案和实测踩坑一起整理出来,给刚入门MATLAB的学生一个清晰起点,也给经常处理多源数据列合并的工程师一份可直接参考的实操手册。
1. 三种横向拼接写法,先从结果形态看懂差异
1.1 方括号[A B]:最常用但也最容易忽略细节
方括号拼接是MATLAB里最直观的横向拼接写法。所谓横向拼接,就是把数组沿着列的方向并排放置,结果的列数等于各输入列数之和,行数保持不变。比如一个3行1列的向量A和一个3行1列的向量B,用[A B]得到的就是3行2列的矩阵;如果B变成3行4列,[A B]就是3行5列。关键点只有一个:参与拼接的所有数组,行数必须完全一致。
A = [1; 2; 3]; % 3x1 列向量 B = [4; 5; 6]; % 3x1 列向量 C = [A B]; % 3x2 矩阵这里的空格和逗号作用完全相同,[A, B]和[A B]是等价的。方括号里的分号则是纵向拼接,[A; B]会得到6行1列,这一点新手特别容易搞混。所以我会建议初学者先把一个规则刻在脑子里:同行号、逗号、空格都表示"这一行接着往右写";分号表示"换行往下写"。横向拼接就是逗号与空格的活。
方括号还有一个容易被忽略的细节:它支持嵌套。你可以把一个拼接表达式再作为另一个拼接表达式的输入,比如[[A B] C],这在构造分块矩阵时非常常用。另外,方括号拼接对输入个数没有限制,想拼多少就写多少,但每个输入都要满足同样的行数约束。
1.2 horzcat函数:适合多输入和批量展开
horzcat是横向拼接的函数写法,语法是horzcat(A, B, ...),结果和[A B ...]完全一致。既然一致,为什么还要用函数?我实际使用下来有两个理由。
第一个理由是意图清晰。当代码里同时出现横向拼接和纵向拼接时,函数名能把"我要横着拼"这个意图直接写出来,比肉眼分辨逗号和分号可靠得多。
C = horzcat(A, B, D, E);第二个理由是配合逗号分隔列表展开。你手上如果有几十个列向量存在同一个元胞数组里,写成[A B C D ...]既不现实也不可维护,此时用horzcat加冒号展开,一行搞定:
cols = {A, B, C, D, E}; % 每个元素都是一个列向量 M = horzcat(cols{:}); % 等价于 M = horzcat(cols{1}, cols{2}, ...)这一个技巧,基本就是批量列数据合并的雏形。后面第4节我会专门展开谈工程化写法。
1.3 cat(2, ...):高维场景的统一入口
cat函数是更通用的拼接接口,第一个参数指定拼接维度。cat(1, ...)是纵向拼接,等价于[A; B ...]或者vertcat;cat(2, ...)就是横向拼接,等价于[A B ...]或者horzcat;cat(3, ...)则是在第三维上追加,相当于把数组一页一页叠起来。
对二维矩阵来说,用cat(2, ...)和用horzcat没有任何区别。它的价值在于维度可以参数化:当你的拼接方向需要根据条件动态决定时,把一个变量传给cat的第一个参数就行,而方括号和horzcat都做不到这一点。
对于三维数组,cat(2, ...)和方括号的行为也基本一致,都是沿第二维拼接,但cat能让你更清楚地表达"我是在操作维度,而不是在写表达式"。比如你有20个二维数组,每个都是3行2列,想每4个拼成一组放在一个三维数组里,用cat写出来的循环,语义比方括号拼接嵌套清晰很多。
X1 = rand(3, 2); X2 = rand(3, 2); Y = cat(2, X1, X2); % 3x4,横向拼接 Z = cat(3, X1, X2); % 3x2x2,沿页方向叠放三种写法先用一个表收拢一下,方便对照记忆:
| 写法 | 典型场景 | 备注 |
|---|---|---|
| [A B] 或 [A, B] | 手写少量矩阵、临时拼接 | 最直观,嵌套构造分块矩阵方便 |
| horzcat(A, B, ...) | 多输入拼接、元胞数组批量展开 | 函数名语义清晰,支持冒号展开 |
| cat(2, A, B, ...) | 高维数组、维度需要动态指定 | 同一套逻辑可横向/纵向/沿页拼接 |
2. 维度匹配的硬规则与那些"报错现场"
2.1 行数必须一致:最常见的维度不匹配
横向拼接最常遇到的报错,就是"Dimensions of arrays being concatenated are not consistent."。这个报错说白了就是:你想把两个数组并排放,但它们的行数对不上。
举个例子,一个3行1列的向量和一个2行1列的向量没法横向拼接,因为并排放置后,第一行和第二行到底谁跟谁对齐,矩阵根本没法画出来——矩阵是规则的矩形,不允许某一行比其他行多一列。
A = [1; 2; 3]; % 3x1 B = [4; 5]; % 2x1 C = [A B]; % 报错:维度不一致这个规则的原因可以理解得更深一层:MATLAB的矩阵在内存里是连续存储的,拼接操作的本质是重新分配一块更大的内存,按顺序把数据填进去。既然是按行存放在连续内存里,每一行的长度就必须相同,否则就无法确定每个元素的存储位置。
所以我在批量合并列数据前,都会先跑一行检查:
assert(size(A, 1) == size(B, 1), '行数不一致:A有%d行,B有%d行', size(A,1), size(B,1));不要嫌这行检查多余。数据一旦来自不同文件、不同采集时间,行数不一致几乎是必然会发生的事,早发现比拼接报错之后再回去翻数据要省事得多。
2.2 方向搞反:分号与逗号只差一个符号
第二个很常见的错误是把横向拼接写成了纵向拼接。A和B都是3行1列的列向量时,[A B]是3行2列,[A; B]是6行1列;如果A和B都是1行3列的行向量,[A; B]则是2行3列。方向一变,结果的形状就完全不一样了。
A = [10 20 30]; % 1x3 行向量 B = [40 50 60]; % 1x3 行向量 H = [A B]; % 1x6,横向拼接 V = [A; B]; % 2x3,纵向拼接我见过不少同学在代码里把逗号写成空格、把空格看成逗号,然后把"横向"写成"A B"、"纵向"写成"A;B"。实际上,逗号和空格在方括号里完全等价,真正决定方向的是分号。
这里有个非常实用的自查习惯:写完拼接表达式后,立即手动算一遍size。横向拼接只改变列数,纵向拼接只改变行数。如果你预期是横着拼,结果的行数变了,那一定是写成了分号。代码评审时也多花两秒钟看一眼方向,这类bug往往不影响报错,但会静悄悄地污染后续所有计算。
2.3 标量扩展和空矩阵的边界行为
除了常规矩阵,两个边界情况也值得单独拎出来说,因为它们经常在实际工程里出现。
第一个是标量参与拼接。MATLAB允许把单个数字和一个矩阵横着拼,效果是该数字自动扩展到每一行。比如[zeros(3, 2) 5]会得到一个3行3列的矩阵,第三列全是5。这在给数据矩阵添加常数列(比如回归分析里的截距项)时非常方便,一行代码就搞定,不用再去写repmat。
X = [1 2; 3 4; 5 6]; % 3x2 Xc = [X 1]; % 3x3,第三列全是1第二个是空矩阵。size(A, 1)等于0的数组照样能参与横向拼接,只要参与方的行数也都是0。比如zeros(0, 3)和zeros(0, 5)拼在一起,结果是0行8列的空矩阵。但一个0行3列的数组和一个2行3列的数组拼,行数对不上,照样报错。做循环批量合并时,如果初始矩阵写成了空的竖向量[],后面横向拼接时就要留意行数是0还是最终行数,这是个非常隐蔽的坑。
3. 数据类型的拼接差异:数值、字符串、元胞与table
3.1 数值矩阵:类型提升与混拼限制
数值矩阵是横向拼接的主战场。double和single混拼通常没问题,MATLAB会按内置的类型提升规则把结果统一成其中一种;但整数类型(int8、int16、uint32这一族)和浮点类型(double、single)混拼,直接报错,不允许你"模糊处理"。
D = [1; 2; 3]; % double S = single([4; 5; 6]); % single M = [D S]; % 一般没问题,结果会被统一提升 I = int8([7; 8; 9]); M2 = [D I]; % 报错:不同类型不能直接拼接 M3 = [D double(I)]; % 先转换再拼接我的建议是:别依赖自动提升。凡是在一个合并结果里涉及两种以上数值类型,合并前统一用double()或single()转一遍,顺便加一行断言确认转换后类型一致。这样写出来的代码在任何版本都不会出现"换个版本结果类型就变了"的尴尬。
另外,复数和实数的拼接也是允许的,结果会自动变成复数类型。逻辑数组和数值混拼时,逻辑值会按0/1参与计算。这些都属于"能拼但结果类型要留心"的情况。
3.2 字符数组与字符串数组:最容易翻车的地方
MATLAB里有两套"文本"类型:老式的char字符数组,和新版全面推广的string字符串数组。这两个在横向拼接上的行为差异非常大,也是最容易翻车的地方。
char字符数组本质上就是一个数值矩阵,只是元素是字符编码。所以它遵循矩阵拼接规则:行向量拼行向量,得到更长的行向量;列向量拼列向量,得到更长的列向量;但想把两个不同长度的字符行向量纵向拼成矩阵,就会报错,因为字符矩阵要求每一行等长。
s1 = 'abc'; % 1x3 char s2 = 'def'; % 1x3 char M = [s1; s2]; % 2x3 char 矩阵 s3 = 'ghijk'; % 1x5 char M = [s1; s3]; % 报错:列数不一致string数组则是对象化的字符串,每个元素独立存在,所以横向拼接不受字符长度影响。["a"; "b"]和["ccc"; "d"]直接拼接完全没问题,因为拼的是"字符串元素"的数量,不是字符个数。
strA = ["a"; "b"]; strB = ["ccc"; "d"]; S = [strA strB]; % 2x2 string最坑的是char和string不能混拼。["abc" 'def']会直接报错,提示类型不一致。我实测中遇到的文本列数据合并问题,十有八九都是"这个文件读出来是char,那个文件读出来是string"造成的。解决方案统一得很:涉及多种来源文本时,先全部转成string再接,也就是string(charData)。转完之后再用string数组或cellstr统一管理,后续拼接就不会再出幺蛾子。
3.3 元胞数组与table:结构化数据的横向合并
元胞数组(cell)是异构数据的容器,横向拼接规则和数值矩阵一样,要求行数一致,但每一列可以是完全不同的类型和大小。这在处理"一个文件里既有字符串又有数值"时特别顺手。
cA = {1; 2; 3}; % 3x1 cell cB = {'x'; 'y'; 'z'}; % 3x1 cell C = [cA cB]; % 3x2 cellcell横向拼接最有价值的技巧还是和horzcat配合。比如你从Excel的不同sheet读了一批列数据,每列类型不一样,先统一放进cell,最后用horzcat(metaCols{:})一次性展开成矩阵——前提是每个cell元素都是等行数的数值列。
table是更高级的结构化容器,每一列有自己的名字。两个table横向拼接用[t1 t2]或horzcat(t1, t2)都可以,行为是把两个表的列并排放,变量名会合并成一张表的列名集合。但这里有两个注意点:第一,两个表的行数必须一致,位置对齐;第二,如果出现同名变量,不同版本的MATLAB处理方式不太一样(有的报错,有的自动重命名),稳妥做法是合并之前先用renamevars统一列名。
t1 = table([1;2;3], 'VariableNames', {'x'}); t2 = table([4;5;6], 'VariableNames', {'y'}); t = [t1 t2]; % 3x2 table,列名为 x, y还要区分一个概念:horzcat这种横向拼接是位置对齐——第一行对第一行,不问理由。如果你需要按某个ID或时间戳来做匹配对齐,应该用join、innerjoin、outerjoin等按变量连接的操作,而不是横向拼接。把这两类操作搞混,是做数据合并时最隐蔽的逻辑错误。
4. 批量列数据合并:从循环陷阱到工程化写法
4.1 循环拼接为什么慢:每步都在给整个矩阵搬家
批量列数据合并最原始的写法,是循环里不断用方括号往数据矩阵上追加列:
data = []; for k = 1:5000 col = randn(100, 1); data = [data col]; end这段代码在功能上没有任何问题,但性能非常差。原因要从MATLAB的内存模型说起:矩阵在内存里是连续的一段空间,data = [data col]看起来是"追加一列",实际动作是重新分配一块比原来大一列的内存,然后把旧数据全部拷过去,再填上新列。循环每走一步,历史数据就要整体搬家一次。
累加起来就是O(n^2)的拷贝量。数据只有几十行、几百列的时候感觉不出来,一旦到达几千列,耗时就会急剧上升。我在实测里见过循环拼接比预分配方案慢好几个数量级的情况,而且数据量越大,差距越离谱。新版本MATLAB在某些简单场景下会对这种数组增长做内部优化,但一旦代码被包进函数、数据被多个变量引用,优化就会失效,所以千万不要依赖这种"碰运气"的加速。
4.2 两种靠谱的工程写法:预分配和cell展开
第一推荐:预分配。既然知道最后会有多少列,就先建好一块大小合适的矩阵,循环里只填数,不扩容。
nRows = 100; nCols = 5000; data = zeros(nRows, nCols); for k = 1:nCols col = randn(nRows, 1); data(:, k) = col; end这段代码把"搬家"完全消除了,循环里只剩赋值,速度可以比上面的循环拼接快几个数量级。唯一要求是你在循环开始前知道nCols;如果确实不知道,可以先把结果存进cell,最后一次性展开。
第二推荐:cell暂存加horzcat冒号展开。适合"列数事先不确定、每列来源逻辑比较复杂"的场景。
cols = cell(1, nCols); for k = 1:nCols cols{k} = loadOneColumn(k); % 自定义的读取函数 end M = horzcat(cols{:});两种写法在工程上都足够稳定,预分配在速度上略优,cell方案在灵活性上更优。我个人习惯是:列数确定且数据纯数值时用预分配;列数不确定、类型混杂时用cell。
4.3 批量读取多个文件列数据的可复用脚本
现在给出一份可以直接改改用的批量列合并脚本。场景是:某个文件夹里有几百个CSV文件,每个文件只含一列数值,要把所有文件的列按文件名顺序并成一个大矩阵。
folder = 'data'; fileList = dir(fullfile(folder, '*.csv')); n = numel(fileList); if n == 0 error('该文件夹下没有CSV文件'); end % 读第一个文件,确定行数 firstFile = fullfile(folder, fileList(1).name); firstData = readmatrix(firstFile); nRows = size(firstData, 1); % 预分配 data = zeros(nRows, n); data(:, 1) = firstData; for k = 2:n tmp = readmatrix(fullfile(folder, fileList(k).name)); if size(tmp, 1) ~= nRows error('%s的行数为%d,与第一个文件的行数%d不一致', ... fileList(k).name, size(tmp,1), nRows); end data(:, k) = tmp; end脚本里有三个细节值得强调:一是用fullfile拼接路径,省去手动处理斜杠和中文路径的麻烦;二是在正式合并前先读第一个文件来锚定行数,后续文件都用error中断来保证行数一致;三是预分配的矩阵在循环开始前就已经存在,后面的赋值只是填数据。
4.4 合并前的校验与列顺序控制
批量合并最容易被忽略的问题是列顺序。dir函数返回的文件列表是按文件名字符串排序的,也就是ASCII顺序,不是自然数顺序。也就是说,"data_2.csv"会排在"data_10.csv"后面,因为字符'1'排在'2'前面。如果你的列顺序依赖文件名编号,必须显式做一次自然排序。
% 提取文件名中的数字序号,按数值大小重排 nums = cellfun(@(s) str2double(regexp(s, '\d+', 'match', 'once')), {fileList.name}); [~, idx] = sort(nums); fileList = fileList(idx);这一步做完之后,前面的读取循环才能保证"第k列就是第k号文件的数据"。我见过太多人忽略这个细节,最后得到的大矩阵列顺序和预期完全错位,而且因为是顺序铺开、没有明显报错,往往要等后续计算出了奇怪结果才能发现问题。
合并完成之后,建议顺手做三件校验:第一,size(data)是否符合预期;第二,检查是否有NaN或Inf,可以用sum(isnan(data), 'all')快速扫描;第三,如果后续要导出或做统计分析,把矩阵转成table并给每列起名字,会让下游工作舒服很多。
T = array2table(data); T.Properties.VariableNames = sprintfc('col_%d', 1:n); % 生成 col_1, col_2, ...5. 我实测踩过的坑:扩容、对齐与结构选型
5.1 隐式扩展的诱导:能加不等同于能拼
新版MATLAB引入了隐式扩展(implicit expansion),两个维度不同的数组做加减乘除时,会自动把长度为1的维度扩展到另一个数组的长度。这个特性日常用着很爽,但也给我带来过麻烦:它会让你的思维惯性认为"维度不一样也能运算",于是写拼接时也想当然。
A = [1; 2; 3]; % 3x1 B = [1 2 3]; % 1x3 C = A + B; % 3x3,隐式扩展自动广播 D = [A B]; % 报错:维度不一致同一个A和B,加法能算出3行3列,拼接却直接报错。原因很简单:隐式扩展是算术运算的规则,拼接是内存布局的操作,两者的语义完全不同。如果你的代码里同时用了广播式的算术和数据合并,写拼接前一定要用size确认一下维度。别让"加法能通"欺骗了"拼接也该能通"的判断。
5.2 行数相同不等于能对齐:时间序列合并先插值
横向拼接是严格的位置对齐。这在很多场景下没问题,但做传感器数据、时间序列合并时,行数相同只能说明"记录条数一样",不能说明"第k行的数据发生在同一个时刻"。
我实际处理过两套采样率不同的监测数据:一套低频,一套高频,各自先整理到同一个条数之后,直接横向拼接,结果相关性计算变得一团糟——因为两套数据的第100行根本不是同一时刻。这种错误不会报错,却会让所有下游分析失真。
正确做法是先把所有列插值到统一的时间轴上,再做横向拼接。
t1 = (0:0.1:1)'; % 低频采样 t2 = (0:0.05:1)'; % 高频采样 x1 = sin(t1); x2 = cos(t2); t = (0:0.01:1)'; % 统一到更细的网格 x1i = interp1(t1, x1, t, 'linear'); x2i = interp1(t2, x2, t, 'linear'); X = [x1i x2i];如果你是做时间序列的,另一个更省心的方案是直接用timetable,然后用synchronize按时间轴对齐,它会自动处理不同时间点采样的对齐和插值。横向拼接只是工具,什么时候该用、什么时候该先做对齐,才是真正体现工程经验的地方。
5.3 结果承载结构怎么选:matrix、cell还是table
最后聊聊合并结果用什么结构装。这问题没标准答案,但我的选择逻辑很固定,可以参考这个表:
| 合并后的数据特征 | 推荐结构 | 理由 |
|---|---|---|
| 全数值、类型统一、无列名需求 | matrix | 内存紧凑,运算最快,兼容所有数值算法 |
| 类型混杂、每列含义不同但不需要列名 | cell | 灵活,每列独立管理,便于逐列处理 |
| 有列名、行有观测属性、需要统计分析 | table | 列名语义清晰,配合统计函数和分组操作很顺手 |
判断的先后顺序是:先看类型是否统一,类型统一再看是否有列名需求,有列名需求再看行是否代表独立观测。三者都满足,果断用table;只是临时合并算个矩阵运算,matrix足够;类型本身就五花八门,cell是兜底方案。
矩阵和table之间的转换也很成熟,array2table把矩阵转成表格,table2array把表格转回矩阵,代价很小。所以结构选型不用纠结太久,选择一个能让你下游代码最清晰的方式就行,后面随时可以转换。
最后说一个我坚持了很多年的习惯:不管用哪种横向拼接写法,动手前先花十秒钟确认class、size和whos这三件事。类型搞清楚、维度确认好、内存规模心里有数,大部分拼接报错和性能问题都能提前挡在门外。批量场景我永远优先写预分配,没有特殊理由不做循环内拼接;遇到时间序列先对齐再合并,绝不让"行数刚好相等"蒙混过关。这些习惯帮我省下的调试时间,远比当初多写的几行检查代码多。希望这篇能让你从"会用横向拼接"走到"用好横向拼接",批量列数据合并时少走几个我走过的弯路。