力扣26.有序数组去重:HashSet vs 双指针法

问题描述

给定一个有序 的整数数组,我们需要原地移除所有重复元素,使得每个元素只出现一次,并返回新数组的长度。要求不使用额外的数组空间,空间复杂度为O(1)。

示例:

复制代码
输入:nums = [1,1,2,2,3,4,4,5]
输出:5, nums = [1,2,3,4,5,...]

方法一:HashSet

HashSet是Java集合框架中的一个类,它实现了Set接口,底层基于哈希表实现。HashSet的核心特性是不允许重复元素 ,且不保证元素的顺序

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Iterator;

public class HashSetSolution {
    public int removeDuplicates(int[] nums) {
        // 边界条件检查
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // 创建HashSet存储唯一元素
        HashSet<Integer> uniqueSet = new HashSet<>();
        
        // 遍历原数组,将元素添加到HashSet中
        // HashSet会自动去重,重复元素不会被添加
        for (int num : nums) {
            uniqueSet.add(num);
        }
        
        // 将Set中的元素复制回数组
        // 注意:HashSet不保证顺序,所以结果可能不是有序的
        int index = 0;
        Iterator<Integer> iterator = uniqueSet.iterator();
        while (iterator.hasNext()) {
            nums[index++] = iterator.next();
        }
        
        // 返回唯一元素的数量
        return uniqueSet.size();
    }
}

算法分析

时间复杂度
  • 添加元素到HashSet:O(n)

  • 遍历HashSet:O(k),其中k是唯一元素的数量

  • 总时间复杂度:O(n)

空间复杂度
  • HashSet需要存储所有唯一元素:O(k) ≈ O(n)(最坏情况)
优点
  1. 代码简洁:逻辑直观,易于理解

  2. 通用性强:适用于无序数组的去重

  3. 自动去重:利用Java集合框架的特性

缺点
  1. 破坏顺序:HashSet不保证元素的插入顺序,对于有序数组会打乱原始顺序

  2. 额外空间:需要O(n)的额外空间,不符合题目原地修改的要求

  3. 性能开销:哈希表操作有一定的开销,包括哈希计算、处理哈希冲突等

适用场景

  1. 当数组无序且需要去重时

  2. 当不关心元素顺序时

  3. 当内存空间充足,可以接受O(n)额外空间时

方法二:双指针法(推荐)

双指针法是一种经典的原地算法技巧。我们使用两个指针:

  • 快指针:遍历整个数组,寻找新的不重复元素

  • 慢指针:指向下一个不重复元素应该存放的位置

由于数组是有序 的,重复元素必然相邻,我们可以利用这个特性高效去重。

代码实现

java 复制代码
public class TwoPointerSolution {
    public int removeDuplicates(int[] nums) {
        // 边界条件检查
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // 慢指针,指向下一个不重复元素应该存放的位置
        // 初始为1,因为第一个元素肯定不需要检查
        int slow = 1;
        
        // 快指针,遍历整个数组
        for (int fast = 1; fast < nums.length; fast++) {
            // 如果当前元素不等于前一个元素,说明找到了新的不重复元素
            if (nums[fast] != nums[fast - 1]) {
                // 将不重复元素复制到慢指针的位置
                nums[slow] = nums[fast];
                // 慢指针前进
                slow++;
            }
            // 快指针始终前进
        }
        
        // 慢指针的值就是新数组的长度
        return slow;
    }
    
    // 更简洁的写法
    public int removeDuplicatesConcise(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;
    }
}

算法分析

时间复杂度
  • 只需要遍历数组一次:O(n)
空间复杂度
  • 只使用了几个指针变量:O(1)
优点
  1. 原地修改:不需要额外空间,符合题目要求

  2. 保持顺序:保持原始数组的有序性

  3. 性能高效:只需要一次遍历,没有哈希计算开销

  4. 内存友好:适合处理大规模数据

缺点
  1. 仅适用于有序数组:对于无序数组无效

  2. 需要手动实现:相比HashSet,代码需要自己控制指针

工作原理详解

让我们通过一个例子来理解双指针法:

初始数组:[1, 1, 2, 2, 3, 4, 4, 5]

步骤:

  1. 初始化:slow = 1, fast = 1

  2. fast=1: nums[1]=1, nums[0]=1,相等,跳过

  3. fast=2: nums[2]=2, nums[1]=1,不相等,复制到slow=1,slow=2

  4. fast=3: nums[3]=2, nums[2]=2,相等,跳过

  5. fast=4: nums[4]=3, nums[3]=2,不相等,复制到slow=2,slow=3

  6. 以此类推...

最终结果:前5个元素为[1, 2, 3, 4, 5],返回5

对比分析

特性 HashSet法 双指针法
时间复杂度 O(n) O(n)
空间复杂度 O(n) O(1)
是否保持顺序
是否原地修改
适用数组类型 任意数组 有序数组
代码复杂度 简单 中等
内存使用
性能表现 良好(哈希计算开销) 优秀

实际应用场景

适合使用HashSet的场景

  1. 处理日志去重:日志数据通常无序,且不关心顺序

  2. 用户ID去重:用户ID列表需要快速去重,且顺序不重要

  3. 数据预处理:在数据清洗阶段,需要快速识别和去重

适合使用双指针的场景

  1. 数据库查询结果去重:数据库查询结果通常有序

  2. 时间序列数据处理:时间序列数据天然有序

  3. 算法竞赛:对内存和性能有严格要求

  4. 嵌入式系统:内存受限的环境

变种问题与扩展

问题变种:保留最多k个重复元素

java

复制代码
public int removeDuplicatesK(int[] nums, int k) {
    if (nums.length <= k) return nums.length;
    
    int slow = k;  // 前k个元素肯定保留
    for (int fast = k; fast < nums.length; fast++) {
        // 检查当前元素是否与slow-k位置的元素不同
        if (nums[fast] != nums[slow - k]) {
            nums[slow] = nums[fast];
            slow++;
        }
    }
    return slow;
}

问题变种:无序数组去重(要求原地)

java

复制代码
public int removeDuplicatesUnordered(int[] nums) {
    // 先排序,再使用双指针法
    Arrays.sort(nums);
    return removeDuplicates(nums);
}

最佳实践建议

  1. 分析问题特性:首先判断数组是否有序

  2. 考虑内存限制:如果内存受限,优先考虑双指针法

  3. 考虑顺序要求:如果需要保持顺序,选择双指针法

  4. 代码可读性:在团队开发中,选择易于理解和维护的方法

  5. 性能需求:对于性能敏感的场景,进行基准测试

总结

在有序数组去重的问题中,双指针法无疑是更优的选择。它不仅满足题目要求的原地修改和O(1)空间复杂度,而且保持数组有序,性能优异。虽然HashSet法代码更简洁,但其破坏顺序和额外空间开销的缺点使其不适用于此问题。

关键启示

  • 没有绝对"最好"的算法,只有最适合特定场景的算法

  • 理解问题约束是选择算法的关键

  • 在面试中,不仅要写出解决方案,还要能解释为什么选择这种方法

掌握这两种方法,不仅能够解决有序数组去重问题,还能为处理更复杂的数组操作问题打下坚实基础。在实际开发中,根据具体需求选择合适的算法,是每个开发者必备的技能。

相关推荐
SweetCode2 小时前
汉诺塔问题
android·java·数据库
XFF不秃头2 小时前
力扣刷题笔记-合并区间
c++·笔记·算法·leetcode
p&f°2 小时前
Java面试题(全)自用
java·开发语言
爬山算法2 小时前
Hibernate(9)什么是Hibernate的Transaction?
java·后端·hibernate
Craaaayon2 小时前
深入浅出 Spring Event:原理剖析与实战指南
java·spring boot·后端·spring
麷飞花2 小时前
Intellij Idea 操作汇总
java·intellij-idea·idea
码农水水2 小时前
蚂蚁Java面试被问:接口幂等性的保证方案
java·开发语言·面试
毕设源码-钟学长2 小时前
【开题答辩全过程】以 高校课程档案管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
巧克力味的桃子2 小时前
学习笔记:查找数组第K小的数(去重排名)
笔记·学习·算法