#LC26. 删除有序数组中的重复项

删除有序数组中的重复项

你好!我是你的信息学奥赛教练。这道题(LeetCode 26. 删除有序数组中的重复项)是非常经典的 双指针(Two Pointers) 入门题,也是NOI系列竞赛中数组模拟类的基础。


一、 NOI竞赛风格题目描述

题目名称: 删除有序数组的重复项 (Remove Duplicates) 时间限制: 1.0s 内存限制: 256MB

【题目背景】

小明正在整理一堆按非递减顺序排列的数字卡片。但他发现里面有很多重复的数字,他希望只保留每种数字的一个副本,并且保持它们原来的相对顺序。老师要求他在原地完成整理,不能申请新的卡片堆(即不使用额外的数组空间),最后告诉老师剩下多少张不重复的卡片。

【题目描述】

给定一个长度为 nn 的整数数组 nums,数组中的元素已经按非递减顺序排列。 请你原地删除重复出现的元素,使每个元素只出现一次 ,返回删除后数组的新长度。 元素的相对顺序应该保持一致。

由于在某些语言中无法改变数组的长度,你需要将结果放在数组的前部。具体来说,如果删除重复项后有 kk 个元素,那么 nums 的前 kk 个元素应该保存最终结果。对于 nums 中第 kk 个元素之后的元素,题目并不关心。

你需要输出最终的长度 kk,以及去重后的数组前 kk 个元素。

【输入格式】

第一行包含一个整数 nn,表示数组的长度。 第二行包含 nn 个整数,表示数组 nums 的元素,相邻整数之间用空格分隔。

【输出格式】

第一行输出一个整数 kk,表示去重后的数组长度。 第二行输出 kk 个整数,表示去重后的数组元素,之间用空格分隔。

【样例输入 1】

3
1 1 2

【样例输出 1】

2
1 2

【样例输入 2】

10
0 0 1 1 1 2 2 3 3 4

【样例输出 2】

5
0 1 2 3 4

【数据范围与提示】

  • 1n3×1041 \le n \le 3 \times 10^4
  • 104nums[i]104-10^4 \le nums[i] \le 10^4
  • nums 已按非递减顺序排列。
  • 注意: 必须在原数组上操作,空间复杂度要求为 O(1)O(1)

二、 读题关键词与预备知识

1. 读题关键词(Key Insights)

  • "有序" / "非递减顺序":这是解题的核心!意味着重复的元素一定是相邻的。你不需要在整个数组里到处找重复项,只需要比较相邻的元素。
  • "原地" (In-place):严禁 vector<int> new_array!这意味着你不能创建一个新数组来存结果,必须在原数组的内存块上进行搬运。
  • "O(1) 额外空间":除了几个变量(比如循环变量 i,ji, j),不能申请与 nn 成正比的额外内存。
  • "前 k 个元素":你只需要保证数组前面是正确的去重结果,后面的数据是垃圾数据也没关系。

2. 预备知识点

  • 数组(Array):基本的下标访问与赋值。
  • 双指针算法(Two Pointers):特别是**快慢指针(Fast-Slow Pointers)**技巧。
  • 变量迭代:理解如何在循环中更新状态。

三、 教练的启发式教学:草稿纸上的推演

来,拿出草稿纸,我们不要直接写代码,先画图模拟这个过程。

场景想象: 想象你是一名图书管理员,面前有一排排好的书(有序数组),你的任务是把重复的书拿走,把唯一的书往前挤。 你需要两根手指(指针):

  • 手指 A(慢指针 slow:指向当前已经整理好的序列的最后一个位置。
  • 手指 B(快指针 fast:充当“侦察兵”,不断向后探索,寻找新出现的、不同的书。

推演步骤(以样例 0 0 1 1 1 2 为例):

初始状态: 第1个数字肯定是不重复的(因为它前面没有数字),所以我们从第2个数字开始侦察。 slow 指向下标 0 (数值 0),表示目前整理好的只有第1个。 fast 从下标 1 开始侦察。

下标:   0   1   2   3   4   5
数值: [ 0 , 0 , 1 , 1 , 1 , 2 ]
       ^   ^
    slow  fast

第 1 步: fast 发现 nums[fast] (0)nums[slow] (0) 是一样的。 思考: 是重复的!需要保留吗?不需要。 操作: fast 继续往前走,slow 不动。

下标:   0   1   2   3   4   5
数值: [ 0 , 0 , 1 , 1 , 1 , 2 ]
       ^       ^
    slow      fast

第 2 步: fast 发现 nums[fast] (1)nums[slow] (0) 不一样思考: 发现新大陆了!这是个新数字。 操作:

  1. 既然发现了新数字,整理好的队伍就要加一个人,slow 往前挪一格 (slow++)。
  2. 把这个新数字搬运过来:nums[slow] = nums[fast]
状态变化:
nums[slow+1] 变成了 1
下标:   0   1   2   3   4   5
数值: [ 0 , 1 , 1 , 1 , 1 , 2 ]  <-- 注意下标1变成了1
           ^       ^
         slow     fast (侦察完,准备走下一步)

第 3 步: fast 继续走,指向下标 3 (数值 1)。 比较 nums[fast] (1)nums[slow] (1)思考: 一样,是重复的废纸。 操作: fast 继续走,slow 不动。

... (中间略过 fast 指向下标 4 的情况,也是重复)...

第 N 步: fast 走到了下标 5 (数值 2)。 比较 nums[fast] (2)nums[slow] (1)思考: 不一样!发现新数字。 操作: slow 往前挪 (slow++),搬运数值 (nums[slow] = nums[fast])。

最终结果:
下标:   0   1   2   3   4   5
数值: [ 0 , 1 , 2 , 1 , 1 , 2 ]
               ^               ^
             slow            fast (结束)

最终分析:

  • 循环结束时,slow 指向的是最后一个有效数字的下标。
  • 题目要求返回长度。长度 = 下标 + 1。所以返回 slow + 1

复杂度分析

  • 时间复杂度:你的 fast 指针从头走到尾,每个元素只被访问一次。所以是 O(N)O(N)。这在 N=30000N=30000 的数据范围内非常快(由 10810^8 次操作约为1秒可知,完全无压力)。
  • 空间复杂度:你只用了 slowfast 两个变量,没有开新数组。所以是 O(1)O(1)

四、 NOI风格参考代码 (C++14)

这是你可以提交到评测机的完整代码。注意代码规范,变量命名清晰。

/**
 * NOI Style Solution
 * Language: C++14
 * Problem: Remove Duplicates from Sorted Array (Rewritten)
 */
#include <iostream>
#include <vector>

// 使用标准命名空间,但在大型工程中通常不建议这么做,竞赛中为了速度可以这么写
using namespace std;

// 核心逻辑函数
// 输入引用类型的 vector,以便直接修改原数组
int removeDuplicates(vector<int>& nums) {
    // 边界条件处理:如果是空数组,直接返回0
    if (nums.empty()) {
        return 0;
    }
    
    // 定义慢指针,初始指向第1个元素(下标0)
    int slow = 0;
    
    // 定义快指针,从第2个元素(下标1)开始遍历
    for (int fast = 1; fast < nums.size(); fast++) {
        // 关键点:因为数组是有序的,只要当前元素不等于上一个已保留的元素
        // 就说明遇到了一个新的不重复元素
        if (nums[fast] != nums[slow]) {
            slow++; // 慢指针前进一位,准备接纳新元素
            nums[slow] = nums[fast]; // 将新元素复制到慢指针的位置
        }
        // 如果相等,快指针自动在for循环中fast++跳过,不做处理
    }
    
    // 慢指针指向的是最后一个有效元素的下标
    // 所以长度是 slow + 1
    return slow + 1;
}

int main() {
    // 优化 I/O 操作速度,NOI竞赛必备技巧
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    if (cin >> n) {
        vector<int> nums(n);
        for (int i = 0; i < n; i++) {
            cin >> nums[i];
        }
        
        // 调用解决函数
        int k = removeDuplicates(nums);
        
        // 按题目格式输出
        cout << k << endl;
        for (int i = 0; i < k; i++) {
            cout << nums[i] << (i == k - 1 ? "" : " ");
        }
        cout << endl;
    }
    
    return 0;
}

总结

  1. 关键词:看到“有序”和“原地”,立刻想到双指针。
  2. 核心逻辑nums[slow] != nums[fast] 时才移动 slow 并赋值。
  3. 坑点:别忘了处理空数组(n=0n=0)的情况(虽然本题数据范围 n1n \ge 1,但在面试或更严谨的题目中要注意)。

希望这个思路能帮你在草稿纸上把过程理顺!如果理解了,尝试不看代码自己写一遍吧。