LeetCode 1929 数组串联:从基础操作到刷题体系构建 刷题日记系列的第一篇我选了一个看起来简单到“没什么好写”的题目——数组串联LeetCode 1929。之所以拿它开头是因为这几年刷leetcode热门100题、看题解、打周赛的经历让我越来越清楚一件事真正决定你能不能走远的恰恰是怎么对待这些“简单题”。很多人一上来就追求难题、偏题结果基础操作和复杂度意识都不牢后面刷到二分、双指针、动态规划时处处卡壳。数组串联这道题准确说是“LeetCode 1929Concatenation of Array”目标是把一个数组原样拼接成两遍。它涉及的数组操作、内存分配、边界处理几乎是后续所有题目都要用的基本功。这篇日记我打算讲五块内容先拆解题目到底在考什么再给出三种主流解法并对比优劣接着聊复杂度和边界条件然后结合leetcode热门100题和周赛430的启发说明这类题怎么串起整个刷题体系最后整理我实际踩过的坑和排查思路。无论你是刚接触数据结构的初学者还是刷题遇到瓶颈想回头补基础的人这篇都值得花五分钟看完——我会把每一步的“为什么”讲清楚而不只是给个能跑的答案。1. 题目拆解数组串联到底在考什么1.1 原题描述与核心需求题目要求非常简单给定一个长度为 n 的整数数组 nums你需要返回一个长度为 2n 的数组 ans其中ans[i] nums[i] 对于 0 i n ans[i n] nums[i] 对于 0 i n换句话说ans 就是把 nums 原样复制一份接在自己后面。举个例子nums [1, 2, 1]那么 ans 就是 [1, 2, 1, 1, 2, 1]nums [1, 3, 2, 1]ans 就是 [1, 3, 2, 1, 1, 3, 2, 1]。看到这个需求很多人的第一反应是“这也能算一道题直接加起来不就完了”确实这道题在 LeetCode 上的通过率很高难度也标为 Easy。但它出现在“数组”这个分类的开头绝不仅仅是凑数。它真正想考察的是你对数组基本操作、内存空间分配以及代码简洁性的理解。这里有个容易被忽略的点题目要求返回一个新数组而不是在原数组上修改。这意味着你需要考虑“从哪来、到哪去”的问题——原数组的数据不能丢新数组要有足够的空间遍历的边界要正确。别小看这个简单的逻辑它背后是所有数组类题目共同的骨架先确定目标结构再确定填充方式最后检查边界。1.2 从需求推导出两种基本思路既然要把数组复制两份拼接起来最自然的想法当然是“复制一份接在后面”也就是把原数组遍历一遍每读到一个元素就写两次到新数组。这样做的核心操作是“读一次、写两次”时间复杂度必然和数组长度成正比也就是 O(n)。另一个思路是“先原样拷贝一遍再拷贝一遍”也就是借助语言内置的数组拼接或拷贝能力直接把 nums 和 nums 连接起来。这本质上也是遍历两次但因为底层可能做了内存优化写起来更简洁。两种思路没有本质区别核心都是你需要一个长度为 2n 的目标数组然后把 nums 中的每个元素按顺序填进去。明确了这一点后面无论用哪种语法实现都不会跑偏。2. 三种解法实现与对比从最直白到最高效2.1 解法一列表拼接Python 的语法糖如果你用的是 Python最直觉的写法大概是这样class Solution: def getConcatenation(self, nums: List[int]) - List[int]: return nums nums对就这么一行。Python 的列表支持运算符直接拼接底层会创建一个新列表把两个列表的元素依次拷贝进去。这种写法可读性极高几乎是零学习成本。但也正因为太简洁很多初学者反而心里没底“这算不算作弊面试官会不会觉得我在投机取巧”我的看法是在笔试或日常开发中这种写法完全没问题Python 的就是常规操作底层一样是老老实实地分配内存、拷贝元素。不过在面试场景里如果面试官让你手写实现你最好还是能说出底层发生了什么——也就是下面两种解法。2.2 解法二循环 append最符合直觉的通用思路class Solution: def getConcatenation(self, nums: List[int]) - List[int]: ans [] for i in range(2): for x in nums: ans.append(x) return ans或者干脆展开成两次遍历class Solution: def getConcatenation(self, nums: List[int]) - List[int]: ans [] for x in nums: ans.append(x) for x in nums: ans.append(x) return ans这种写法是“零基础也能看懂”的版本先建一个空列表把 nums 的元素逐个加进去再加一遍。它不需要任何语言特性C、Java、Go、JavaScript 全都能照搬这个思路。不过我实测下来这种写法在 Python 里效率并不差因为append的均摊复杂度是 O(1)。但有个小缺点如果提前知道数组长度是 2n却让列表动态扩容中间可能会发生多次内存重新分配。当然对这道题的 n 来说影响微乎其微我提出来只是希望你有这个意识。2.3 解法三预分配空间直接赋值最严谨的写法class Solution: def getConcatenation(self, nums: List[int]) - List[int]: n len(nums) ans [0] * (2 * n) for i in range(n): ans[i] nums[i] ans[i n] nums[i] return ans这是我最推荐在面试时写的版本因为它把“内存分配”和“数据填充”两件事分得清清楚楚。先一次性分配好 2n 个空间再用一个循环同时完成两次赋值。关键点在于i n这个索引偏移——它正是题目定义里ans[i n] nums[i]的直接翻译。有的同学可能会问ans [0] * (2 * n)这一步会不会很慢其实不会Python 的列表乘法底层会一次分配好连续内存比循环 append 触发多次扩容更稳定。放到面试里这个写法还能体现你对“预先分配内存”的敏感性后面做类似“数组合并”“矩阵展开”的题目时这个习惯会帮你省下不少时间和空间。2.4 三种写法对比什么时候用哪种写法代码长度可读性是否依赖语言特性面试推荐度列表拼接nums nums一行极高依赖 Python 语法适合笔试双重循环 append几行高几乎不依赖适合教学和讲解预分配空间赋值几行高依赖基本数组操作面试最稳我的建议是笔试争时间直接用nums nums面试讲思路用预分配空间的写法并且在白板上把ans[i]和ans[i n]两个赋值动作画出来给面试官看。这样既展示了代码能力也展示了思路的清晰度。3. 复杂度的门道为什么说时间 O(n)、空间 O(n)3.1 时间复杂度解析题目要求生成长度为 2n 的结果数组那就意味着至少有 2n 个位置要被写入。任何一个解法都不可能少于 2n 次写入因此时间复杂度下界就是 O(n)。上面三种写法无论是nums nums、循环 append、还是预分配空间赋值底层都是线性遍历原文并写入目标所以都是 O(n)。有些读者可能会疑惑循环 append 里面套了两层循环for i in range(2)加for x in nums这算不算 O(n²)不算。外层循环固定只执行 2 次所以总操作数是 2n不是 n²。这里要区分“嵌套循环”和“嵌套次数固定为常数”的区别——外层是常数次的话时间复杂度依然是线性的。3.2 空间复杂度解析空间复杂度分析是这道题真正有价值的地方。题目明确要求返回新数组所以新数组 ans 本身的长度 2n 是必须的空间这属于“输出占用的空间”通常不计入额外空间复杂度但如果你严谨地把输出也算进去那么总空间复杂度就是 O(n)。原地修改原数组是不行的因为原数组长度只有 n你没法在它内部扩展出 2n 个空间。可以把这道题和“数组原地去重”“反转字符串”这类题目对比后者的目标是在原数组上操作尽量做到 O(1) 额外空间而数组串联的目标从一开始就是生成新数组所以 O(n) 空间是不可回避的。刷题的时候如果你能养成本能地判断“这题能不能原地做、输出空间算不算额外开销”后面做矩阵旋转、链表合并时思路会清晰很多。4. 刷题延伸从数组串联到热门100题、周赛4304.1 为什么 LeetCode 热门100题里也有它的影子很多刷题指南把“热门100题”当作重点清单这份清单里大多数题目都是中等难度。但数组串联这种 Easy 题的意义恰恰在于它是清单里所有数组题的“地基”。比如热门100题里的“两数之和”它需要你遍历数组并用哈希表记录见过的值如果没有“遍历数组并逐元素处理”的肌肉记忆做题时会不断卡在语法细节上。再比如“合并两个有序数组”它需要你从后往前填充这和本题“预分配空间后填充”的思路一脉相承。从出题角度讲LeetCode 官方把这题放在数组分类靠前的位置就是要让初学者先体会“新建数组、遍历、填值”这套循环。你对这一套操作越熟练后面看题解时越能抓住重点而不是被代码结构绕晕。4.2 由数组串联联想到的数组类题目地图刷题不能一题一题孤立地刷最好每做完一题就在脑子里挂一条线把它和相关的题目连起来。从数组串联出发我建议你串联这几道题LeetCode 88合并两个有序数组——同样是往数组里填值只不过要处理两个来源数组的先后顺序引入了“双指针”思想。LeetCode 189轮转数组——把数组旋转 k 次本质上是“重新映射索引”和本题的“索引偏移”有共通处。LeetCode 724寻找数组的中心下标——先求总和再遍历属于数组前缀和思想的起点。LeetCode 875爱吃香蕉的狒狒——虽然这题是二分查找但它对数组的读法和“能否在限定时间内完成任务”的判断依然离不开对数组的逐元素检查能力。我个人的习惯是每做完一题顺着题解区找 1 到 2 道相似思路的题用同样的套路独立写一遍写不出来就回去看题解隔天再写一遍。这个过程比单纯刷题数量更重要。4.3 周赛430带来的启发简单题是比赛的前菜最近 LeetCode 周赛430里不少选手栽在前面的简单题上原因不是不会做而是读题太快、边界考虑不全导致提交报错、罚时。周赛和平时刷题最大的区别是时间压力下你会不自觉地用最熟悉、最能一次写对的写法。如果你平时刷 Easy 题都用“最稳”的预分配写法到了赛场上自然会选择可靠性优先的方案。我自己参加过好几次周赛最深的一个体会是靠前的基础题不是给你秀操作的而是让你稳稳拿分的。与其在简单题里用花哨的写法炫技不如用最直白的方式快速通过把时间留给后面的中等题和难题。5. 常见问题与排查技巧实录5.1 新手最容易踩的三个坑第一个坑试图原地修改原数组。有的同学觉得“反正要复制一份那我直接把 nums 后面追加一份不就行了”比如直接nums nums或者nums.extend(nums)。这在 Python 里虽然能运行但会改变原数组而且如果后面还要用原数组做别的计算数据已经乱了。更严重的是如果循环里动态往列表中添加元素可能会造成无限循环因为列表长度在不断变化。第二个坑索引越界。用预分配空间写法时如果循环写成for i in range(2 * n)然后在循环里写ans[i] nums[i]当 i 大于 n 时就会越界。正确做法是用一个循环同时做两次赋值或者用i % n取模来访问原数组。发现了没很多越界错误都源于“把一个索引当成所有场景通用了”。第三个坑返回了不必要的东西。有人会不小心把打印语句写进返回逻辑或者返回了一个元组、字符串而不是列表。这题明确要求返回整数数组类型不对直接判错。考试和面试时提交前多看一眼返回值类型能避免很多低级失分。5.2 现场排查思路报错后我一般怎么查如果你写完代码在 LeetCode 上运行报错先不要慌按照下面的顺序查看报错类型IndexError 就去查索引计算TypeError 就去查数据类型MemoryError 就去查是不是不小心创建了超大列表。构造最小用例不要上来就测大数组先拿nums [1]和nums [1, 2]这种短数组跑一遍把结果在草稿纸上手写出来再和代码输出比对。检查边界条件数组长度为 0 时你的代码会不会返回空数组长度为 1 时是否只拼了一次这两个边界测试通过基本就稳了。确认输出类型Python 里返回tuple或generator都会导致类型不匹配记得转成 list。这个排查步骤我用过很多次不仅在数组串联这道题上在后来的二分、滑动窗口、动态规划题目中同样适用。先缩小范围再定位错误比对着整段代码发呆有效得多。5.3 给刷题新手的三个独家建议建议一同一道题至少用两种写法做一遍。数组串联这道题我建议你先写nums nums再写预分配空间赋值版。两种写法切换的过程能帮你理解语言底层的数组机制而不是只会抄答案。建议二建立一个“错题索引表”。不需要很复杂一个表格记录四列题目编号、错误类型、原因、对照解法。比如数组串联这道题你可以在表里写1929IndexError循环里越界访问 nums用 i % n 或双赋值解决。这个表刷到 50 题之后回头翻一翻你会清楚地看到自己在哪类问题上反复犯错。建议三别急着做难题先把数组基础打得足够牢。我当时跳过了这类入门题直接刷中等难度结果每个题都要频繁翻数组语法效率极低。后来老老实实把 Easy 数组题刷透了再回去做中等题明显顺畅很多。所谓“慢就是快”在刷题这条路上格外适用。6. 从这一题学会“读题三遍”的好习惯题目虽然简单但值得多说一句的是读题习惯。数组串联这道题原题一共就四五行很多人扫一眼就开写结果漏掉了“返回新数组”这个关键要求。我现在的习惯是拿到任何题目先找三个要素——输入是什么、输出是什么、限制条件是什么然后用自己的话把题目复述一遍再开始动笔。这个过程不超过三十秒却能省下后面的重写时间。以前我总觉得自己读题没问题直到有一回刷一道“反转字符串中的单词”的题目把“单词间保留一个空格”看成了“去掉所有空格”写到一半才发现整段代码白写。那次之后我强迫自己慢下来哪怕题目再简单也先复述再动手。读到这篇日记的朋友不妨也试试这个习惯——它能帮你避开大量本可以避免的错误。我个人的体会是能把这种一眼看穿的题讲明白才是刷题真正入门的时候。数组串联不是终点但它是“构建数组类问题直觉”的一块重要拼图。刷完这题你可以顺手把上面提到的 LeetCode 88、189、724 都翻出来看看沿着这条线继续往前走。下一道题我计划写写“爱吃香蕉的狒狒”这种二分查找和数组结合的题目到时候会把这类题的通用套路再拆细一些。如果你也在刷题欢迎一起交流思路和踩坑经历。