引言:为何这个简单问题如此重要?
在编程面试中,"删除有序数组中的重复项" 是一个经典的入门级算法题。看似简单,却蕴含了重要的编程思想------原地算法和双指针技巧。这道题不仅是LeetCode上的高频题目(第26题),更是许多大厂面试的"必考题"。
今天,我将带你深入理解这个问题的本质,掌握双指针法 的精髓,并提供**Python、Java、PHP、C++、C#**五种语言的完整实现。
问题本质:有序数组的特殊性
先明确题目要求:
给定一个升序排列 的数组,你需要原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
关键约束:
-
必须原地修改,空间复杂度O(1)
-
只需返回长度,无需考虑数组中超出新长度的部分
-
数组是有序的(这是解决问题的关键)
核心思想:双指针法
理解双指针
想象你有两个指针在数组中移动:
-
慢指针(i):指向当前已处理的不重复序列的最后一个位置
-
快指针(j):扫描整个数组,寻找新元素
算法步骤
-
边界检查:如果数组为空,直接返回0
-
初始化:慢指针i从0开始,快指针j从1开始
-
遍历比较:当快指针扫描时:
-
如果
nums[j] == nums[i]:继续移动快指针(跳过重复项) -
如果
nums[j] != nums[i]:发现新元素!慢指针前进,将新元素复制过来
-
-
返回结果 :遍历结束,
i+1就是新长度
可视化过程
让我们通过一个例子来理解:
html
原始数组:[0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
步骤:
初始:i=0, j=1 → [0,0,1,1,1,2,2,3,3,4]
nums[0]=0, nums[1]=0 相同 → j++
i=0, j=2 → [0,0,1,1,1,2,2,3,3,4]
nums[0]=0, nums[2]=1 不同 → i=1, nums[1]=1
i=1, j=3 → [0,1,1,1,1,2,2,3,3,4]
nums[1]=1, nums[3]=1 相同 → j++
i=1, j=4 → [0,1,1,1,1,2,2,3,3,4]
nums[1]=1, nums[4]=1 相同 → j++
i=1, j=5 → [0,1,1,1,1,2,2,3,3,4]
nums[1]=1, nums[5]=2 不同 → i=2, nums[2]=2
... 继续这个过程 ...
最终:i=4,新数组前5位是[0,1,2,3,4]
返回长度:5
多语言实现
Python实现(简洁优雅)
python
def removeDuplicates(nums):
"""
删除有序数组中的重复项(Python版本)
时间复杂度:O(n),空间复杂度:O(1)
"""
if not nums: # 处理空数组情况
return 0
i = 0 # 慢指针
for j in range(1, len(nums)): # 快指针遍历
if nums[j] != nums[i]: # 发现新元素
i += 1 # 慢指针前进
nums[i] = nums[j] # 复制新元素到正确位置
return i + 1 # 新长度 = 索引 + 1
# 测试示例
if __name__ == "__main__":
test_cases = [
[1, 1, 2],
[0, 0, 1, 1, 1, 2, 2, 3, 3, 4],
[],
[1],
[1, 2, 3, 4, 5]
]
for nums in test_cases:
print(f"原始数组: {nums}")
new_len = removeDuplicates(nums)
print(f"新长度: {new_len}")
print(f"修改后数组: {nums[:new_len]}")
print("-" * 30)
Java实现(严谨高效)
java
public class RemoveDuplicates {
/**
* 删除有序数组中的重复项(Java版本)
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0; // 空数组情况
}
int i = 0; // 慢指针
for (int j = 1; j < nums.length; j++) { // 快指针
if (nums[j] != nums[i]) { // 发现新元素
i++; // 慢指针前进
nums[i] = nums[j]; // 复制
}
}
return i + 1; // 新长度
}
// 测试代码
public static void main(String[] args) {
RemoveDuplicates solution = new RemoveDuplicates();
int[][] testCases = {
{1, 1, 2},
{0, 0, 1, 1, 1, 2, 2, 3, 3, 4},
{},
{1},
{1, 2, 3, 4, 5}
};
for (int[] nums : testCases) {
System.out.print("原始数组: ");
for (int num : nums) {
System.out.print(num + " ");
}
System.out.println();
int newLen = solution.removeDuplicates(nums);
System.out.println("新长度: " + newLen);
System.out.print("修改后数组: ");
for (int i = 0; i < newLen; i++) {
System.out.print(nums[i] + " ");
}
System.out.println("\n" + "-".repeat(30));
}
}
}
PHP实现(Web开发常用)
php
<?php
/**
* 删除有序数组中的重复项(PHP版本)
* 时间复杂度:O(n),空间复杂度:O(1)
*/
function removeDuplicates(&$nums) {
$n = count($nums);
if ($n == 0) {
return 0; // 空数组情况
}
$i = 0; // 慢指针
for ($j = 1; $j < $n; $j++) { // 快指针
if ($nums[$j] != $nums[$i]) { // 发现新元素
$i++;
$nums[$i] = $nums[$j]; // 复制
}
}
return $i + 1; // 新长度
}
// 测试代码
$testCases = [
[1, 1, 2],
[0, 0, 1, 1, 1, 2, 2, 3, 3, 4],
[],
[1],
[1, 2, 3, 4, 5]
];
foreach ($testCases as $nums) {
echo "原始数组: " . implode(", ", $nums) . "\n";
$newLen = removeDuplicates($nums);
echo "新长度: " . $newLen . "\n";
echo "修改后数组: ";
for ($i = 0; $i < $newLen; $i++) {
echo $nums[$i] . " ";
}
echo "\n" . str_repeat("-", 30) . "\n";
}
?>
C++实现(性能优先)
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
/**
* 删除有序数组中的重复项(C++版本)
* 时间复杂度:O(n),空间复杂度:O(1)
*/
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) {
return 0; // 空数组情况
}
int i = 0; // 慢指针
for (int j = 1; j < nums.size(); j++) { // 快指针
if (nums[j] != nums[i]) { // 发现新元素
i++;
nums[i] = nums[j]; // 复制
}
}
return i + 1; // 新长度
}
};
// 测试代码
int main() {
Solution solution;
vector<vector<int>> testCases = {
{1, 1, 2},
{0, 0, 1, 1, 1, 2, 2, 3, 3, 4},
{},
{1},
{1, 2, 3, 4, 5}
};
for (auto& nums : testCases) {
cout << "原始数组: ";
for (int num : nums) {
cout << num << " ";
}
cout << endl;
int newLen = solution.removeDuplicates(nums);
cout << "新长度: " << newLen << endl;
cout << "修改后数组: ";
for (int i = 0; i < newLen; i++) {
cout << nums[i] << " ";
}
cout << endl << string(30, '-') << endl;
}
return 0;
}
C#实现(.NET生态)
cs
using System;
public class Solution {
/**
* 删除有序数组中的重复项(C#版本)
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public int RemoveDuplicates(int[] nums) {
if (nums.Length == 0) {
return 0; // 空数组情况
}
int i = 0; // 慢指针
for (int j = 1; j < nums.Length; j++) { // 快指针
if (nums[j] != nums[i]) { // 发现新元素
i++;
nums[i] = nums[j]; // 复制
}
}
return i + 1; // 新长度
}
}
public class Program {
// 测试代码
public static void Main() {
Solution solution = new Solution();
int[][] testCases = {
new int[] {1, 1, 2},
new int[] {0, 0, 1, 1, 1, 2, 2, 3, 3, 4},
new int[] {},
new int[] {1},
new int[] {1, 2, 3, 4, 5}
};
foreach (var nums in testCases) {
Console.Write("原始数组: ");
foreach (int num in nums) {
Console.Write(num + " ");
}
Console.WriteLine();
int newLen = solution.RemoveDuplicates(nums);
Console.WriteLine("新长度: " + newLen);
Console.Write("修改后数组: ");
for (int i = 0; i < newLen; i++) {
Console.Write(nums[i] + " ");
}
Console.WriteLine("\n" + new string('-', 30));
}
}
}
算法分析
时间复杂度:O(n)
-
快指针j遍历整个数组一次
-
每个元素最多被访问两次(比较和可能复制)
-
总体操作次数与数组长度成线性关系
空间复杂度:O(1)
-
只使用了常数级别的额外变量(i和j)
-
完全满足原地修改的要求
为什么双指针法如此高效?
-
利用有序性:重复元素必然相邻,只需比较相邻元素
-
一次遍历:不需要嵌套循环,单次扫描完成
-
原地操作:不需要额外数组,节省内存
常见错误与陷阱
错误1:忽略空数组
python
# 错误示例
def removeDuplicates(nums):
i = 0
for j in range(1, len(nums)): # 空数组时len=0,range(1,0)不会执行
# ... 但应该返回0而不是1
return i + 1 # 空数组会错误地返回1
错误2:错误理解返回值
python
# 错误示例
def removeDuplicates(nums):
# ... 处理逻辑
return i # 应该返回i+1
错误3:使用额外空间
python
# 错误示例:不符合原地修改要求
def removeDuplicates(nums):
unique_nums = list(set(nums)) # 创建了新数组
# ... 复制回原数组
return len(unique_nums)
实际应用场景
-
数据库去重:有序记录集的重复数据清理
-
日志分析:时间戳有序的日志去重
-
大数据处理:流式数据中去除连续重复
-
图像处理:连续帧中的重复帧检测
扩展思考
变式1:允许最多重复k次(LeetCode 80)
如果允许每个元素最多出现两次,如何修改算法?
python
def removeDuplicates(nums, k=2):
if len(nums) <= k:
return len(nums)
i = k - 1 # 慢指针
for j in range(k, len(nums)): # 快指针
if nums[j] != nums[i - k + 1]: # 检查是否超过k次重复
i += 1
nums[i] = nums[j]
return i + 1
变式2:无序数组去重
如果数组无序,双指针法还适用吗?
-
需要先排序(O(nlogn)时间)
-
或者使用哈希表(O(n)时间但O(n)空间)
变式3:返回删除的元素
如果要求同时返回被删除的元素,如何修改?
面试技巧
-
先问清楚:数组是否有序?是否原地修改?重复定义是什么?
-
边界条件:空数组、单元素数组、全重复数组
-
测试用例:主动提供多种测试情况
-
复杂度分析:主动分析时间和空间复杂度
-
扩展思考:展示对问题变式的理解
总结
"删除有序数组中的重复项"虽然是一个简单的算法题,但它教授了几个重要的编程概念:
-
双指针技巧:处理数组问题的强大工具
-
原地算法:在有限空间内高效操作
-
边界条件处理:编写健壮代码的关键
-
算法优化:利用数据特性(有序性)简化问题
掌握这个问题的解法,不仅是为了通过面试,更是为了培养解决实际问题的思维模式。双指针法在后续的许多算法问题中都有广泛应用,如:
-
两数之和(有序数组)
-
合并两个有序数组
-
移动零
-
盛最多水的容器
希望这篇详细的讲解能帮助你彻底理解这个问题。建议你不仅记住代码,更要理解背后的思想,并尝试自己实现这些变式问题。
记住:优秀的程序员不是背代码,而是理解算法思想,并能根据问题变化灵活调整解决方案。