
昨晚刷LeetCode的时候我习惯性地点开了第1929题“数组串联”Concatenation of Array难度标着“简单”。说实话第一次刷这道题的人大概率会觉得它简单到像在送分给一个长度为 n 的数组 nums要求返回一个长度为 2n 的数组 ans前 n 个元素是 nums后 n 个元素也是 nums本质上就是把同一个数组首尾相接拼两遍。但简单题不等于没有营养。尤其是对准备面试、想系统整理刷题笔记的人来说1929是整个数组专题里最容易被低估的“地基题”。它表面上只考了一个数组拷贝实际上却牵扯出循环索引映射、API 底层实现、复杂度口径、同类题型串联等一系列值得展开的内容。这篇文章我会从一道“三分钟 AC”的题出发讲讲它背后的这些东西适合刚开始刷题的新手也适合刷了几十道但总感觉知识点零散的中段选手。1. 题目到底在说什么用例和约束先看懂1.1 原题描述里的三个关键信息原题描述通常长这样Given an array nums of length n, return an array ans of length 2n where ans[i] nums[i] and ans[i n] nums[i] for 0 i n。翻译成人话就是输入是一个长度为 n 的数组 nums。输出是一个长度为 2n 的新数组 ans。映射关系有两个前 n 位照搬 nums后 n 位也照搬 nums。看两个例子就非常清楚了输入[1,2,1]输出[1,2,1,1,2,1]输入[1,3,2,1]输出[1,3,2,1,1,3,2,1]我自己做题有个习惯拿到这种题先别急着写代码拿笔在纸上把输入输出画一遍。你会发现这道题要的根本不是什么算法而是“数组长度翻倍后下标如何映射回原数组”。一旦把这个映射关系想明白了代码只是顺手的事。1.2 为什么周赛的Q1偏爱这种题标题里有个“Q1”老刷题人看到这个词都会心一笑。无论是周赛、双周赛还是某些模拟赛第一题往往都是这个难度水平。它存在的目的很简单让你用三分钟进入竞技状态把紧张感降下来同时检查最基础的语法敏感度——数组怎么声明、循环边界怎么写、索引怎么映射。不要觉得Q1没区分度。我见过有人因为把长度为 2n 的数组写成了new int[n]直接 WA 的也见过有人纠结“到底要不要用取模”而多想了五分钟。一道 Q1其实已经在偷偷筛选“读题是否仔细”的人。LeetCode 热门 100 题里也有类似定位的题放在列表最前面不是为了难住谁而是让你起步的时候走稳一点。1.3 简单题对新手的第一课读题比写代码重要这个题没有任何复杂的算法但有个非常典型的陷阱题目描述里那句“0 i n”你如果没仔细看很容易凭直觉直接写循环结果要么漏掉后半段要么数组越界后对着报错发呆。我在刷题群里见过太多人题目读了一半就开始上手最后被边界条件折磨到怀疑人生。数组串联这道题就是用来给新手立规矩的先画图再写码。输入[4,5,6]你在草稿纸上写一个长度为 6 的目标数组然后从下标 0 到 5 一个一个填填完你就明白了那个所谓“后半段”不过就是源下标从 0 重新开始而已。2. 最朴素的写法一次循环双赋值2.1 循环双赋值的思路与 Java 实现最直观的解法就是声明一个长度为 2n 的数组然后一个 for 循环遍历目标数组的所有下标用取模的方式把下标映射回原数组。Java 写出来长这样class Solution { public int[] getConcatenation(int[] nums) { int n nums.length; int[] ans new int[2 * n]; for (int i 0; i 2 * n; i) { ans[i] nums[i % n]; } return ans; } }这种写法的好处是只管理一个循环变量 i代码短逻辑闭环。但你要小心如果 n 为 0i % n会直接抛异常。这道题约束保证n 1所以提交没问题但如果你把这个逻辑抽成工具方法就不得不考虑防御。另一种写法更贴近题目原始定义一次循环同时处理前半段和后半段class Solution { public int[] getConcatenation(int[] nums) { int n nums.length; int[] ans new int[2 * n]; for (int i 0; i n; i) { ans[i] nums[i]; ans[i n] nums[i]; } return ans; } }我实际写题时更喜欢第二种。它的赋值顺序和题目给的映射关系一一对应前 n 位放nums[i]后 n 位放nums[i]。几乎不可能把索引写错。如果是一个新手让我推荐我会让他先写第二种等熟练了再去看第一种取模写法理解它为什么能工作。2.2 两种循环写法的差异第一种写法把循环边界改成了 2n用取模处理映射第二种写法保持 n 的边界通过i n访问后半段。两者时间复杂度一样空间占用也一样真正的差异在语义和工程习惯上。取模写法表面简单但“取模”是相对耗费周期的算术运算。n 比较小的时候无所谓比如这道题的 n 最大只有 1000但如果让你在工程里拼接超大数组循环内部每一次i % n都可能让 CPU 分支预测和多周期延迟拖慢整体性能。这也是为什么底层库比如System.arraycopy从来不这么干。双赋值写法多写了一行赋值但语义清晰很多编译器也更容易做循环展开优化。所以我的结论很直接面试写题优先保证可读性用双赋值如果你只是图秀操作用取模也不是不行但最好能顺口讲出两者的取舍。2.3 复杂度账本时间O(n)、空间O(n)和那个“额外空间”的小陷阱时间上循环跑了 n 次或 2n 次都是 O(n)。空间上返回数组长度为 2n所以至少 O(n)。这里有个面试官很喜欢追问的细节那额外空间复杂度到底是 O(n) 还是 O(1)LeetCode 的惯例是返回结果数组不计入额外空间所以严格按“除了原数组之外的空间”算这道题的答案是 O(1)——你只开了几个变量没有额外数组。但很多面试官习惯把输出数组也算进空间复杂度这时候答案就成了 O(n)。这个问题我在模拟面试里答错过两次。后来养成了一个习惯先问清楚面试官说的“空间复杂度”包不包含输出数组再回答。做这道题时你只需要记住时间复杂度 O(n) 是确定的空间复杂度取决于口径描述清楚即可。3. 再精简一点API 调用与各语言一行解3.1 Java 版System.arraycopy 解决一切既然是把数组拼两遍那最贴近工程实践的方式就是系统拷贝class Solution { public int[] getConcatenation(int[] nums) { int n nums.length; int[] ans new int[2 * n]; System.arraycopy(nums, 0, ans, 0, n); System.arraycopy(nums, 0, ans, n, n); return ans; } }System.arraycopy是 native 方法JVM 底层用 C/C 实现很多 HotSpot 版本还会走到手写汇编或者向量化拷贝的路径对小数组有快速路径对大数组尽量做到缓存友好。它比我们用 Java 手写循环逐元素复制要快尤其在 n 比较大的时候差异会更明显。不过 LeetCode 的 n 最大才 1000性能差异基本可以忽略。这种写法的真正价值是让你熟悉 JVM 里最常用的数组拼接 API。很多人在实际工作中写业务代码拼接两个数组还在手动循环其实System.arraycopy才是标准答案。还有一种玩法用Arrays.copyOf减少一次拷贝操作int[] ans Arrays.copyOf(nums, 2 * n); System.arraycopy(nums, 0, ans, n, n); return ans;Arrays.copyOf(nums, 2 * n)会先 new 一个 2n 长度的数组然后把 nums 拷贝到前 n 位。这一行等于“创建数组 前置拷贝”再补一次arraycopy把后半段填上。代码少了一行但你要知道Arrays.copyOf底层其实还是调用的System.arraycopy。3.2 其他语言怎么玩Python 最简单直接列表相加class Solution: def getConcatenation(self, nums: List[int]) - List[int]: return nums nums或者return nums * 2Python 的列表乘法会生成一个新列表不会修改原对象。但要注意nums nums是原地修改不能直接返回原数组因为这会改变函数入参不符合题目“返回一个新数组”的要求。我自己刷题时还是倾向写nums nums语义更清晰也避免了和的混淆。JavaScript 用展开语法var getConcatenation function(nums) { return [...nums, ...nums]; };C 则可以用 insertclass Solution { public: vectorint getConcatenation(vectorint nums) { vectorint ans nums; ans.insert(ans.end(), nums.begin(), nums.end()); return ans; } };这些写法都很“语言特性”刷题时可以炫技但面试的时候最好能补充一句“它的时间复杂度是多少底层开了多大的内存”展示你对语言实现的认知深度。3.3 一行解虽好面试时也得能讲清复杂度我见过太多候选人简历上写了“熟练使用 Python”结果被问到nums nums的时间复杂度就卡住了。实际是这样的Python 的list list会先计算两个列表的总长度分配一个新列表然后逐个元素拷贝过去时间复杂度 O(n)空间也 O(n)。JS 的[...nums, ...nums]同理会 allocate 一个 2n 的数组再展开填入。C 的insert可能涉及 reallocation 和元素移动均摊下来也是 O(n)。理解了这些语言背后的实现“一行解”才不只是一个花哨的写法而是你对这门语言理解深度的证明。所以我给刷题者的建议一直是先把最原始的循环写明白再去记那些一行解。顺序反了基础就悬了。4. 从1929延展出去索引映射在数组题里反复出现4.1 取模 i % n 是循环数组的通用钥匙数组串联这道题本质上要求你建立一种“从目标下标映射到源下标”的直觉。任何时候只要你想把数组当成一个无限循环来看就一定会用到i % n或者i - n这类映射。这个模式在算法题里太常见了旋转数组要处理下标回绕循环队列要处理头尾相接轮询调度算法要循环分配任务。现实业务里也有活生生的例子——比如把一批商品 ID 列表按轮询方式分发到三台机器上处理k % 3就是最经典的分配逻辑。所以不要小看取模操作它在数组题里的地位相当于指针在链表题里的地位。4.2 四道同类数组题的对照复盘与其单独刷完 1929 就划走不如顺手把下面几道题放到同一天做效果翻倍。这里我整理了一个对照表题目核心考点与1929的共鸣点189. 旋转数组三次反转 / 原地操作下标需要通过(k i) % n做回绕映射88. 合并两个有序数组从后往前双指针目标数组长度提前确定填充顺序决定代码简洁度26. 删除有序数组中的重复项快慢指针覆盖在同一段连续内存里控制有效长度283. 移动零快慢指针 / 覆盖后补零同样是利用下标控制在原数组上操作这些题都没有引入树、图、动态规划这类复杂结构它们拼的就是“索引感”和“对数组底层连续内存的直觉”。1929 虽然最简单但它牵出的这条线几乎覆盖了数组题最常见的四类操作拷贝、映射、覆盖、合并。4.3 边界条件与工程化延伸题目给的约束是1 nums.length 1000所以你可以放心地 new 一个 2n 的数组。但如果你把这段逻辑放到真实业务里比如日志聚合模块要把多个分片拼成一个完整结果情况就完全不一样了分片可能来自不同的存储节点每个分片可能非常大一次性 new 2n 可能直接内存溢出你会考虑分段读取、流式写入或者用零拷贝技术来拼接数据。所以我在代码里通常会先判空if (nums null || nums.length 0) { return new int[0]; }LeetCode 上不加判空完全没问题因为约束保证非空但工程上这是必要的防御。这也是“简单题”给我们的另一个提醒算法题里的隐含条件不会写进方法签名但你自己要清楚真实环境里什么意外都可能发生。5. 复盘方法一道简单题怎么刷出更高性价比5.1 让“看懂范围”成为肌肉记忆刷 LeetCode 最容易翻车的就是不看数据范围。1929 的 n 只有 1000所以哪怕你写一个 O(n²) 的暴力嵌套循环也可能侥幸通过。但同样一道题如果把 n 改成 10^5你的暴力解法就彻底废了。我的习惯是把 Constraints 当作题目的一部分来读甚至先于示例来读。示例只给你感性认识约束才告诉你应该选哪一层复杂度的算法。从一道简单题开始养成这个习惯后面做到中等题、困难题时你会被这个习惯反复救回来。5.2 先写最稳的解法再用最优写法收尾我给刷题新手的实操建议是四步走第一遍不管代码漂不漂亮用你最顺手的双循环或双赋值把题目 AC。第二遍看官方题解的写法理解它为什么会更简洁。第三遍打开讨论区看高赞回答有没有什么骚操作比如System.arraycopy或 Python 的nums nums。第四遍关掉 LeetCode在编辑器里默写一遍你真正想掌握的解法。这种“先 AC再复盘再默写”的流程比一口气刷十道新题要有用得多。1929 的循环双赋值就是那个“最稳解法”System.arraycopy就是“最优写法”两者都值得留在你的记忆里。5.3 把一道题变成一个小专题的串联清单我刷完 1929 之后给自己定的当天小专题是“数组下标映射”搭配 189 和 88 两题一起看。因为这三道题放在一起正好能回答三个问题如何用取模处理循环位移189如何从后往前合并数组避免覆盖未处理的元素88如何用最简单的前后拼接完成数组串联1929如果你还有余力可以再去碰一碰二分查找相关的数组题比如热度很高的“爱吃香蕉的狒狒”。它本质上也是“对数组取值做映射再判断边界条件”只是判断方式从下标映射变成了二分逼近。这样一盘一道简单题就不再是孤立知识点而是整个数组知识网的一个锚点。写到这里我突然想起自己第一次系统刷 LeetCode 时的状态专门挑简单题一天刷十几道刷完就忘。后来才明白刷题不是集邮而是建立“问题解决模式”的索引。1929 这道题我后来每次拿它当热身——三分钟写一遍双循环再写一遍 arraycopy 版本顺带在脑子里把 189 和 88 的解法过一遍。这套热身动作在好几次面试前的晚上帮了大忙。如果你刚开始刷题或者正在为面试焦头烂额别急着跳过简单题——把它吃透、讲明白、串联起来你的下一道题一定会写得比之前快很多。