原地删除有序数组重复项:双指针法的艺术与实现

引言:为何这个简单问题如此重要?

在编程面试中,"删除有序数组中的重复项" 是一个经典的入门级算法题。看似简单,却蕴含了重要的编程思想------原地算法和双指针技巧。这道题不仅是LeetCode上的高频题目(第26题),更是许多大厂面试的"必考题"。

今天,我将带你深入理解这个问题的本质,掌握双指针法 的精髓,并提供**Python、Java、PHP、C++、C#**五种语言的完整实现。

问题本质:有序数组的特殊性

先明确题目要求:

给定一个升序排列 的数组,你需要原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度

关键约束

  • 必须原地修改,空间复杂度O(1)

  • 只需返回长度,无需考虑数组中超出新长度的部分

  • 数组是有序的(这是解决问题的关键)

核心思想:双指针法

理解双指针

想象你有两个指针在数组中移动:

  • 慢指针(i):指向当前已处理的不重复序列的最后一个位置

  • 快指针(j):扫描整个数组,寻找新元素

算法步骤

  1. 边界检查:如果数组为空,直接返回0

  2. 初始化:慢指针i从0开始,快指针j从1开始

  3. 遍历比较:当快指针扫描时:

    • 如果nums[j] == nums[i]:继续移动快指针(跳过重复项)

    • 如果nums[j] != nums[i]:发现新元素!慢指针前进,将新元素复制过来

  4. 返回结果 :遍历结束,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. 利用有序性:重复元素必然相邻,只需比较相邻元素

  2. 一次遍历:不需要嵌套循环,单次扫描完成

  3. 原地操作:不需要额外数组,节省内存

常见错误与陷阱

错误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. 数据库去重:有序记录集的重复数据清理

  2. 日志分析:时间戳有序的日志去重

  3. 大数据处理:流式数据中去除连续重复

  4. 图像处理:连续帧中的重复帧检测

扩展思考

变式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:返回删除的元素

如果要求同时返回被删除的元素,如何修改?

面试技巧

  1. 先问清楚:数组是否有序?是否原地修改?重复定义是什么?

  2. 边界条件:空数组、单元素数组、全重复数组

  3. 测试用例:主动提供多种测试情况

  4. 复杂度分析:主动分析时间和空间复杂度

  5. 扩展思考:展示对问题变式的理解

总结

"删除有序数组中的重复项"虽然是一个简单的算法题,但它教授了几个重要的编程概念:

  1. 双指针技巧:处理数组问题的强大工具

  2. 原地算法:在有限空间内高效操作

  3. 边界条件处理:编写健壮代码的关键

  4. 算法优化:利用数据特性(有序性)简化问题

掌握这个问题的解法,不仅是为了通过面试,更是为了培养解决实际问题的思维模式。双指针法在后续的许多算法问题中都有广泛应用,如:

  • 两数之和(有序数组)

  • 合并两个有序数组

  • 移动零

  • 盛最多水的容器

希望这篇详细的讲解能帮助你彻底理解这个问题。建议你不仅记住代码,更要理解背后的思想,并尝试自己实现这些变式问题。

记住:优秀的程序员不是背代码,而是理解算法思想,并能根据问题变化灵活调整解决方案。

相关推荐
你怎么知道我是队长2 小时前
C语言---排序算法6---递归归并排序法
c语言·算法·排序算法
智驱力人工智能2 小时前
景区节假日车流实时预警平台 从拥堵治理到体验升级的工程实践 车流量检测 城市路口车流量信号优化方案 学校周边车流量安全分析方案
人工智能·opencv·算法·安全·yolo·边缘计算
MicroTech20252 小时前
微算法科技(NASDAQ :MLGO)抗量子攻击区块链共识机制:通过量子纠缠态优化节点验证流程,降低计算复杂度
科技·算法·区块链
pp起床2 小时前
贪心算法 | part01
算法·贪心算法
梵刹古音2 小时前
【C语言】 字符数组与多维数组
c语言·数据结构·算法
咩咩不吃草2 小时前
机器学习不平衡数据处理三招:k折交叉验证、下采样与过采样实战
人工智能·算法·机器学习·下采样·过采样·k折交叉验证
weixin_452159552 小时前
模板编译期条件分支
开发语言·c++·算法
多恩Stone2 小时前
【3DV 进阶-11】Trellis.2 数据处理与训练流程图
人工智能·pytorch·python·算法·3d·aigc·流程图
老师用之于民2 小时前
【DAY20】数据结构基础:(算法)排序、折半查找的函数实现
数据结构·算法·排序算法