把数组里“每隔一个元素删除一个”,题目名还写着 Removing Elements,Level easy。这类题我见过很多人几十秒写完就提交,觉得自己稳了,实际上里面藏着好几个容易让人栽跟头的点,而且这些点不是算法难度问题,是你对数组操作语义有没有真搞明白的问题。
先交代一下背景:这道题常见于各类在线刷题平台,输入一个数组,期望输出一个新数组,新数组里面保留原数组的第 0 个、第 2 个、第 4 个……也就是所有偶数索引位置的元素,奇数索引位置的元素不要了。光看这句话确实 easy,但我后来拿这道题试过几个刚入门的朋友,发现至少有三种写法跑出来结果是错的,而且错得还都挺隐蔽。所以这篇文章不是来教你怎么把题目 AC 掉,而是想借这个看似简单的场景,把数组遍历、索引变化、过滤语义、原地修改和不可变数据这些概念一次性捋清楚。
1. 题干复盘:先弄明白“每隔一个移除”到底在说什么
1.1 输入输出约定
先看一个最直观的例子。假设输入数组是:
javascript复制const people = ["Zhang", "Li", "Wang", "Zhao", "Qian", "Sun"];
期望输出:
javascript复制["Zhang", "Wang", "Qian"]
也就是说索引 0、2、4 被保留,索引 1、3、5 被移除。英文描述里的 “remove every second element” 经常被误解成“移除第二个元素、第四个元素……”,这个理解其实是正确的,every second element 并不是说“每两个保留一个”或者“把第二个以后的元素都留着”,而是指“每隔一个元素,拿走下一个”。
我做这道题的时候习惯先把测试用例固定下来,避免自己写着写着把语义弄反。最值得写进测试的几组用例是这样的:
| 输入 | 期望输出 | 说明 |
|---|---|---|
[] |
[] |
空数组直接返回空 |
["a"] |
["a"] |
单个元素要保留 |
["a", "b"] |
["a"] |
第二个元素被移除 |
["a", "b", "c"] |
["a", "c"] |
奇数索引全部消失 |
[0, false, "", null] |
[0, false] |
保留的内容可以是 falsy |
[1, 2, 3, 4, 5, 6, 7] |
[1, 3, 5, 7] |
长度是奇数时的边界 |
最后两组特别容易踩坑。一组是关于 falsy 值,一组是关于奇数长度数组。后面我会专门说为什么这两组最容易让新手翻车。
1.2 “删除”这个动词容易误导人
我看到很多第一次写这道题的人,目光会死死盯住“Remove”这个词,接着第一反应就是:那我用 splice 把不要的元素从原数组里删掉不就行了?
思路本身没有错,但这里有一个细节容易被忽略:题目虽然叫 Removing Elements,但绝大多数版本的题干都要求你 return 一个新数组,原数组不应该被改动。也就是说,你要做的是“筛选并复制出需要保留的部分”,而不是真的在原数组上做删除。
这个区别非常关键。用 filter 或用循环加 push,你会天然生成一个新数组,原数组纹丝不动;用 splice 去原地删,要么你小心翼翼控制索引,要么就得先复制一份再删,否则就把原数据破坏了。实际工程里,函数对外暴露时是否修改入参,是会写进接口文档的约定,很多隐蔽 bug 都来自函数内部偷偷改了调用方还在用的数组。
1.3 索引条件是什么
如果你理解了这道题的关键不在于“删”,而在于“保留哪些”,核心逻辑就变成了一句特别简单的话:
只保留索引能被 2 整除的元素。
对应代码语言的表达就是 index % 2 === 0。想明白这一点的人,会用 filter 几秒钟写出来:
javascript复制const removeEveryOther = (arr) => arr.filter((_, index) => index % 2 === 0);
而没想明白的人,容易先去想“我要删除索引 1、3、5……”,然后陷入各种索引动态变化的泥潭。从写代码的经验来说,凡是能用“保留”来表达的过滤逻辑,尽量不要用“删除”来反向实现。人的脑子在处理“保留所有满足条件的东西”时,比处理“删掉所有不满足条件的东西”要可靠得多,尤其当条件还牵扯到位置关系时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一题多解:从最直白的循环到一行 filter
同一个需求,不同阶段的人会写出完全不同的代码。我不打算只给一个标准答案,而是把这几种解法都摆出来对比一下,重点讲清楚每一种背后的代价和适用场景。
2.1 最不容易错的基础循环
如果你刚开始学数组,我建议先从最笨的 for
