数组这词几乎出现在每一场技术面试、每一段业务代码里。很多人看到“高频-数组”这个标题会觉得太宽泛但如果你把搜索词摊开看数组初始化、js数组排序的几种方法、指针数组、动态数组、python数组切片、numpy三维数组相乘、树状数组模板、vba数组对比最快、二维字符数组、JSON数组……会发现“数组”从来不是一个孤立的知识点它是算法、语言特性、内存布局和业务处理四条线的交汇点。前端在搜去重和排序后端在搜多维数组和指针数组刷题的人在搜区间最大值做报表的人在搜Excel数组公式。这篇文章不是教科书而是我把这些年写C/C、Java、Python、JavaScript偶尔还要碰VBA和数组公式的经验攒成的一套“高频-数组”实操手册适合刚入行的同学补基础也适合老手当查漏补缺的备忘录。1. 数组为什么是程序员绕不开的“高频”主题1.1 从热搜关键词看数组的覆盖面我特意把这些热搜词归类看了一下发现一个规律数组的高频场景其实只有三类。第一类是“语言基础操作”比如数组初始化规则、指针数组、二维数组、动态数组、字符串数组初始化这类问题往往是面试第一轮考察候选人基本功的入口。第二类是“常用算法与业务处理”比如数组去重、排序、删除指定元素、数组转字符串、对象数组去重、求区间最大值这些是每天写业务代码都要面对的。第三类是“特定工具链的坑”比如VBA数组对比、Excel提取前两列匹配数据成数组、CString转char数组这些词说明很多人是在真实干活时卡住了不得不搜解决办法。所以“高频-数组”不是一个单一项目而是一个“解题方法包”。前端后端都要用算法题和业务逻辑都要用甚至做数据分析、做Excel报表也躲不开。理解了数组的本质你在不同语言之间切换时很多概念是平移的。1.2 数组的核心本质连续内存与随机访问数组最根本的特性是“连续内存”。无论在C、Java还是C里一维数组都是一段连续的内存空间。比如C语言中int a[5]a[2]的地址可以直接用首地址加上2 * sizeof(int)算出来所以数组按下标访问是O(1)的随机访问。这个特性是所有数组操作性能分析的基础。但不同语言对这个“连续”的实现有差异。C/C和Java的基本类型数组是真正连续的元素存储Java的Integer[]数组存的是引用实际对象散落在堆里但引用数组本身连续Python的list底层是“PyObject指针数组”每个元素都是指向真实对象的指针所以list才能装不同类型的数据但也意味着一次访问要多一次间接寻址。JS的数组本质上是对象它更接近一个“哈希表的类数组容器”虽然大多数引擎会优化成连续存储但理论上它的连续性不如C数组那么严格。还有一个容易被忽略的点静态数组长度固定动态数组自动扩容通常按1.5倍或2倍申请新内存再复制。不理解这一点后面讨论多维数组、切片、树状数组时总会觉得哪里别扭。2. 多语言数组初始化与内存布局坑都在这2.1 静态数组与动态数组C/C的初始化差异C语言里最容易被“初始化”这个词坑到。很多人写int arr[10] {0};以为只是把第一个元素赋成0其实C标准规定只要初始化列表中有一个元素剩余未指定的元素会被自动补0所以{0}是清空整个数组的最简写法。宏定义数组#define SIZE 10、int a[SIZE]是在编译期就确定的没有运行期长度问题。C里推荐优先用std::array或std::vector。std::arrayint, 10 arr{};能把数组全部初始化为0std::vectorint vec(10, 0)则是10个0。真正容易翻车的是动态数组new int[n]它只分配内存不初始化里面的值是随机的。很多线上bug就是忘了std::fill或者memset。这里必须强调一个高频坑函数传数组后sizeof失效。看这段代码void clear(int arr[]) { memset(arr, 0, sizeof(arr)); // sizeof(arr)被当成指针大小通常是8字节 }当数组作为函数参数时它会退化成指针sizeof(arr)不再是整个数组的长度。我见过多次因为这个问题只清除了前8字节导致数据残留的bug。解决办法是额外传入长度或者直接用std::array、std::vector。2.2 Java、Python、JS数组初始化的规则对比Java的数组初始化看起来简单但仍有细节。int[] arr new int[10]会把基本类型数组初始化为0引用类型数组初始化为null。int[] arr2 {1,2,3}是语法糖。需要注意的是Java数组的length是属性不是方法写arr.length()的人一定踩过坑。Python里没有真正意义上的“固定数组”最常用的是list。一维初始化可以用[0] * n对不可变对象是安全的。二维数组推荐列表推导式matrix [[0] * 4 for _ in range(3)] # 正确 bad [[0] * 4] * 3 # 错误三行指向同一个列表[[0]*4]*3这个坑太经典了改一个元素全会变。因为*操作复制的是引用不是新建内存对象。热搜里“python创建数组元素为汉字”其实很简单Python3默认字符串是Unicode直接写[汉, 字]或list(汉字)就能得到字符数组。JS数组初始化的坑是稀疏数组。Array(3)只创建一个长度为3的空壳里面没有实际元素而[undefined, undefined, undefined]是三个真实元素。两者在map、forEach时行为不同。要生成密集数组推荐Array.from({length: 3}, () 0)。2.3 指针数组、数组指针与多维数组的“表象”C/C里“指针数组”和“数组指针”是面试高频送命题。指针数组是指数组里存的是指针比如char* argv[]数组指针是指向数组的指针比如int (*p)[10]。写法上括号决定了含义。二维数组int a[3][4]传入函数时形参必须写成int (*a)[4]内层维度不能省略。因为编译器要按内层长度计算行偏移。三维数组int a[2][3][4]同理只有第一维可以省略。很多人被多维数组搞晕本质上是因为C数组是“数组的数组”内存上仍是连续按行优先存储。相比之下Python的numpy多维数组引入了轴axis的概念。shape(2,3,4)的三维数组a[1, :2, ::2]这种切片需要从后往前理解轴顺序。numpy默认C顺序row-major存储也可以用orderF指定列优先。我建议新手先不要背“维度就相当于几层循环”而是直接把数组画成一个立方体操作哪个轴就沿哪个方向切。3. 高频数组操作实战排序、去重、删除、切片、转换3.1 JS数组排序的几种方法与去重套路JS的sort方法绝对是高频搜索词。默认排序会把元素转成字符串再按字典序排所以[10, 1, 2].sort()的结果是[1, 10, 2]。要排数字必须传比较函数const numbers [10, 1, 2]; numbers.sort((a, b) a - b); // [1, 2, 10]面试时还会问“sort会不会改变原数组”。会而且是原地排序。如果不想改原数组先拷贝再排序[...numbers].sort((a,b)a-b)。数组去重最简单的是Setconst arr [1, 2, 2, 3, 3, 3]; const unique [...new Set(arr)]; // [1, 2, 3]但如果去重的是对象数组直接用Set就没用了因为引用不同。更稳妥的做法是按唯一标识Map去重const arr [{id: 1, name: a}, {id: 1, name: b}, {id: 2, name: c}]; const map new Map(); const unique arr.filter(item !map.has(item.id) map.set(item.id, true));3.2 JS删除指定元素的可靠姿势热搜“js数组删除指定元素”对应的场景非常多。删除指定下标用splice删除指定值要先找到下标const arr [10, 20, 30, 20]; const index arr.indexOf(20); if (index -1) arr.splice(index, 1); // 删除第一个20indexOf只删除第一个匹配项。要删除所有匹配项可以用filterconst arr [10, 20, 30, 20]; const filtered arr.filter(x x ! 20); // [10, 30]但如果必须在原数组上删除所有匹配反向遍历for循环再splice或者while循环配合indexOf。反向遍历的原因是splice会使后续元素下标前移正序遍历会跳过元素。3.3 Python数组切片与numpy多维运算Python的切片语法是seq[start:stop:step]对字符串、list、tuple都适用。几个高频写法a[::-1]反转整个序列a[::2]取偶数位置a[-1]取最后一个元素a[1:3]取下标1到2。注意list切片返回的是一个新列表修改切片不会影响原列表但numpy数组切片返回的是原数组的视图修改切片会改变原数据。numpy三维数组相乘是深度学习里的高频操作。np.matmul或支持批量矩阵乘法假设有两个三维数组x形状为(2,3,4)、y形状为(2,4,5)x y的结果形状是(2,3,5)相当于把两批矩阵对应相乘import numpy as np x np.random.rand(2, 3, 4) y np.random.rand(2, 4, 5) z x y print(z.shape) # (2, 3, 5)首次接触时容易把轴搞反。记忆方法是去掉第一维批量维度后剩下的矩阵乘法规则照常最后两维满足(..., M, K) (..., K, N) - (..., M, N)。3.4 VBA数组对比提速与Excel数组公式妙用Excel VBA里操作单元格是性能杀手。比如几百行数据要逐格比对直接读写Range会慢到让人怀疑人生。最快的方式是一次性把区域读进VBA数组在内存里处理完再写回。示例Dim arr As Variant arr Range(A1:C1000).Value 数组维度默认从1开始arr(1,1)对应A1单元格热搜“vba数组对比最快”通常是指两列数据找匹配。把两列分别读入VBA数组然后用字典做匹配比嵌套循环快一个数量级Set dict CreateObject(Scripting.Dictionary) For i 1 To UBound(arr1) dict(arr1(i, 1)) i Next iExcel里“提取前两列匹配的数据成一个数组”可以直接用新函数FILTER和XLOOKUP旧版本则需要数组公式比如INDEX配合SMALL和IF三键结束。我的经验是如果数据量不大直接用辅助列反而更清晰数据量大才考虑数组公式或VBA。4. 高频数组算法题从暴力到优雅4.1 数组求区间最大值滑动窗口、线段树与ST表“数组求区间最大值”是算法面试常客但很多人不知道要区分场景。静态数组多次查询区间最值用ST表Sparse Table预处理后O(1)查询动态修改加区间最值用线段树固定滑动窗口找最大值用单调队列O(n)。单调队列是面试高频LeetCode 239题的解法骨架from collections import deque def maxSlidingWindow(nums, k): dq deque() res [] for i, v in enumerate(nums): while dq and nums[dq[-1]] v: dq.pop() dq.append(i) if dq[0] i - k: dq.popleft() if i k - 1: res.append(nums[dq[0]]) return res核心思想是维护一个“从队头到队尾下标递增、值递减”的队列窗口滑动时队头就是当前最大值。我第一次写的时候总忘记先移除过期元素结果最大值一直是旧窗口里的调试半天才反应过来顺序问题。4.2 三个数的最大乘积排序与线性扫描热搜词写的是“三个数组最大的乘积”但大多数人其实是在问经典题“三个数的最大乘积”输入是一个数组。解题思路很直接要么选三个最大的正数要么选两个最小的负数和一个最大的正数因为负负得正。排序法最简洁def maximumProduct(nums): nums.sort() return max(nums[-1] * nums[-2] * nums[-3], nums[0] * nums[1] * nums[-1])如果是真正“三个数组分别取一个数求最大乘积”就要多考虑每个数组的正负分布。两个负数配合正数也可能成为最大值。处理方式是每个数组返回最大、最小两个候选然后枚举有限的组合通常只需要每个数组的最大值和最小值参与计算。4.3 树状数组模板与常见应用场景热搜“树状数组模板”说明这个数据结构几乎是算法竞赛必备。树状数组Fenwick Tree支持单点更新和前缀求和代码非常短int n, bit[N]; void add(int i, int x) { for (; i n; i i -i) bit[i] x; } int sum(int i) { int s 0; for (; i 0; i - i -i) s bit[i]; return s; } int range_sum(int l, int r) { return sum(r) - sum(l - 1); }i -i取的是二进制最低位的1这个lowbit是整个数据结构的灵魂。树状数组下标必须从1开始因为0的lowbit永远为0会死循环。应用场景包括动态维护前缀和、求逆序对、区间频率统计。注意树状数组本身求任意区间最值比较麻烦如果有区间最值需求我更推荐线段树。4.4 JSON数组、对象数组去重与Java数组工具类前后端联调时JSON数组是高频数据形态。接口返回的字符串经过JSON.parse后变成JS数组但对象数组里如果有嵌套对象直接打印会发现所有行都“长得一样”其实是引用同一对象的坑。这时要深拷贝比如JSON.parse(JSON.stringify(arr))。Java后端处理数组的“方法”其实在java.util.Arrays里sort、binarySearch、copyOf、equals、deepToString等。数组本身没有API只有一“些工具类方法。比如Arrays.stream(arr)把数组转流配合Collectors.toList()实现数组转List但要小心Arrays.asList返回的是固定大小列表不能add。另外“group()数组java”应该是指Java 8的Collectors.groupingBy对流元素按某个字段分组等价于把数组转成MapString[] arr {a, b, c}; MapBoolean, ListString groups Arrays.stream(arr) .collect(Collectors.partitioningBy(s - s.length() 1));数组和高频业务操作永远是连在一起的学会工具类能省不少事。5. 数组相关的典型问题与排查技巧实录5.1 数组越界、下标混乱与“多一位”陷阱C语言数组越界不会直接报错可能悄悄破坏相邻内存这类bug最难查。Python的list越界会抛IndexError但切片不会所以很多新手把a[1:3]用在不存在的位置上结果返回空列表不报错反而误导。真正需要警惕的是C字符串的“多一位”char str[3] abc;看起来刚好但字符串末尾有隐式\0实际需要4字节。搜索“二维字符数组”的人很多也在初始化时栽过char s[3][10]每行长度要预留\0位置。排查这类问题我的习惯是先检查下标是从0还是1开始再检查循环边界是还是最后看长度单位是字符数还是字节数。多一位少一位的错误几乎都藏在这三个点里。5.2 C/C数组作为函数参数后退化成指针这是C/C面试必问的点。数组传入函数后sizeof失效arr会移动一个元素大小而不是整个数组。更重要的是形参int arr[]和int* arr在函数内完全等价。正确做法是同时传长度或者使用指针加长度结构。C20之后std::span可以更安全地表示一段连续数组void process(std::spanint arr) { for (int v : arr) { v; } }我实测用std::span后很多原来需要手写长度参数的地方都变清爽了而且天然防越界。5.3 字符串数组、CString转char数组等处理心得热搜里“cstring转char数组 函数”大概率来自MFC开发。MFC的CString转char数组最稳妥的方式之一CString str test; char buf[64] {0}; strcpy_s(buf, sizeof(buf), CT2A(str));C标准库则直接std::string s test; const char* p s.c_str(); // 只读如果想拷贝到数组用strcpy_s字符串数组和普通数组最大的区别是末尾的\0凡是涉及内存拷贝都要在目标数组预留结束符位置。我一直建议团队统一封装一个copyStringToBuffer函数避免每个人写各自的strcpy出错。6. 我的几条数组使用心得数组这个话题看起来门槛低但能考的东西其实非常深。我做技术评审时看到候选人能清晰讲出“这个数组是定长还是动态、元素是值还是引用、是否会发生指针退化”基本就能判断他对内存模型有概念。实际开发里我给自己定了三条规则第一能用std::vector、ArrayList这类动态容器就不裸用C数组第二凡是做切片、拷贝先搞清楚是“视图”还是“新对象”这直接影响后续修改会不会互相影响第三算法题里涉及区间查询时先问自己数据是静态还是动态再决定用ST表、单调队列还是线段树。最后分享一个检查小技巧在C/C里写完数组相关代码先查所有下标边界在Python里写完切片思考一下切片结果是不是指向原数据。数组的坑多数不是语法不会而是“你以为你操作的是副本实际是原数据”或者反过来。养成先确认“引用关系”的习惯数组相关的高频问题能少踩一半。