# JS 算法实战手册:数组篇(一)

22 阅读4分钟

序言:第一次刷 LeetCode 的思维觉醒

作为一名习惯了 JavaScript 灵活语法的开发者,第一次踏入算法的世界,我逐渐意识到,算法练习并不是在学习如何使用 API,而是在学习如何精准地操控内存。 在这个系列中,我将记录下我攻克每一种数据结构的历程。起点便从最常见、操作也最为细腻的数组开始。


数组篇:原地算法(In-place)的逻辑美学

在原地算法中,我们必须像操作底层语言那样去对待 JS 数组:

  • 拒绝“黑盒”操作:弃用 splice()。虽然它能一键删除,但会引发后续元素的集体位移,导致时间复杂度飙升至 O(n2)O(n^2)
  • 拥抱覆盖(Overwrite) :原地算法的真谛不是“删除”,而是用“有效”的数据去覆盖那些“不需要”的数据,将空间复杂度压制在 O(1)O(1)

经典题型一:合并两个有序数组 (LeetCode 88)

1. 题目描述

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn。请你合并 nums2nums1 中,使合并后的数组同样按非递减顺序排列。

  • 注意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. 核心思路:保送机制与前项对比

既然数组是有序的,那么重复的元素必然是相邻的。

  • 逻辑:我们只需要比较当前快指针看到的数,是否和慢指针区域中“最后一个确定的数”相同。

  • 避坑细节:这是我第一次遇到索引越界问题。

    • 技巧:将 slowfast 都从 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;
};