FEATURED · 精选文章

LeetCode 27移除元素全解析:双指针技巧与面试官思维深度拆解

发布时间 / 2026/9/11 7:35:35
来源 / 创域科博编辑部
栏目 / 资讯中心
LeetCode 27移除元素全解析:双指针技巧与面试官思维深度拆解 前阵子有个准备跳槽的学弟跑来问我LeetCode 27这道移除元素的题我闭着眼睛都能写出来为什么面试官还能问半个小时我当时就乐了这道题恰恰是我当年面试时栽过跟头的地方。LeetCode 27确实属于新手友好型题目代码量少思路直观但真正可怕的地方在于面试官能从一个双指针原地移除元素的实现里看出你对数组存储的理解、边界条件的敏感度、有没有写过工程代码的习惯甚至是你遇到问题时会不会主动优化。这篇文章我就把这道经典题彻底拆开聊透从题面到思路从代码到面试官视角再附上我刷题和面试多年总结下来的排查技巧希望对正在备战算法面试的你有点帮助。1. 先别急着写代码把题意啃透1.1 输入输出到底怎么算很多人做LeetCode 27只记住了“删除元素”四个字但题目真正要求的是两件事第一把数组里所有等于目标值 val 的元素移除第二返回移除后数组的新长度。这里有一个很关键的隐藏信息题目允许你改变数组中元素的顺序而且只关心前 k 个元素k 就是函数返回的那个长度。也就是说数组后半部分残留什么值题目根本不检查。举个实际例子输入是 nums [3,2,2,3]val 3。合法的输出是返回 2同时让 nums 的前两个元素变成 2 和 2。你不需要把后面的 nums[2] 和 nums[3] 真的擦掉它们哪怕是原来的 3 也行。这一点很多第一次刷题的人会误解还有人会傻乎乎地去pop()数组元素结果时间复杂度和空间复杂度都变得很糟糕。原地操作的目的就是让你在同一个数组上做修改不额外开一个大数组去拷贝所以理解清楚“只看前 k 个元素”这一点是做题的第一步。1.2 为什么“原地”这两个字这么关键如果允许额外开数组这道题就变成了简单遍历写起来五分钟搞定新建一个数组把不等于 val 的值放进去再拷回去。但面试官刻意强调“原地”本质上是在考察你能不能控制内存开销也考察你对数组这种连续内存结构的底层认知。数组和链表不一样数组是一片连续的内存空间删除一个元素意味着要把后面的所有元素往前搬。如果每次都删一个就搬一次碰到极端情况比如整个数组全是 val会出现 O(n²) 的复杂度。真正的原地做法是让指针在移动过程中完成“覆盖”而不是“删除”把不该留下来的值直接盖掉这就是双指针技巧的用武之地。想明白这一步你才能理解为什么快慢指针能够做到一次遍历就解决问题因为你的操作从来都不是删除而是选择性地覆盖。1.3 它是整个“数组类双指针”题目的地基LeetCode 27 和 26题删除有序数组中的重复项、80题删除有序数组中的重复项 II、283题移动零本质上是同一个家族。它们都是在一个数组上通过两个指针分别承担“探测”和“写入”的职责在不借助额外存储的情况下完成数据整理。这也是我把27题称为“地基”的原因后面那些题目无非是在地基上加了排序性、允许重复次数、交换条件等约束。如果你刷题比较有体系会发现LeetCode官方把27题归类为“双指针”标签但很多教程并没有点透双指针不只是一种代码技巧更是一种抽象思维模型。理解这个模型比背下代码有用得多。我在下个章节会完整讲清楚这个模型怎么从暴力解法一步步演化出来。2. 双指针思路从哪来暴力解法到最优解2.1 暴力解法为什么不行先把最直观的解法写出来遍历数组一旦发现 nums[i] 等于 val就把后面的所有元素往前移动一位然后让数组长度减一。这里最容易写错的点是移动完元素之后当前下标 i 的位置上换成了一个新的元素这个新元素还没有被检查过所以 i 不能直接加一否则可能跳过一个连续的 val。这个解法在面试时用来铺垫思路是可以的但它的问题非常明显最坏情况下数组里全是 val每个元素都会被移动 n 次总复杂度 O(n²)。这在工程里是不能接受的。面试官问这道题一大半原因就是想看看你能不能从 O(n²) 的直觉解法进化到 O(n) 的双指针解法。如果你只停留在暴力层面那说明对算法复杂度优化还没有形成本能。2.2 快慢指针的写法以及它为什么正确快慢指针的思想非常朴素设置两个指针一个叫 slow慢指针一个叫 fast快指针。fast 负责在前面探路逐个检查数组里的每个元素slow 则指向“下一个可以被覆盖写入的位置”。一开始 slow 0fast 0。fast 每走到一个元素就判断这个元素是否不等于 val。如果不等于说明这个元素要保留下来那就把它写到 nums[slow] 的位置然后 slow 加一。如果等于 val说明要丢弃fast 继续往前走slow 原地不动。遍历结束后slow 的值就是新数组的长度。这个写法最精彩的地方是它维护了一个不变量区间 [0, slow) 里的所有元素全都是不等于 val 的。fast 每走一步这个性质都不会被破坏。正因为有这个不变量你根本不需要犹豫当前元素是不是需要“删除”只需要判断“是否保留”。这种用覆盖代替删除的思路恰恰就是工程里“原地整理数组”的经典套路。2.3 对向指针的另一种思路什么时候用快慢指针虽然好理解但并不是唯一解法。还有一类解法是两个指针分别从数组两端向中间移动左指针从左往右找等于 val 的元素右指针从右往左找不等于 val 的元素然后把右指针指向的元素复制到左指针位置。每覆盖一次右指针就往左移一位直到两个指针相遇。对向指针的好处是它能尽量减少元素的移动次数。因为它是拿后面的元素去填前面的坑只要后面的元素自己不需要保留移动就是“顺便”的。所以如果你的目标是“最小化移动次数”对向指针更优。但它也有代价它会改变数组中元素的相对顺序。题目本身不要求保持相对顺序所以两种写法都合法不过如果面试题后续要求顺序稳定你就不能这种写法了。从更广义的角度看这两种思路正好对应双指针的两种常见形态同向指针和相向指针。同向指针适合“保留部分元素并保持顺序”的场景相向指针适合“快速整理、不关心顺序”的场景。LeetCode 27 一题两解恰好可以当作双指针入门的第一课。2.4 复杂度速查表解法时间复杂度额外空间是否保持原顺序适用场景暴力覆盖O(n²)O(1)是仅用于教学铺垫快慢指针O(n)O(1)是主流写法通用性强对向指针O(n)O(1)否追求最少移动次数面试时如果能把这三种解法按复杂度排出来并说清楚各自取舍基本就已经向面试官证明你具备工程上的复杂度意识了。3. 面试官到底在考察什么我拆成四个维度讲3.1 对数组存储结构的底层理解面试官问LeetCode 27首先想知道你对数组的理解到底停在哪一层。如果说“数组就是可以按下标访问的一排数据”这只能说及格但如果你能说出“数组是一片连续内存删除某元素必须移动后续元素因此删除操作本身是O(n)的”那就完全不一样了。这就是为什么很多面试官看完你写代码还会追问一句“你的解法里到底有没有真正删除元素”如果你回答“没有只是覆盖”他会觉得你对内存和指针是有感知的。如果你支支吾吾说“应该算删除吧”那印象分会大打折扣。LeetCode 27这道题表面考逻辑实际考的是你在系统层面有没有建立起“数组删除并不便宜”的直觉。3.2 对双指针思维模型的建立双指针不是死记硬背的模板而是一种用“相对位置”解决问题的思路。在LeetCode 27里快慢指针分别代表了探测和写入两种职责。能把一个数组问题拆成两个角色的协作这是很多中级算法题的通法。面试官考察这个点的方式通常是追问“如果数组不是无序的而是有序的你会不会想到别的双指针用法”或者是“这个思路还能解决什么其他问题”一旦你把双指针理解成“用两个游标维护一段区域的语义”你就能举一反三。比如三数之和、接雨水、最长无重复子串等等背后都有双指针的影子。26、80、283这些题也都可以用同一套思维模型快速解决面试官看到你能主动建立知识网络通常会非常欣赏。3.3 对边界条件的敏感度LeetCode 27 的小陷阱非常多。空数组怎么办整个数组全部等于 val 怎么办一个元素都没有被删掉怎么办val 不存在于数组中怎么办这些都考察你对极端情况的敏感度。写出正确代码不难难的是在写代码的同时就把这些边界情况全部考虑到。我面试别人的时候常常看到一个候选人写完代码就停在那里等我来验证。我更喜欢看到的是候选人主动在代码注释里写清楚“这里假设 nums 长度可以为0”或者在小黑板上画几个例子自测一下。这体现出的是工程师对自己代码负责的态度而不是“能跑就是赢”的学生思维。3.4 代码规范与口头沟通算法题不是只考算法它同时考你能不能把思路清晰地传递给别人。面试官会让你先讲思路再写代码这道题因为实现太短甚至可能出现“你还没讲完面试官就已经知道你要写什么”的情况。这时候你的口头表达就显得更重要。我见过很多候选人代码写得很对但全程沉默写完就交卷。也有候选人表达能力极好每一步都说清楚先定义两个指针的语义再说循环结束条件最后说明返回值。后者往往在面试评价里会获得更高分因为工程开发中代码评审、团队协作都需要沟通能力。简而言之你把LeetCode 27当做一道“表达题”来准备收获会比单纯刷题大得多。4. 手写实现从伪代码到可运行代码4.1 三种主流语言的参考实现先放一段最经典的快慢指针实现我用Python写def removeElement(nums, val): slow 0 for fast in range(len(nums)): if nums[fast] ! val: nums[slow] nums[fast] slow 1 return slow用C写的话逻辑完全一样int removeElement(vectorint nums, int val) { int slow 0; for (int fast 0; fast nums.size(); fast) { if (nums[fast] ! val) { nums[slow] nums[fast]; } } return slow; }Java版本public int removeElement(int[] nums, int val) { int slow 0; for (int fast 0; fast nums.length; fast) { if (nums[fast] ! val) { nums[slow] nums[fast]; } } return slow; }看到没有三种语言的差异只在语法层核心逻辑都是从0开始的快慢指针扫描一遍在 fast 找到合法元素时写入 slow 的位置。这段代码甚至不需要额外处理空数组因为空数组的循环根本不会进入slow 自然返回0。4.2 这段代码为什么能处理所有边界条件我们来逐条验证。空数组场景循环体不执行slow 返回0正确。整个数组全是 val比如 [3,3,3,3]val 3fast 每次找到的元素都等于 val所以永远不会执行写入slow 保持0返回0正确。整个数组全是合法元素比如 [1,2,3,4]val 5fast 每走一步都把元素写到 nums[slow]slow 逐渐追上 fast最终返回4数组内容原封不动正确。混合场景 [0,1,2,2,3,0,4,2]val 2执行完slow等于5前5个元素是0,1,3,0,4后面的元素虽然还是残留的2但题目不关心正确。这段代码简洁到让人怀疑它是不是太简单了但它的正确性正是由之前说的“不变量”保证的在任何时刻[0, slow) 区间内的所有元素都已经通过检查且都不等于 val。只要你不破坏这个不变量代码就是安全的。4.3 面试中常见的三个追问变体面试官不会只让你把代码写完就放你走LeetCode 27经常搭配下面几个变体。第一个变体是“如果要求必须保持元素的原始相对顺序你能不能用快慢指针”这个追问基本是送分题因为快慢指针本身就是保持相对顺序的你把数组里保留的元素按扫描顺序写入顺序天然不变。第二个变体是“如果要移除的元素不止一个而是一个数组 removeSet你怎么办”这时候你可以在循环里先用一个哈希集合缓存需要删除的目标值再用同样的快慢指针逻辑判断if (!removeSet.contains(nums[fast]))。时间复杂度仍是 O(n)但空间复杂度会上升为 O(m)其中 m 是需要删除的目标种数。第三个变体是“能不能把空间复杂度降到 O(1)并且只扫描一次”这个问题实际上已经把答案送到你嘴边了就是要求你用双指针原地做。你甚至可以反问面试官“您是希望我保持顺序还是允许交换”这样做瞬间显得你很专业因为两种需求对应不同写法。5. 从27延伸出去一套“双指针”打法5.1 LeetCode 26、80、283 如何套用LeetCode 26题要求删除有序数组中的重复项其实就是LeetCode 27的变体val 不再是一个固定值而是前一个元素。用快慢指针写时判断条件从nums[fast] ! val变成了nums[fast] ! nums[slow - 1]。注意这里要小心越界所以通常让 slow 从1开始fast 从1开始第0个元素天然保留。LeetCode 80是26的加强版要求每个元素最多出现两次。这时你只需要把判断条件改成nums[fast] ! nums[slow - 2]原理是只要 fast 指向的元素不等于 slow 前面第二个元素就说明它和当前已保留区域的最多重复两个元素不冲突可以写入。这个技巧非常经典面试时如果你能顺手写出来会很有加分效果。LeetCode 283移动零本质上也是27题的变形。它要求把数组里的0全部移动到末尾并保持非零元素的相对顺序。做法是用快慢指针把非零元素按顺序写入前部然后剩余位置全部补0。核心思想是一样的用慢指针标记“下一个非零元素该放的位置”快指针负责找一个非零元素。所以说学会27题相当于学了五道题这是我对它评价最高的原因。5.2 快慢指针还可以解决哪些经典题快慢指针的适用范围远不止移除元素。链表里判定是否有环、寻找链表中点都会用到快慢指针只不过那边快指针每次走两步慢指针每次走一步。数组里求“最长无重复子串”也要用到滑动窗口本质上是两个指针维护一个窗口的合法状态。还有一道典型题是“最短无序连续子数组”需要利用双指针分别从左往右和从右往左找边界。另一道是“盛最多水的容器”用相向指针不断收缩短边。这些题目看起来各不相同但当你做多了会发现只要你明确两个指针各自的含义以及移动指针的条件剩下的就是细心维护边界。我建议你刷题时做一个“双指针归纳表”记录每道题里 slow 和 fast 分别代表什么移动条件是什么终止条件是什么。LeetCode 27作为第一行放进表里后面每遇到一类新题就新增一行。整理一段时间后你会发现这类题的套路非常有限真正需要死记硬背的几乎没有。5.3 面试官如果继续深挖怎么办有的面试官对LeetCode 27会深挖到“你能不能用类似的思想处理字符串”字符串本质上可以看作字符数组很多数组题都能平移到字符串场景。比如移除字符串里的空格、去除指定字符都是同一个套路。你只需要把字符数组用快慢指针整理最后返回新长度或者原地修改字符串。如果面试官继续问“如果数组特别大大到内存装不下怎么办”这个问题就超出算法题本身了可能会往分治、外部排序方向走。你不需要给出完美答案但至少应该能说出双指针算法的优势是单次扫描、内存友好但面对超大数据时瓶颈在磁盘IO可能需要分批加载数据每一批内做双指针整理再思考如何跨批次归并。这其实已经到了系统设计层面的讨论能聊到这里已经说明你具备一定的工程全局观。6. 常见错误与排查技巧实录6.1 我都见过哪些翻车现场先说一个新手高频错误用 for 循环遍历数组时在循环体里对 nums 做pop()操作。你这样一改数组长度变了遍历范围也乱了轻则跳过元素重则下标越界。在Python里for fast in range(len(nums))里的 len(nums) 只计算一次后续 pop 会导致 fast 指向位置错位。如果你真要在Python里边删边遍历只能用 while 循环手动控制下标但那样写很容易出bug。再说第二个常见错误快慢指针初始值写错。有人把 slow 初始化成1认为第一个元素肯定保留。这在不清楚 val 到底是什么的时候显然不成立万一 val 恰好等于第一个元素呢用1当初始值就直接跳过了第一位的检查。所以最稳妥的写法就是把 slow 和 fast 都从0开始让代码对任何输入都保持一致。第三个错误是试图在循环结束后把后续元素置为0或者清空。你这么做虽然不影响评测但额外增加了时间复杂度还可能因为修改了数组元素导致调试时出错。原地移除元素的题意是“只关心前 k 个元素”所以完全没有必要做这一步“打扫卫生”的工作。6.2 遇到了隐藏的坑怎么调试LeetCode 27 属于思路简单但实现容易出小错的题调试时最好的工具不是 print 到处打而是在心里维护一张表每一步 slow 和 fast 分别指向哪里当前 [0, slow) 的内容是什么。真要打日志就打印 slow、fast、当前要写入的值三个变量。举个例子nums [1, 1, 2, 1, 3], val 1。刚开始 slow 0fast 0nums[0] 1 等于 val不写入slow 保持0。fast 1nums[1] 1仍然不写入。fast 2nums[2] 2 不等于 val写入 nums[0]数组变成 [2,1,2,1,3]slow 1。fast 3nums[3] 1不写入。fast 4nums[4] 3不等于 val写入 nums[1]数组变成 [2,3,2,1,3]slow 2。最终返回2前两个元素是2和3。整个过程非常清晰一旦你在纸上画过一遍你基本不会写错。6.3 面试时的表达节奏怎么控制面试时建议你按照“场景确认 - 思路铺垫 - 代码实现 - 复杂度分析 - 主动测试”的顺序走。先确认题目细节是否需要保持原始顺序数组长度可以为0吗val 一定出现在数组里吗这些问题哪怕你已经知道答案也可以快速问一句目的是展示你做题前有澄清需求的习惯。然后跟面试官简单聊一下暴力解法“最直观的做法是每次删除都移动元素复杂度很高。所以我会用双指针一个负责找需要保留的元素一个负责记录写入位置。”这样就把思考过程展示清楚了。写代码时保持安静也可以但最好穿插一句“我让 slow 从0开始因为它代表写入位置的初始值”让面试官跟上你的思路。写完代码后主动做复杂度分析时间复杂度 O(n)空间复杂度 O(1)。最后自己挑一两个边界例子自测。如果你能把这个流程走完面试官对你的评价通常不会差。说到底LeetCode 27拼的不是你会不会做而是你会不会像一个成熟的工程师那样思考和表达。我个人在刷题和真实面试里的体会是越简单的题越能暴露一个人的真实水平。LeetCode 27就像一面镜子能照出你对数组、指针、边界条件和工程习惯的理解程度。建议你不仅会写还要能讲明白最好再把26、80、283这几道变体串起来练一遍形成一个完整的双指针知识小单元。最后分享一个小技巧刷这类题时先别急着打开题解试着把快慢指针画在一张白纸上模拟一遍运行过程比看十遍答案都管用。很多年后你可能会忘记题号但这个“用慢指针维护一段合法区域”的思路会在你处理工程里的数组整理、数据清洗问题时反复出现。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻