序言:第一次刷 LeetCode 的思维觉醒
作为一名习惯了 JavaScript 灵活语法的开发者,第一次踏入算法的世界,我逐渐意识到,算法练习并不是在学习如何使用 API,而是在学习如何精准地操控内存。 在这个系列中,我将记录下我攻克每一种数据结构的历程。起点便从最常见、操作也最为细腻的数组开始。
数组篇:原地算法(In-place)的逻辑美学
在原地算法中,我们必须像操作底层语言那样去对待 JS 数组:
- 拒绝“黑盒”操作:弃用
splice()。虽然它能一键删除,但会引发后续元素的集体位移,导致时间复杂度飙升至 。 - 拥抱覆盖(Overwrite) :原地算法的真谛不是“删除”,而是用“有效”的数据去覆盖那些“不需要”的数据,将空间复杂度压制在 。
经典题型一:合并两个有序数组 (LeetCode 88)
1. 题目描述
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。
- 注意:
nums1初始化长度为m + n,其中前m个元素表示有效数据,后n个元素为0。
2. 核心思路:逆向双指针
如果从前往后合并,每次向 nums1 插入数据都要挪动其后的所有元素。
巧妙之处:利用 nums1 后端预留的“0”空位,从后往前比较。由于是从最大值开始向最大的索引位填补,我们永远不会在处理完 nums1 的有效数字前覆盖掉它们。
3. 代码实现
JavaScript
var merge = function(nums1, m, nums2, n) {
let p1 = m - 1; // nums1 有效数据末尾
let p2 = n - 1; // nums2 末尾
let p = m + n - 1; // nums1 物理末尾
while (p1 >= 0 && p2 >= 0) {
// 谁大谁往后面放,填补后端空位
if (nums1[p1] > nums2[p2]) {
nums1[p--] = nums1[p1--];
} else {
nums1[p--] = nums2[p2--];
}
}
// 特殊情况:如果 nums2 还没搬完(nums2 还有更小的数),补齐到前端
while (p2 >= 0) {
nums1[p--] = nums2[p2--];
}
};
经典题型二:移除指定元素 (LeetCode 27)
1. 题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。返回移除后数组的新长度 k。你不需要考虑数组中超出新长度后面的元素。
2. 核心思路:快慢指针(读写分离)
这道题教会了我“逻辑删除”。
-
快指针 (fast) :充当“探路者”,寻找数组中不等于
val的有效元素。 -
慢指针 (slow) :充当“记录员”,锁定下一个可以被写入的坑位。
本质:这是一场“洗牌”,快指针负责把好的牌递给慢指针,慢指针按顺序在前端排好。
3. 代码实现
JavaScript
var removeElement = function(nums, val) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
// 只有当快指针发现有效数据时,才写入慢指针指向的“安全区”
if (nums[fast] !== val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow; // 返回有效区域的长度
};
经典题型三:删除有序数组中的重复项 (LeetCode 26)
1. 题目描述
给你一个 非严格递增排列 的数组 nums,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的相对顺序应该保持一致。
2. 核心思路:保送机制与前项对比
既然数组是有序的,那么重复的元素必然是相邻的。
-
逻辑:我们只需要比较当前快指针看到的数,是否和慢指针区域中“最后一个确定的数”相同。
-
避坑细节:这是我第一次遇到索引越界问题。
- 技巧:将
slow和fast都从1开始。因为第一个元素nums[0]前面没有数字,它绝对不可能是重复项,所以它被“保送”通过。 - 安全:通过比较
nums[fast]与nums[slow - 1],我们永远不会触碰到nums[-1]。
- 技巧:将
3. 代码实现
JavaScript
var removeDuplicates = function(nums) {
if (nums.length === 0) return 0;
let slow = 1; // 第一个元素默认有效,直接跳过处理
for (let fast = 1; fast < nums.length; fast++) {
// 拿当前探路数与已确定的前一个有效数对比
// 如果不同,说明遇到了一个新的唯一数字
if (nums[fast] !== nums[slow - 1]) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
};