算法题 数组中重复的数据

442. 数组中重复的数据

问题描述

给定一个整数数组 nums,其中 1 ≤ nums[i] ≤ n(n 为数组长度),数组中某些元素出现两次,其余元素出现一次。找出所有出现两次的元素。

示例

示例 1:

输入:nums = [4,3,2,7,8,2,3,1]
输出:[2,3]

示例 2:

输入:nums = [1,1,2]
输出:[1]

示例 3:

输入:nums = [1]
输出:[]

算法思路

原地哈希标记法

  1. 核心思想:利用数组值作为索引进行标记
  2. 标记策略:将对应位置的数值变为负数表示已访问
  3. 检测重复:当再次访问到负数时,说明该索引对应的值是重复的

关键

  • 数组元素范围 [1,n] 与索引范围 [0,n-1] 存在映射关系:value → index = value-1
  • 可以利用正负号作为"访问标记",不额外占用空间
  • 遍历时,将 nums[Math.abs(nums[i])-1] 变为负数
  • 若发现已是负数,说明 Math.abs(nums[i]) 重复

代码实现

方法:原地哈希标记(最优解)

import java.util.ArrayList;
import java.util.List;

class Solution {
    /**
     * 找出数组中所有出现两次的元素
     * 
     * @param nums 整数数组,1 ≤ nums[i] ≤ n
     * @return 包含所有重复元素的列表
     */
    public List<Integer> findDuplicates(int[] nums) {
        List<Integer> result = new ArrayList<>();
        
        // 第一次遍历:使用原数组进行标记
        for (int i = 0; i < nums.length; i++) {
            // 获取当前元素的绝对值(防止已被标记为负)
            int value = Math.abs(nums[i]);
            
            // 计算对应索引:value-1
            int index = value - 1;
            
            // 检查对应位置是否已被标记
            if (nums[index] > 0) {
                // 未标记:将其变为负数,表示value已出现过
                nums[index] = -nums[index];
            } else {
                // 已标记:说明value是重复元素
                result.add(value);
            }
        }
        
        // 可选:恢复数组原始状态(如果要求不修改原数组)
        // for (int i = 0; i < nums.length; i++) {
        //     nums[i] = Math.abs(nums[i]);
        // }
        
        return result;
    }
}

方法二:交换法(另一种原地解法)

import java.util.ArrayList;
import java.util.List;

class Solution {
    /**
     * 使用交换法将元素放到正确位置
     * 
     * @param nums 整数数组
     * @return 重复元素列表
     */
    public List<Integer> findDuplicates(int[] nums) {
        List<Integer> result = new ArrayList<>();
        
        // 将每个元素放到其应该在的位置
        // 理想情况下:nums[i] = i+1
        for (int i = 0; i < nums.length; i++) {
            // 当前位置的元素不在正确位置,且目标位置的元素也不正确
            while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
                // 交换 nums[i] 和 nums[nums[i]-1]
                int temp = nums[i];
                nums[i] = nums[temp - 1];
                nums[temp - 1] = temp;
            }
        }
        
        // 再次遍历,找出不在正确位置的元素
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != i + 1) {
                // 该位置的元素是重复的
                // 但要避免重复添加
                if (result.isEmpty() || result.get(result.size() - 1) != nums[i]) {
                    result.add(nums[i]);
                }
            }
        }
        
        return result;
    }
}

算法分析

  • 时间复杂度:O(n)
    • 方法一:单次遍历 O(n)
    • 方法二:虽然有while循环,但每个元素最多被交换两次,总体仍为 O(n)
  • 空间复杂度:O(1)
    • 不考虑返回结果占用的空间
    • 原地操作,仅使用常数额外空间

算法过程

输入:nums = [4,3,2,7,8,2,3,1]

方法一执行过程:

初始: [4, 3, 2, 7, 8, 2, 3, 1]

i=0: value=4, index=3
      nums[3]=7>0 → 标记为-7
      数组: [4, 3, 2, -7, 8, 2, 3, 1]

i=1: value=3, index=2  
      nums[2]=2>0 → 标记为-2
      数组: [4, 3, -2, -7, 8, 2, 3, 1]

i=2: value=2, index=1
      nums[1]=3>0 → 标记为-3
      数组: [4, -3, -2, -7, 8, 2, 3, 1]

i=3: value=7, index=6
      nums[6]=3>0 → 标记为-3
      数组: [4, -3, -2, -7, 8, 2, -3, 1]

i=4: value=8, index=7
      nums[7]=1>0 → 标记为-1
      数组: [4, -3, -2, -7, 8, 2, -3, -1]

i=5: value=2, index=1
      nums[1]=-3<0 → 重复!添加2
      result: [2]

i=6: value=3, index=2
      nums[2]=-2<0 → 重复!添加3
      result: [2,3]

i=7: value=1, index=0
      nums[0]=4>0 → 标记为-4
      数组: [-4, -3, -2, -7, 8, 2, -3, -1]

返回: [2,3]

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    int[] nums1 = {4,3,2,7,8,2,3,1};
    System.out.println("Test 1: " + solution.findDuplicates(nums1)); // [2,3]
    
    // 测试用例2:无重复
    int[] nums2 = {1,2,3,4,5};
    System.out.println("Test 2: " + solution.findDuplicates(nums2)); // []
    
    // 测试用例3:全部重复(偶数长度)
    int[] nums3 = {1,1,2,2};
    System.out.println("Test 3: " + solution.findDuplicates(nums3)); // [1,2]
    
    // 测试用例4:单个重复
    int[] nums4 = {1,2,2};
    System.out.println("Test 4: " + solution.findDuplicates(nums4)); // [2]
    
    // 测试用例5:连续重复
    int[] nums5 = {1,1};
    System.out.println("Test 5: " + solution.findDuplicates(nums5)); // [1]
    
    // 测试用例6:空数组
    int[] nums6 = {};
    System.out.println("Test 6: " + solution.findDuplicates(nums6)); // []
    
    // 测试用例7:长度为1
    int[] nums7 = {1};
    System.out.println("Test 7: " + solution.findDuplicates(nums7)); // []
}

关键点

  1. 索引映射关系

    • 数值 x 对应索引 x-1
    • 这是能使用原地标记的前提条件
  2. 正负号标记法

    • 正数:未访问
    • 负数:已访问过一次
    • 再次遇到负数 → 重复
  3. 使用 Math.abs()

    • 必须取绝对值获取原始数值
    • 防止因标记操作影响数值判断
  4. 边界条件

    • 空数组返回空列表
    • 单元素数组无重复
    • 全部元素都可能重复

常见问题

  1. 为什么能保证正确性?

    • 每个数值 x 都会尝试标记位置 x-1
    • 第一次标记成功(正→负)
    • 第二次发现已标记(负),说明重复
    • 不会漏判或误判
  2. 是否修改了原数组?

    • 是的,方法一会修改原数组
    • 如果要求不修改,可在最后恢复数组状态
    • 或使用额外空间的哈希表解法
  3. 能否扩展到找出现k次的元素?

    • 正负号只能表示0次/1次/≥2次
    • 找恰好k次需要其他方法(如计数排序)
  4. 与其他类似问题的关系?

    • 与448. 找到所有数组中消失的数字思路相同
    • 都是利用值域与索引的映射关系进行原地标记
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值