☰
数组拼接跨语言指南:从concat到视频流拼帧的工程实践
2026/9/29 17:11:38 网站建设 项目流程

如果你去问一个前端新人:数组拼接用哪种方式?他大概率会回答concat。再问一个写了三年 Java 的同事,他多半会说用 Stream 更优雅。Python 这边,则可能是“直接加号就行”。答案五花八门,但真正上手拼大数组、拼完还要做去重和分割的时候,很多人会发现:自己的写法不是不行,只是要么慢得离谱,要么莫名其妙把原数组改坏了。

这篇文章想把这些坑系统梳理一遍,从 JavaScript、Python、Java、C++ 这些常用语言说起,涉及拼接的语义差异、性能开销,以及拼完之后经常要做的去重、分割、对象数组提取字段等联动操作。最后会聊到视频帧缓冲、多路数据流这类“数组拼接”在真实系统里的延伸场景——别觉得夸张,等你写过一个视频流拼帧模块,再回头看数组拼接,会明白很多道理是通用的。这篇文章适合正在写数据处理逻辑、前端列表操作、后端聚合接口的人参考,尤其是那些被“拼接”这个简单动作坑过的开发者。

1. 先搞清楚拼接到底在拼什么:维度、引用与类型的三个暗坑

很多人觉得数组拼接是个低阶操作,不值得深入讨论。但说实话,我在代码评审里看到的拼接错误,大部分根本不是不会写,而是没想清楚三个问题:维度对不对、复制的是值还是引用、目标数组的类型约束是什么。这三个问题不解决,换什么语言都会踩坑。

1.1 维度没对齐:一维拼二维的经典翻车现场

先说维度。一维数组拼一维数组,结果是更长的数组;二维数组拼一维数组,结果可能是把一维数组当成一个整体塞进去。这个差异在不同语言里表现还不一样。

举一个最典型的场景:你有一个“多个班级的学生 ID 列表”,每个班级是一个数组,你想把所有班级的学生 ID 合并成一个总数组。用 JavaScript 写:

const classes = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]; // 想得到 [1,2,3,4,5,6,7,8,9] const flat = classes.reduce((acc, cur) => acc.concat(cur), []);

这样得到的是拍平的一维数组。但如果你写成:

const wrong = classes.concat(10); // 得到 [[1,2,3],[4,5],[6,7,8,9],10]

问题就出来了:数字 10 被当成一个独立元素拼到二维数组末尾,而不是追加到某个一维数组里。这看起来是小事,但在动态拼接的场景里,比如循环里先 build 出一个子数组再 concat 进总数组,维度一变,后面所有下标访问和遍历逻辑全乱。

Python 也有同样的情况。list + list是浅层拼接,但如果你用numpy,np.append默认会把输入 flatten 成一维,而np.concatenate则严格要求所有输入维度一致。我记得有段时间我习惯用np.append做累积拼接,结果每次都得检查维度,后来干脆全部改成先收集成列表再用np.concatenate。这是经验之谈:批量拼接之前,先确认每个分块的 shape 一致,别让维度问题在下游暴露。

1.2 引用复制:你拼的不是值,是指针

第二个坑更隐蔽:很多语言里数组拼接复制的是引用而不是深层数据。这意味着拼接后的数组和原数组共享了同一批对象,改一边,另外一边也跟着变。

以 JavaScript 为例:

const a = [{id: 1}, {id: 2}]; const b = [{id: 3}]; const c = a.concat(b); c[0].id = 999; console.log(a[0].id); // 999,a 也被改了

原因很简单:concat复制数组元素时,如果元素是对象,复制的是对象的引用。你要的是“新数组”,但不是“新对象”。想彻底切断关联,得手动做深拷贝,比如structuredClone或者逐项展开重新构造。

Java 里的System.arraycopy、Arrays.copyOf也一样,复制的是引用数组,对象本身没有复制。C++ 里默认拷贝构造函数浅拷贝也会导致同样的悬挂引用问题,但如果你用std::vector且元素是std::string,则拷贝语义是值拷贝,反而没有这个烦恼。

所以,一个实用建议是:拼接前想清楚,后续会不会修改数组里的对象;会修改且不想影响原数组,就提前写好深拷贝逻辑,别指望拼接函数帮你兜底。

1.3 类型约束:强类型语言里的拼接安检

最后一个暗坑是类型。像 C++、Java 这类强类型语言,数组类型是元素类型的一部分。int[]拼接long[],很多语言直接不让编译;Pythonlist可以装任意类型,但numpy数组不行,np.concatenate遇到 dtype 不一致时会尝试自动转换,大概率转成更大的类型,比如int+float变成float,如果int是 64 位而float是 32 位,还可能出现精度损失。

我实际遇到过一个挺离谱的场景:一个 C++ 项目里,有人用std::vector<int>和std::vector<unsigned int>拼接,编译器直接报模板实例化错误,排查半天发现是类型不匹配。后来统一改用int64_t才解决。这不是 C++ 不够灵活,而是它把类型约束写在了明面上——这其实是在帮你避免潜在的数据截断问题。

所以,拼接之前先看一眼两边的元素类型。跨类型拼接不是不能做,但要明确转换规则,别让语言默认的隐式转换替你“背锅”。

2. 不同语言里的常规拼接姿势与隐藏差异

清楚了维度、引用和类型这三个基础问题之后,我们来逐个过一下主流语言的常用拼接方式,以及它们各自最适合什么场景。

2.1 JavaScript:concat、push、splice 的边界感

JavaScript 的数组拼接方法,说多不多,说少也不少,常见的有concat、push、splice、unshift,ES6 之后还有展开运算符...。

  • concat:返回新数组,不改动原数组,适合链式写法和纯函数式风格。
  • push(...arr):原地修改,性能上通常优于concat一点点,因为少一次新数组分配。
  • splice(start, 0, ...arr):可以在任意位置插入整段元素,本质上也是拼接。
  • arr1.push(...arr2)有个注意点:如果arr2特别大,展开运算符在传参时可能碰到参数数量上限,虽然现代引擎基本都能处理百万级,但保守起见可以用push.apply(arr1, arr2)或循环逐一 push。

我自己的经验是:声明式代码用 concat,性能敏感路径用 push,需要指定插入位置的时候用 splice。尤其在做链式数据处理时,concat更安全,因为不会污染原数组变量;写高频循环里的累积拼接时,push更快,因为避免了反复创建临时数组。

另外,二维数组拍平为拼接也是一种常见需求。arr.flat(Infinity)虽然能拍平任意维度,但在特定场景下用reduce+concat更可控。比如多路数据流按帧号压入分组数组,最后要合并成总数据序列时,flat和reduce结果一样,但flat对稀疏数组和深层嵌套的处理更激进,容易把不该拍平的结构也拍掉。

2.2 Python:extend、+ 与 numpy 拼接的不同哲学

Python 常规列表拼接有几种方式:

a = [1, 2, 3] b = [4, 5, 6] # 方式1:+ c = a + b # 返回新列表,a/b 不变 # 方式2:extend a.extend(b) # 原地修改 a # 方式3:列表推导式 c = [x for sub in [a, b] for x in sub]

+和extend的区别就是新数组 vs 原地扩展。如果后续还要继续操作原列表,extend更省内存;如果不希望原数据被改,用+更安全。

关键字是在处理数值计算时,Python 原生 list 拼接只是“元素堆叠”,想就地对批量数据做矢量运算,就得用 numpy:

import numpy as np a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6]]) c = np.concatenate([a, b], axis=0) # 竖直方向拼接 d = np.concatenate([a, b.transpose()], axis=1) # 如果维度不匹配会报错

numpy的拼接必须沿着某个axis,而且要求除去该轴之外的其他维度完全一致。这在处理三维数组相乘、二维特征矩阵合并这类任务时几乎是必经之路。顺便提一句热词里的“numpy三维数组相乘”,如果你要做两个三维数组的批量矩阵乘,多半是先对若干二维矩阵切片做拼接或堆叠,再用np.matmul。顺序其实很关键,先拼好再乘,比先乘再拼更容易规避广播错误。

2.3 Java 与 C++:静态语言里的数组搬运术

Java 侧,经典做法是System.arraycopy,它是最快、最低层的数组复制手段。Arrays.copyOf则适合从旧数组扩展出一个新数组。集合层面用ArrayList.addAll最方便。还有一个高级玩法是 Stream:

Integer[] a = {1, 2, 3}; Integer[] b = {4, 5}; Integer[] c = Stream.concat(Arrays.stream(a), Arrays.stream(b)) .toArray(Integer[]::new);

Stream 的优点是可以用在链式过滤、映射后拼接,但性能上不如System.arraycopy。如果你的接口只是负责把两个数组拼起来返回,不涉及后续流式操作,直接arraycopy就好。

C++ 这边,std::vector的insert是最直接的:

std::vector<int> a = {1, 2, 3}; std::vector<int> b = {4, 5}; a.insert(a.end(), b.begin(), b.end());

如果想在头部拼接,insert(a.begin(), b.begin(), b.end())也可以,但性能上不如尾部拼接,因为后续元素都要移动。实际开发中,如果是多维数组、指针数组,一个很常见的需求是“拼接两个指针数组”,这时需要手动分配连续内存然后逐个拷贝指针,别忘了释放旧内存。

C++ 里还有个容易被忽视的点:判断是否使用reserve预分配。如果不 reserve,vector 会在插入过程中多次扩容,导致性能明显下降。更隐蔽的问题是,扩容会让所有指向元素的迭代器和引用失效,很多人拼完后发现以前保存的指针变空了,就是没提前 reserve 或拼完没重新取地址。

2.4 Excel/VBA 里的数组拼法:表格世界同样需要拼接

别以为数组拼接只存在于编程语言。Excel 里提取两列数据匹配成一个数组、VBA 数组之间的搬运,都是常见需求。VBA 里没有直接的“数组拼接”函数,通常的做法是:

Dim arr1 As Variant, arr2 As Variant arr1 = Range("A1:A10").Value arr2 = Range("B1:B10").Value ' 转化为一维再合并 Dim result() As String ReDim result(1 To UBound(arr1) + UBound(arr2))

这里的核心思想是:先把二维数组(因为 Excel Range 读出来天然是二维的)转成你想要的结构,再手动 ReDim 一个足够大的结果数组循环赋值。没有原生 concat,所以尺寸推断和循环效率很重要。

热词里有一条“Excel 提取前两列匹配的数据成一个数组”,其实就是先用Application.Match或循环匹配每一行,把匹配结果写入新数组。这种场景下,善用Application.Index可以从二维数组里提取单列,再拼进结果数组,比一行行在单元格里读快很多。

3. 拼接的性能账:复制、扩容与预分配的真实差距

很多开发者以为数组拼接只是“把两个数组倒进一个容器”,性能差别不大。但实测下来,不同写法的耗时可以差出几个数量级。下面拆开算算这笔账。

3.1 循环里拼接为什么越拼越慢

最常见的性能杀手是:在 for 循环里反复拼接同一个数组。

// 反面案例:循环里用 concat let result = []; for (let i = 0; i < 10000; i++) { result = result.concat(subArray); // 每次都生成一个全新的数组! }

问题在于,concat每次都会重新分配一块内存,把旧数组的所有元素和新数组所有元素都复制一遍。假设subArray长度是 10,循环到第 n 轮时,result 里已经有 n*10 个元素,所以总复制量是 10 + 20 + 30 + ... + 100000,约等于 5 亿次复制。当然实际引擎可能会做一些优化,但这个量级依然非常可怕。

相比之下,用push并且预分配容量(JavaScript 里没有原生 reserve,但可以预先new Array(totalLength)然后按下标填),复制量是线性的,差距就出来了。

同样的逻辑也适用于 Python:

# 慢 result = [] for item in arr2: result = result + [item]

每次result = result + [item]都会生成一个新列表,O(n^2) 的复杂度。改成result.append(item)或者先用列表推导式收集,再一次性拼接,就是 O(n)。这一条规则几乎适用于所有语言,可以记住:不要在循环体里做数组的“整体拼接”,要么用原地扩展,要么先收集再一次性合并。

3.2 预分配:数组扩容的省力技巧

说到 C++ 的reserve、Java 的ArrayList(capacity)或System.arraycopy前的new int[totalLen],本质都是“提前告诉容器我要这么大,避免中间反复扩容”。

比如你从数据库里查了两批数据,已知第一批 100 条、第二批 80 条,要把它们合并进一个ArrayList,直接写:

List<Integer> all = new ArrayList<>(180); // 提前指定容量 all.addAll(list1); all.addAll(list2);

如果没写180,ArrayList可能扩容两次以上,每次扩容都要把旧数组搬进新数组。数据量大时,这些拷贝就是纯浪费。

Python 的列表没有预分配 API,但你可以用[None] * total提前占位,再按下标切入赋值,或使用array模块。其实 Python 的append本身已经做了动态扩容摊销,单次添加是 O(1) 均摊,所以一般不需要手动预分配。真正的瓶颈是+和concat这类一次性复制。

3.3 系统级拼接:numpy 与内存连续性的威力

到了数值计算这块,python list 拼接会被 numpy 甩开好几个量级,核心原因是内存连续性。numpy数组在内存里是连续的一块缓冲区,np.concatenate只需要分配新缓冲区然后 memcpy 级别的批量拷贝;而 Python list 里存的是 PyObject 指针,拼接时要逐个处理引用计数和对象检查。

实际做个简单的性能对比,往一个数组里累积拼接 10000 个长度为 10 的小数组:

  • Python listextend:几十毫秒级别
  • numpynp.concatenate在循环里调用:慢到离谱,因为每次调用都要检查 shape、分配新缓冲,numpy 也怕循环内拼接
  • 正确姿势:先收集小数组到一个 list,循环外只调一次np.concatenate:毫秒级

我之前在做一个特征工程模块时,要拼接 5000 个 128 维特征向量,一开始在循环里np.vstack,跑了十几秒;改成先把所有向量放进一个 list,最后统一np.concatenate,瞬间降到不到 0.1 秒。这是一个非常典型的误区,大家在网上搜“numpy 三维数组相乘”或“python 数组拼接”时,很多教程没说清楚这一点。

4. 拼接之后的事:去重、分割、筛选与对象数组的联动处理

数组拼接很少是终点。拼完之后,你大概率还要做去重、按条件分割、筛选包含某个字符的元素,或者从一堆对象里提取指定字段。这一节把这些“组合拳”拆开讲。

4.1 去重:Set 不是唯一答案

最常见的是数组合并后去重。JavaScript 一行流:

const merged = [...a, ...b]; const unique = [...new Set(merged)];

优点是好写,缺点是结果顺序保持的是首次出现顺序,符合大多数场景。但如果元素是对象,Set 的去重比较的是引用,不是深层结构,此时就要换思路。

如果元素是数值型且需要高性能去重,可以试试排序后双指针扫描:

const sorted = merged.sort((x, y) => x - y); let write = 0; for (let read = 1; read < sorted.length; read++) { if (sorted[read] !== sorted[write]) { sorted[++write] = sorted[read]; } } sorted.length = write + 1;

这在数组已经接近有序时会非常快;但如果是无序大数组,排序的开销可能比 Set 大,所以实际要按数据分布选择。

Python 侧的去重也一样,set(merged)是最简单的;需要保持顺序时可以遍历加seen集合。如果数组元素是 numpy 数组,不能直接用 set,得借助结构化视图或 pandas 的drop_duplicates,这又是另一个话题了。

4.2 分割与筛选:条件切片的高效做法

拼接之后按条件分割,典型需求是“从数组中找出包含某个子串的元素”“把大于某个阈值的元素分到一组”。JavaScript 里是filter:

const merged = [...a, ...b]; const matched = merged.filter(item => String(item).includes("目标"));

这个写法很直观,但要注意:如果这个 filter 后面还要继续 map,那每次都会遍历新数组。数据量大时可考虑单次遍历同时完成过滤和映射,减少一次循环。

Python 里对应的是列表推导式:

matched = [x for x in merged if "目标" in str(x)]

如果处理的是 numpy 数组,可以用布尔掩码:

mask = np.char.find(arr.astype(str), "目标") >= 0 matched = arr[mask]

这里有个细节:np.char.find对多维数组的处理跟字符串元素数量有关,转换 str 时的性能要提前评估。数据量大时,先转 dtype 再掩码通常比逐元素 Python 循环快很多。

4.3 对象数组:提取字段与重组结构

拼接的对象数组往往还要提取某些字段重新组结构。ES6 里就是map加解构:

const merged = [...userListA, ...userListB]; const ids = merged.map(u => u.id); const keyed = merged.map(({id, name}) => ({label: name, value: id}));

Java 里同样可以用 Stream 完成:

List<User> merged = new ArrayList<>(listA); merged.addAll(listB); List<Integer> ids = merged.stream().map(User::getId).collect(Collectors.toList());

这种“拼接 + 字段提取”不仅逻辑清晰,还天然契合不可变风格。但是别忘了,如果merged里的对象可能重复且你希望去重后再提取,顺序应该是拼接 -> 去重 -> 提取,反过来提取后再去重往往会把结构信息丢掉。

对象数组去重是热词里反复出现的一个点。比如:两个接口分别返回用户基本信息和管理信息,你拼成一个数组后想按用户 ID 去重。Set 对对象没有效果,可借助Map:

const map = new Map(merged.map(u => [u.id, u])); const unique = [...map.values()];

这个技巧在很多代码库里都能看到,核心是利用Map的键唯一性去重对象数组,同时保证最后一个出现的对象覆盖前面同 ID 的。

5. 越出数组边界:帧缓冲拼接与分块数据的“数组思维”

写到这里,如果你以为“数组拼接”只是语言层面的 API,那就低估了它。很多看起来和数组无关的系统设计,核心其实就是在拼数组——最常见的就是视频流的帧缓冲拼接,以及地图瓦片类的分块数据处理。

5.1 多路视频流的“拼帧”本质

热词里有“多部同视角枪机 视频流拼接系统,实现一屏总览”“视频无缝拼接融合”,很多人觉得这是算法活,其实第一步就是数组拼接。举个例子:假设你有 4 路摄像头,每个摄像头每秒生成 25 帧,每帧的编码数据是[tag, timestamp, blob]结构。你要合并成一屏总览,那就得先把这 4 路的帧全部按时间戳对齐后拼成一个新数组,作为渲染循环的输入。

这个“按时间戳对齐”过程,本质上就是多个有序数组的归并拼接。你可以把所有帧一次性收进一个大数组再排序,但更好的做法是类似归并排序的指针法:每路一个指针,取时间戳最小的帧进入输出数组,然后移动对应指针。这就是多个有序数组的无损拼接。

如果有重叠时间段的帧,还得决定是取主码流还是做像素级融合,但至少从“数组拼接”视角看,先把对齐后的数据拼成完整序列,再交给后续的融合算法,模块边界就清晰了。我见过不少做视频拼接的项目,代码卡在“怎么把多路帧高效合并成一条完整帧序列”上,最后发现换成归并拼接 + 预分配输出数组,性能直接翻倍。这类经验对做视频流处理的人很有参考意义。

5.2 无缝拼接的关键:对齐、重叠与融合

“无缝拼接融合”听上去很难,拆开讲也无非三点:对齐、重叠区、融合策略。

对齐就是前面说的按时间戳或按空间坐标,把多段数据放到同一个坐标系下。重叠区要解决的是“两块数据在交界处怎么过渡”:如果是视频帧,常用做法是重叠区做加权平均或金字塔融合;如果是音频波形拼接,重叠区做交叉淡化;如果是文本语义拼接,重叠区做窗口重叠,比如 N-gram 重叠保证上下文连贯。

从数组角度看,这些都对应一个操作:对拼接处的重叠区间做逐元素组合运算。理解这一点后,你可以在任何语言里抽象出一个通用函数:输入两个数组和一个重叠窗口大小,输出一个拼接后并处理了重叠区的数组。视频拼接系统里那些看似复杂的“融合”,落到数据结构层面就是这个。

5.3 地图瓦片、分块下载与“拼图式”构造

另一个典型场景是热词里的“地图拼接工具 map-download”。下载大量地图瓦片后,最直观的展示方式是拼接成大图。这里的“数组拼接”对象不再是数字,而是图像矩阵。每张瓦片是[H, W, C]的 numpy 数组(或等价矩阵),按网格拼接就是:

row_slices = [np.hstack(row_tiles) for row_tiles in tiles_rows] full_map = np.vstack(row_slices)

这个玩法之所以能成立,是因为图像在内存里本来就是二维/三维数组,拼接即矩阵拼接。同理,那些“Excel 提取前两列匹配数据成一个数组”“图片拼接 python”之类的工作,本质都是把表格行、图像块视为数组元素做组合。

所以你会发现,一旦有了数组思维,很多看似无关的需求都能快速定位到同一套解决方案:先确定拼接维度,再处理重叠和去重,最后统一分发。这才是“数组拼接”这个主题背后真正值钱的东西。

就我个人经验,数组拼接的难点从来不是 API 记不记得,而是你对内存布局、性能开销和后续操作有没有通盘考虑。拼之前先问自己三个问题:维度对不对,引用会不会串改,类型会不会隐式转换;拼的时候优先原地操作或预分配;拼完之后再决定要不要去重、分割、提取字段。这套流程你在 JavaScript、Python、Java、C++ 里各走一遍,以后遇到再复杂的拼接需求都不会慌。希望这篇文章能帮你少走一些我当年走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询