删除排序数组中的重复项 II

数组 双指针

题目描述

给定一个已排序的数组 nums,你需要 原地 删除重复出现的元素,使得每个元素 最多出现两次,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。

上一题的区别:上一题每个元素最多保留 1 次,本题最多保留 2 次

思路分析

数组已排序,重复元素一定相邻。使用 双指针(快慢指针)法:

  • 慢指针 (slow):指向下一个可以写入的位置,初始值为 2
  • 快指针 (fast):从索引 2 开始遍历,逐一判断当前元素是否可以保留。

前两个元素无论如何都保留(即使它们相同),因此两个指针都从索引 2 出发。

关键判断条件

nums[fast] !== nums[slow - 2]

因为数组有序,nums[slow - 2] 是已保留区域的倒数第二个元素。若 nums[fast] 与它不同,说明该元素在已保留区域中出现次数还不到 2 次,可以写入;否则跳过。

算法步骤

  1. 如果数组为空或长度 <= 2,直接返回 nums.length(天然满足每个元素最多出现 2 次)。
  2. 初始化 slow = 2fast 从索引 2 开始遍历。
  3. nums[fast] !== nums[slow - 2],将 nums[fast] 写入 nums[slow],然后 slow++
  4. 遍历结束,返回 slow,即有效元素的个数。

示例代码

/**
 * 从有序数组中原地删除重复项,使每个元素最多出现两次。
 *
 * 思路(双指针):
 *   - slow 指向下一个"可以写入"的位置。
 *   - fast 遍历整个数组,每次检查当前元素是否可以保留。
 *   - 判断条件:nums[fast] !== nums[slow - 2]
 *     即:当前元素与"已保留区域的倒数第二个元素"不相同,
 *     说明它在已保留区域中出现次数还不到 2 次,可以写入。
 *
 * 时间复杂度:O(n)  — 只遍历一次数组
 * 空间复杂度:O(1)  — 原地修改,不使用额外空间
 *
 * @param {number[]} nums - 升序排列的整数数组(原地修改)
 * @returns {number} 去重后有效元素的个数 k;
 *                   调用后 nums 前 k 个元素即为结果。
 *
 * @example
 * const nums = [1, 1, 1, 2, 2, 3];
 * const k = removeDuplicatesV2(nums);
 * // k === 5,nums 前 5 项为 [1, 1, 2, 2, 3]
 */
export function removeDuplicatesV2(nums) {
  // 入参校验:非数组直接返回 0
  if (!nums || !Array.isArray(nums)) {
    return 0;
  }

  // 长度 <= 2 时,每个元素最多出现 2 次的条件天然满足,无需处理
  if (nums.length <= 2) {
    return nums.length;
  }

  // slow:已处理(保留)区域的末尾后一位,初始为 2
  // 前两个元素无论如何都保留,所以从索引 2 开始写入
  let slow = 2;

  for (let fast = 2; fast < nums.length; fast++) {
    // 若当前元素与已保留区域的倒数第二个不同,
    // 说明它不会导致某个值出现 3 次以上,可以保留
    if (nums[fast] !== nums[slow - 2]) {
      nums[slow] = nums[fast]; // 写入到 slow 位置
      slow++; // slow 后移,准备接收下一个合法元素
    }
    // 否则跳过该元素(fast 继续前进,slow 不动)
  }

  // slow 即为有效元素的个数
  return slow;
}

// --- tests ---
const cases = [
  { input: [1, 1, 1, 2, 2, 3], expected: 5 }, // [1,1,2,2,3]
  { input: [0, 0, 1, 1, 1, 1, 2, 3, 3], expected: 7 }, // [0,0,1,1,2,3,3]
  { input: [1, 1], expected: 2 }, // length <= 2, return as-is
  { input: [1], expected: 1 },
  { input: [], expected: 0 },
];

for (const { input, expected } of cases) {
  const arr = [...input];
  const result = removeDuplicatesV2(arr);
  const pass = result === expected;
  console.log(
    `${pass ? 'PASS' : 'FAIL'} removeDuplicatesV2([${input}]) => ${result} (expected ${expected})`,
  );
}

复杂度分析

  • 时间复杂度O(n),其中 n 是数组的长度。只需遍历一次数组。
  • 空间复杂度O(1),只使用了两个额外的索引变量。

示例演示

nums = [1, 1, 1, 2, 2, 3] 为例,逐步追踪双指针的移动:

fastnums[fast]nums[slow-2]说明slow
211相同,跳过2
321不同,写入3
421不同,写入4
532不同,写入5

最终 slow = 5nums 前 5 项为 [1, 1, 2, 2, 3]