一、为什么"移除元素"是入门必刷题?
作为算法新手,我第一次看到LeetCode第27题"移除元素"时,内心是崩溃的。题目要求原地修改数组,还不能用额外空间——这简直是把路都堵死了啊!但后来发现,这道题其实是学习双指针技巧的绝佳案例。
先看题目要求:
给你一个数组nums和一个值val,你需要原地移除所有数值等于val的元素,并返回新数组的长度。
举个🌰:
输入:nums = [3,2,2,3], val = 3
输出:2 → 修改后的数组应为 [2,2,_,_]
二、从暴力解法开始思考
方法1:直接删除法(错误示范❌)
新手最容易想到的就是用数组的splice方法:
function removeElement(nums, val) {
for(let i=0; i<nums.length; i++){
if(nums[i] === val){
nums.splice(i,1)
i-- // 必须回退!
}
}
return nums.length
}
问题:每次删除元素都会改变数组长度,导致索引错乱。虽然通过i--能解决,但时间复杂度高达O(n²),性能极差。
方法2:新建数组法(不符合要求❌)
function removeElement(nums, val) {
let newArr = []
for(let num of nums){
if(num !== val) newArr.push(num)
}
return newArr.length
}
问题:题目明确要求原地修改,这种方法虽然简单但直接犯规!
三、双指针法:优雅的解决方案 ✅
1. 快慢指针思想
想象有两个指针:
- 快指针:侦察兵,负责遍历所有元素
- 慢指针:建筑师,只记录保留的元素
function removeElement(nums, val) {
let slow = 0
for(let fast=0; fast<nums.length; fast++){
if(nums[fast] !== val){
nums[slow] = nums[fast]
slow++
}
}
return slow
}
2. 动图演示
初始状态:[3,2,2,3], val=3
快指针↑ 慢指针↑
第1步:快=3 → 跳过
第2步:快=2 → 复制到慢指针位置 [2,2,2,3], 慢+1
第3步:快=2 → 复制 [2,2,2,3], 慢+1
第4步:快=3 → 跳过
最终返回 slow=2,数组前两位是有效元素
3. 为什么高效?
- 时间复杂度:O(n) → 只遍历一次
- 空间复杂度:O(1) → 只用了常数空间
四、变式训练:26题(去重)
学会了27题,26题"删除有序数组中的重复项"就很简单了:
function removeDuplicates(nums) {
let slow = 1
for(let fast=1; fast<nums.length; fast++){
if(nums[fast] !== nums[fast-1]){
nums[slow] = nums[fast]
slow++
}
}
return slow
}
技巧:比较fast和fast-1,而不是fast和slow!
五、避坑指南
- 边界条件:空数组怎么办?
if(nums.length === 0) return 0 - 元素顺序:题目没要求保持顺序时,可以用首尾指针法更高效:
while(left <= right){ if(nums[left] === val){ nums[left] = nums[right] right-- }else{ left++ } }
六、总结
核心思想:
双指针就像两个人配合工作——一个负责筛选(快指针),一个负责记录(慢指针)。这种模式还能解决:
- 移动零(283题)
- 比较含退格的字符串(844题)
- 链表中删除节点
新手建议:
- 先写暴力解法,再优化
- 多用console.log调试
- 画图辅助理解指针移动
(附:文中所有代码已测试通过,可直接复制到LeetCode运行)