1. 问题背景与核心需求
这道题目来自LeetCode题库的第26题,题目描述为:给定一个升序排列的数组nums,要求原地删除重复出现的元素,使每个元素只出现一次,并返回新数组的长度。不能使用额外的数组空间,必须在原数组上修改,并使用O(1)额外空间。
这个问题看似简单,但考察了几个关键编程能力:
- 对数组基本操作的理解
- 双指针技巧的灵活运用
- 边界条件的处理能力
- 时间/空间复杂度的分析能力
在实际工程中,类似场景也很常见。比如处理日志数据时去除重复条目、数据库查询结果去重等。理解这个基础算法对处理更大规模数据有重要意义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 双指针解法详解
2.1 基本思路解析
双指针法是解决这类问题的经典方法。具体思路是:
- 使用一个慢指针(通常记为i或slow)指向当前不重复序列的末尾
- 使用一个快指针(通常记为j或fast)遍历整个数组
- 当发现nums[fast] ≠ nums[slow]时,将nums[fast]的值复制到nums[slow+1]
- 最后返回slow+1作为新数组长度
这种方法的妙处在于:
- 时间复杂度O(n),只需遍历一次数组
- 空间复杂度O(1),没有使用额外存储空间
- 保持了原数组元素的相对顺序
2.2 代码实现示例
以下是Python的典型实现:
python复制def removeDuplicates(nums):
if not nums:
return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
关键点说明:
- 首先处理空数组的特殊情况
- slow初始化为0,fast从1开始
- 比较逻辑确保只有遇到新元素时才移动slow
- 最后返回的是长度,所以是slow+1
3. 边界条件与异常处理
3.1 特殊输入情况
实际编码时需要特别注意以下边界条件:
- 空数组输入:应返回0
- 单元素数组:直接返回1
- 全相同元素的数组:应返回1
- 已经去重的数组:应返回原长度
3.2 常见错误分析
初学者容易犯的错误包括:
- 忘记处理空数组情况导致索引越界
- 返回slow而不是slow+1
- 在移动slow指针前未先检查fast指针的值
- 使用额外数组存储结果(违反题目要求)
提示:在LeetCode上提交前,务必用这些边界案例测试你的代码:
- []
- [1]
- [1,1,1]
- [1,2,3]
4. 算法复杂度与优化
4.1 时间复杂度分析
该算法的时间复杂度是O(n),因为:
- 只需要一次遍历数组(fast指针从1到n-1)
- 每次循环中的操作都是常数时间
- 没有嵌套循环或其他复杂操作
4.2 空间复杂度分析
空间复杂度是O(1),因为:
- 只使用了固定数量的额外变量(slow, fast等)
- 没有使用与输入规模相关的额外存储空间
- 所有操作都在原数组上完成
4.3 可能的优化方向
虽然这个解法已经很高效,但还可以考虑:
- 添加early termination:当fast到达数组末尾时可以提前结束
- 对于几乎无重复的数组,可以减少不必要的赋值操作
- 使用while循环可能比range更灵活
优化后的Python实现示例:
python复制def removeDuplicates(nums):
if len(nums) < 2:
return len(nums)
slow = 0
fast = 1
while fast < len(nums):
if nums[fast] != nums[slow]:
slow += 1
if slow != fast: # 避免不必要的赋值
nums[slow] = nums[fast]
fast += 1
return slow + 1
5. 不同语言的实现对比
5.1 Java实现
java复制public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int slow = 0;
for (int fast = 1; fast < nums.length; fast++) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
Java版本特点:
- 数组长度通过length属性获取
- 前置自增(++slow)简化代码
- 强类型需要明确定义变量类型
5.2 C++实现
cpp复制int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int slow = 0;
for (int fast = 1; fast < nums.size(); ++fast) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
C++版本注意点:
- 使用vector的empty()和size()方法
- 参数通过引用传递避免拷贝
- 前置自增可能带来轻微性能优势
5.3 JavaScript实现
javascript复制function removeDuplicates(nums) {
if (nums.length === 0) return 0;
let slow = 0;
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
JS版本特点:
- 使用严格相等运算符!==
- let声明块级作用域变量
- 数组是对象,length是属性
6. 实际应用场景扩展
6.1 数据库查询结果去重
在数据库操作中,经常需要对查询结果去重。虽然SQL有DISTINCT关键字,但有时需要在应用层进一步处理:
python复制# 假设从数据库获取了可能有重复的记录
records = query_database(...)
# 先按某个字段排序
records.sort(key=lambda x: x['id'])
# 然后使用类似算法去重
slow = 0
for fast in range(1, len(records)):
if records[fast]['id'] != records[slow]['id']:
slow += 1
records[slow] = records[fast]
unique_records = records[:slow+1]
6.2 日志数据处理
处理服务器日志时,可能需要去除连续的重复错误消息:
python复制def dedup_logs(logs):
if not logs:
return []
slow = 0
for fast in range(1, len(logs)):
if logs[fast]['message'] != logs[slow]['message'] or \
logs[fast]['level'] != logs[slow]['level']:
slow += 1
logs[slow] = logs[fast]
return logs[:slow+1]
6.3 扩展至删除重复项II
LeetCode第80题是这个问题的小变种,允许最多保留2个重复项。解法思路类似,只是比较条件稍作修改:
python复制def removeDuplicates(nums):
if len(nums) <= 2:
return len(nums)
slow = 1
for fast in range(2, len(nums)):
if nums[fast] != nums[slow-1]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
这个变种考察了对双指针技巧的灵活运用能力。
7. 解题思路的通用性
双指针技巧在数组相关问题中应用广泛,以下类似问题都可以用这种思路解决:
- 移动零(LeetCode 283):将0移动到数组末尾
- 移除元素(LeetCode 27):移除指定值的所有实例
- 合并两个有序数组(LeetCode 88)
- 两数之和 II(LeetCode 167)
掌握这种技巧的关键在于:
- 明确每个指针的职责
- 确定指针移动的条件
- 处理好边界情况和循环不变式
在实际面试中,面试官可能会先问这个基础问题,然后逐步增加难度,考察候选人能否举一反三。
