加载中...
219-存在重复元素 II(Contains Duplicate II)
发表于:2021-12-03 | 分类: 简单
字数统计: 1.5k | 阅读时长: 6分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/contains-duplicate-ii

英文原文

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) <= k.

 

Example 1:

Input: nums = [1,2,3,1], k = 3
Output: true

Example 2:

Input: nums = [1,0,1,1], k = 1
Output: true

Example 3:

Input: nums = [1,2,3,1,2,3], k = 2
Output: false

 

Constraints:

  • 1 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • 0 <= k <= 105

中文题目

给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 ij 的差的 绝对值 至多为 k

 

示例 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

通过代码

官方题解

概述

这篇文章是为初级读者准备的,文章中会介绍了以下几种方法和数据结构:
线性搜索,二分搜索和散列表。

方法一 (线性搜索) 【超时】

思路
将每个元素与它之前的 $k$ 个元素中比较查看它们是否相等。

算法

这个算法维护了一个 $k$ 大小的滑动窗口,然后在这个窗口里面搜索是否存在跟当前元素相等的元素。

[]
public boolean containsNearbyDuplicate(int[] nums, int k) { for (int i = 0; i < nums.length; ++i) { for (int j = Math.max(i - k, 0); j < i; ++j) { if (nums[i] == nums[j]) return true; } } return false; } // Time Limit Exceeded.

时间复杂度分析

  • 时间复杂度:$O(n \min(k,n))$
    每次搜索都要花费 $O(\min(k, n))$ 的时间,哪怕$k$比$n$大,一次搜索中也只需比较 $n$ 次。

  • 空间复杂度:$O(1)$

方法二 (二叉搜索树) 【超时】

思路

通过自平衡二叉搜索树来维护一个 $k$ 大小的滑动窗口。

算法

这个方法的核心在于降低方法一中搜索前 $k$ 个元素所耗费的时间。可以想一下,我们能不能用一个更复杂的数据结构来维持这个 $k$ 大小的滑动窗口内元素的有序性呢?考虑到滑动窗口内元素是严格遵守先进先出的,那么队列会是一个非常自然就能想到的数据结构。链表实现的队列可以支持在常数时间内的 删除插入,然而 搜索 耗费的时间却是线性的,所以如果用队列来实现的话结果并不会比方法一更好。

一个更好的选择是使用自平衡二叉搜索树(BST)。 BST 中搜索删除插入都可以保持 $O(\log k)$ 的时间复杂度,其中 $k$ 是 BST 中元素的个数。在大部分面试中你都不需要自己去实现一个 BST,所以把 BST 当成一个黑盒子就可以了。大部分的编程语言都会在标准库里面提供这些常见的数据结构。在 Java 里面,你可以用 TreeSet 或者是 TreeMap。在 C++ STL 里面,你可以用 std::set 或者是 std::map

假设你已经有了这样一个数据结构,伪代码是这样的:

  • 遍历数组,对于每个元素做以下操作:
    • 在 BST 中搜索当前元素,如果找到了就返回 true
    • 在 BST 中插入当前元素。
    • 如果当前 BST 的大小超过了 $k$,删除当前 BST 中最旧的元素。
  • 返回 false
[]
public boolean containsNearbyDuplicate(int[] nums, int k) { Set<Integer> set = new TreeSet<>(); for (int i = 0; i < nums.length; ++i) { if (set.contains(nums[i])) return true; set.add(nums[i]); if (set.size() > k) { set.remove(nums[i - k]); } } return false; } // Time Limit Exceeded.

复杂度分析

  • 时间复杂度:$O(n \log (\min(k,n)))$
    我们会做 $n$ 次 搜索删除插入 操作。每次操作将耗费对数时间,即为 $\log (\min(k, n))$。注意,虽然 $k$ 可以比 $n$ 大,但滑动窗口大小不会超过 $n$。

  • 空间复杂度:$O(\min(n,k))$
    只有滑动窗口需要开辟额外的空间,而滑动窗口的大小不会超过 $O(\min(n,k))$。

注意事项

这个算法在 $n$ 和 $k$ 很大的时候依旧会超时。

方法三 (散列表) 【通过】

思路

用散列表来维护这个$k$大小的滑动窗口。

算法

在之前的方法中,我们知道了对数时间复杂度的 搜索 操作是不够的。在这个方法里面,我们需要一个支持在常量时间内完成 搜索删除插入 操作的数据结构,那就是散列表。这个算法的实现跟方法二几乎是一样的。

  • 遍历数组,对于每个元素做以下操作:
    • 在散列表中搜索当前元素,如果找到了就返回 true
    • 在散列表中插入当前元素。
    • 如果当前散列表的大小超过了 $k$, 删除散列表中最旧的元素。
  • 返回 false
[]
public boolean containsNearbyDuplicate(int[] nums, int k) { Set<Integer> set = new HashSet<>(); for (int i = 0; i < nums.length; ++i) { if (set.contains(nums[i])) return true; set.add(nums[i]); if (set.size() > k) { set.remove(nums[i - k]); } } return false; }

复杂度分析

  • 时间复杂度:$O(n)$
    我们会做 $n$ 次 搜索删除插入 操作,每次操作都耗费常数时间。

  • 空间复杂度:$O(\min(n, k))$
    开辟的额外空间取决于散列表中存储的元素的个数,也就是滑动窗口的大小 $O(\min(n,k))$。

统计信息

通过次数 提交次数 AC比率
118753 280886 42.3%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言

相似题目

题目 难度
存在重复元素 简单
存在重复元素 III 中等
上一篇:
218-天际线问题(The Skyline Problem)
下一篇:
220-存在重复元素 III(Contains Duplicate III)
本文目录
本文目录