力扣解题-219. 存在重复元素 II

5 阅读6分钟

力扣解题-219. 存在重复元素 II

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 ij ,满足 nums[i] == nums[j]abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false

示例 1: 输入:nums = [1,2,3,1], k = 3 输出:true

示例 2: 输入:nums = [1,0,1,1], k = 1 输出:true

示例 3: 输入:nums = [1,2,3,1,2,3], k = 2 输出:false

提示:

1 <= nums.length <= 10⁵

-10⁹ <= nums[i] <= 10⁹

0 <= k <= 10⁵

Related Topics

数组、哈希表、滑动窗口


第一次解答(超时)

解题思路

核心方法:暴力双层遍历,通过两层循环遍历所有可能的索引对,逐一校验“数值相等”和“索引差≤k”两个条件,逻辑最简单但时间复杂度极高,直接导致超时。

核心逻辑拆解

该解法的思路是“穷举所有索引对,逐个验证条件”:

  1. 外层slow指针遍历数组每个元素(作为第一个索引);
  2. 内层fast指针遍历数组所有元素(作为第二个索引);
  3. 校验两个核心条件:
    • nums[slow] == nums[fast](数值相等);
    • slow != fast(不同索引);
    • abs(fast - slow) <= k(索引差不超过k);
  4. 只要找到一组满足条件的索引对,立即返回true;遍历完所有组合仍未找到则返回false
超时原因分析
  • 时间复杂度:O(n²)(n为数组长度,最坏情况需遍历n×n个索引对),当n=10⁵时,总操作数达10¹⁰,远超时间限制;
  • 核心缺陷:
    1. 内层循环无边界限制:fast遍历整个数组,大量无效比对(比如slow=0时,fast无需遍历slow+k之后的元素);
    2. 重复校验:slow=0,fast=1slow=1,fast=0会重复校验同一对元素,完全浪费计算资源;
    3. 冗余计算:Math.abs(fast - slow)可通过循环边界直接规避(如fast>slow时无需取绝对值)。
    public boolean containsNearbyDuplicate(int[] nums, int k) {
            for(int slow=0;slow<nums.length;slow++) {
                for (int fast = 1; fast < nums.length; fast++) {
                    if (nums[slow] == nums[fast] && slow!=fast) {
                        int abs = Math.abs(fast - slow);
                        if (abs <= k) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

第二次解答

解题思路

核心方法:优化版暴力遍历(滑动窗口思想雏形),限制内层循环的边界为[slow+1, slow+k],避免无效遍历,大幅减少比对次数,虽仍为暴力法但能通过用例(仅性能极低)。

核心逻辑拆解

该解法在暴力法基础上做了两个关键优化:

  1. 内层循环边界优化fast仅遍历slow+1slow+k的范围(且不超过数组长度),因为超过slow+k的元素与slow的索引差必然大于k,无需校验;
  2. 省略绝对值计算fastslow+1开始,fast - slow天然为正,无需Math.abs()
  3. 提前终止:内层循环一旦找到满足条件的元素,立即返回true,减少无效遍历。
性能说明
  • 时间复杂度:O(n×k)(最坏情况每个slow需遍历k个fast),当k=10⁵时仍接近O(n²),耗时1616ms仅击败5.02%用户;
  • 空间复杂度:O(1)(仅使用指针变量,无额外存储),内存消耗68.63MB击败99.63%用户;
  • 优化效果:相比第一次解答,内层循环次数从n次减少到k次,避免了大量无效比对,从“超时”变为“可通过”,但仍未摆脱暴力法的性能瓶颈。
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        for(int slow=0;slow<nums.length;slow++){
            for(int fast=slow+1;fast<=slow+k && fast<nums.length;fast++){
                if (nums[slow] == nums[fast] && slow!=fast) {
                    int abs = Math.abs(fast - slow);
                    if (abs <= k) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

示例解答

解题思路

解法1:哈希表滑动窗口法(最优解)

核心方法:哈希集合维护滑动窗口,用HashSet存储当前索引i之前的k个元素(窗口范围[i-k, i-1]),遍历数组时只需检查当前元素是否在窗口内,时间复杂度O(n)、空间复杂度O(k),是本题的最优解法。

核心原理铺垫

判断“存在重复元素且索引差≤k”等价于“当前元素在最近k个元素中出现过”:

  1. 维护一个大小不超过k的滑动窗口(窗口内是距离当前元素≤k的所有元素);
  2. 遍历数组时,若当前元素在窗口中,说明存在满足条件的重复元素;
  3. 若不在窗口中,将当前元素加入窗口;
  4. 若窗口大小超过k,移除窗口中最旧的元素(索引为i-k的元素),保证窗口始终只有最近k个元素。
代码实现
import java.util.HashSet;
import java.util.Set;

public boolean containsNearbyDuplicate(int[] nums, int k) {
    Set<Integer> window = new HashSet<>();
    for (int i = 0; i < nums.length; i++) {
        // 当前元素在窗口中,说明存在重复且索引差≤k
        if (window.contains(nums[i])) {
            return true;
        }
        // 加入当前元素到窗口
        window.add(nums[i]);
        // 窗口大小超过k,移除最左边的元素(保持窗口大小≤k)
        if (window.size() > k) {
            window.remove(nums[i - k]);
        }
    }
    // 遍历完未找到
    return false;
}
性能优势
  • 时间复杂度:O(n)(仅遍历数组一次,HashSet的增删查操作均为O(1)),耗时可降至10ms以内(击败99%+用户);
  • 空间复杂度:O(k)(窗口最多存储k个元素);
  • 核心优化点:
    1. 用HashSet的O(1)查找替代内层循环的O(k)遍历,性能呈指数级提升;
    2. 滑动窗口保证只维护最近k个元素,避免存储整个数组;
    3. 提前终止:找到满足条件的元素立即返回,减少无效遍历。
解法2:哈希表记录索引法(备选高效解)

核心方法:HashMap记录元素最后出现的索引,遍历数组时,若当前元素已在Map中,检查索引差是否≤k;若不在则记录当前索引,时间复杂度O(n)、空间复杂度O(n)。

代码实现
import java.util.HashMap;
import java.util.Map;

public boolean containsNearbyDuplicate(int[] nums, int k) {
    Map<Integer, Integer> lastIndexMap = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int num = nums[i];
        // 元素已出现过,检查索引差
        if (lastIndexMap.containsKey(num)) {
            int prevIndex = lastIndexMap.get(num);
            if (i - prevIndex <= k) {
                return true;
            }
        }
        // 更新为最新索引(后续出现的元素与当前索引差更小)
        lastIndexMap.put(num, i);
    }
    return false;
}
优势与适用场景
  • 时间复杂度:O(n),与滑动窗口法持平;
  • 空间复杂度:O(n)(最坏情况存储所有元素),略高于滑动窗口法;
  • 适用场景:若需要记录每个元素的所有出现索引,或k值较大(接近n),该方法更灵活;
  • 核心逻辑:只需记录元素最后出现的索引(因为后续出现的元素与最后索引的差最小,若该差>k则更早的索引差必然>k)。

总结

  1. 纯暴力遍历(第一次解答):O(n²)时间复杂度,完全超时,仅适合理解基础逻辑;
  2. 边界优化暴力法(第二次解答):O(n×k)时间复杂度,可通过但性能极低,是暴力法的极限优化;
  3. 哈希表滑动窗口法(最优解):O(n)时间+O(k)空间,性能最优,工程首选;
  4. 哈希表记录索引法:O(n)时间+O(n)空间,逻辑灵活,适合需要记录索引的场景;
  5. 关键技巧:
    • 涉及“索引差≤k”的重复元素问题,优先考虑滑动窗口+哈希集合,用O(1)查找替代暴力遍历;
    • 滑动窗口的核心是“维护最近k个元素”,避免无效的历史数据存储;
    • 哈希表记录索引时,只需保存最新索引,无需保存所有索引(更小的索引差才可能满足条件)。