1. 概述

在数组中查找第一个重复元素的索引是常见的编程问题,有多种解决方法。虽然暴力法简单直接,但对于大数据集效率低下。

本教程将探讨不同解决方案,从基础的暴力法到使用 HashSet 和数组索引等优化技术。

2. 问题定义

给定一个整数数组,找出第一个重复元素的索引。

输入: arr = [2, 1, 3, 5, 3, 2]
输出: 4
解释: 第一个重复的元素是 3,其第二次出现在索引 4 处。

输入: arr = [1, 2, 3, 4, 5]
输出: -1
解释: 没有重复元素,返回 -1

3. 暴力法

这种方法通过双重循环检查每个元素与后续元素是否重复。

先看实现代码,再逐步解析:

int firstDuplicateBruteForce(int[] arr) {
    int minIndex = arr.length;
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[i] == arr[j]) {
                minIndex = Math.min(minIndex, j);
                break;
            }
        }
    }
    return minIndex == arr.length ? -1 : minIndex;
}

代码解析:

  • 外层循环遍历每个元素 i,内层循环查找后续重复元素 j
  • 发现重复时,用 minIndex 记录最早出现的重复索引
  • break 确保只记录每个元素的第一次重复
  • 最终若 minIndex 未更新则返回 -1

时间复杂度:O(n²)(双重循环)
空间复杂度:O(1)(仅用常数空间)

4. 使用 HashSet

利用 HashSet 存储已遍历元素,遇到重复时立即返回索引。

实现代码:

int firstDuplicateHashSet(int[] arr) {
    HashSet<Integer> firstDuplicateSet = new HashSet<>();
    for (int i = 0; i < arr.length; i++) {
        if (firstDuplicateSet.contains(arr[i])) {
            return i;
        }
        firstDuplicateSet.add(arr[i]);
    }
    return -1;
}

代码解析:

  • 创建空 HashSet
  • 遍历数组时检查元素是否已存在
    • 存在则返回当前索引(第一个重复)
    • 不存在则添加到集合
  • 遍历结束无重复则返回 -1

示例执行过程:

First Duplicate Hashset

时间复杂度:O(n)(单次遍历,哈希操作平均 O(1)
空间复杂度:O(n)(最坏需存储所有元素)

5. 数组索引法

当元素为正数且范围在 [1, n] 时(n 为数组长度),可通过修改数组自身实现空间优化。

实现代码:

int firstDuplicateArrayIndexing(int[] arr) {
    for (int i = 0; i < arr.length; i++) {
        int val = Math.abs(arr[i]) - 1;
        if (arr[val] < 0) {
            return i;
        }
        arr[val] = -arr[val];
    }
    return -1;
}

代码解析:

  • 遍历时将元素值转为索引(val = |arr[i]| - 1
  • 检查目标位置是否已被标记(负值)
    • 若为负,说明重复,返回当前索引
    • 否则标记为负值
  • 无重复则返回 -1

示例执行过程:

First Duplicate Array Indexing

时间复杂度:O(n)(单次遍历)
空间复杂度:O(1)(原地修改数组)

6. 总结

本文探讨了三种查找数组第一个重复元素的方法,各有优劣:

方法 时间复杂度 空间复杂度 适用场景
暴力法 O(n²) O(1) 小数据集,无需额外空间
HashSet O(n) O(n) 通用方案,允许额外空间
数组索引法 O(n) O(1) 元素为正且范围在 [1, n]

关键选择点

  • 允许修改数组且元素范围受限 → 数组索引法
  • 需要通用解法 → HashSet
  • 空间敏感但数据量小 → 暴力法

掌握这些方法后,可根据实际约束灵活选择最优解。