Difficulty: Easy Pattern: Sliding Window LeetCode: https://leetcode.com/problems/contains-duplicate-ii/
k elements sliding across the array. glossaryGiven an integer array nums and an integer k, return true if there exist two
distinct indices i and j such that nums[i] == nums[j] and abs(i - j) <= k.
Otherwise return false.
Signature:
boolean containsNearbyDuplicate(int[] nums, int k)
Example 1:
Input: nums = [1,2,3,1], k = 3
Output: true
(nums[0] == nums[3] and abs(0 - 3) = 3 <= 3.)
Example 2:
Input: nums = [1,0,1,1], k = 1
Output: true
(nums[2] == nums[3] and abs(2 - 3) = 1 <= 1.)
Example 3:
Input: nums = [1,2,3,1,2,3], k = 2
Output: false
You are checking tickets at a gate, and the rule is “no two identical tickets within the
last k people”. You keep a tray holding the tickets of the most recent k people. For
each new person, you glance at the tray – if their ticket is already there, that is a
violation (return true). Then you drop their ticket onto the tray, and if the tray now
holds more than k tickets, you remove the one that has fallen out of range.
The condition abs(i - j) <= k is just another way of saying “the two equal values must
sit within k positions of each other”. So we only need to remember the last k values
seen – nothing older can ever be part of a valid pair.
Smallest example, nums = [1, 2, 3, 1], k = 3:
General rule: keep a hash set of the last k values. At each new element, check membership
– a hit means a nearby duplicate. Then add the element, and if the set now holds more than
k values, remove the one that fell out of range (nums[right - k]).
The invariant is: after processing index right, the set holds exactly the values at
indices right-k+1 .. right. So the next iteration’s membership check covers precisely the
allowed look-back range. Note the guard is size > k, not >= k: the window is allowed to
hold k values, and we evict only the (k+1)-th. The edge case k = 0 works for free –
each value is removed the instant it is added, so the answer is always false (two distinct
indices can never be 0 apart).
This is a fixed-distance sliding window. 0003 Longest Substring Without Repeating Characters reuses the “hash structure tracks the recent window” reflex with a window that grows and shrinks, and 0209 Minimum Size Subarray Sum uses a window whose state is a running sum rather than a set.
Pause and pick before expanding. A wrong first guess teaches more than a fast right one.
Q1 (recall). After processing index right, what does the hash set contain?
k positionsQ2 (comprehend). Why is the eviction guard size > k and not >= k?
k values, so we evict only the (k+1)-th>= k would skip a valuek itemsfunction containsNearbyDuplicate(nums, k):
window = empty set
for right from 0 to len(nums)-1:
if nums[right] in window:
return true # duplicate within the last k indices
add nums[right] to window
if size of window > k: # window would reach back further than k
remove nums[right - k] from window
return false
After processing index right, the set contains the last k values (indices
right-k+1 .. right), which is exactly what the next iteration needs to check.
import java.util.HashSet;
import java.util.Set;
class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
Set<Integer> window = new HashSet<>();
for (int right = 0; right < nums.length; right++) {
if (window.contains(nums[right])) {
return true;
}
window.add(nums[right]);
if (window.size() > k) {
window.remove(nums[right - k]);
}
}
return false;
}
}
A HashSet is enough: we only ever ask “is this value within range?”, not “where was
it?”. The set holds the most recent k values, so contains is the duplicate check.
The eviction line remove(nums[right - k]) fires exactly when the window has grown past
k, keeping the look-back range at <= k. Note the guard size > k, not >= k: the
window is allowed to hold k values, evict only the k+1-th. The edge k = 0 works
for free – size > 0 is always true after an add, so each value is removed the moment
it is inserted, and the answer is always false (two distinct indices cannot satisfy
abs(i - j) <= 0).
Time: O(n) -- one pass; each element is added once and removed at most once.
Space: O(k) -- the set never holds more than k+1 elements.
Step-by-step on nums = [1,2,3,1], k = 3:
| right | nums[right] | window (before) | in window? | window (after add) | size > 3? | evict | window (end) |
|---|---|---|---|---|---|---|---|
| 0 | 1 | {} | no | {1} | no | - | {1} |
| 1 | 2 | {1} | no | {1,2} | no | - | {1,2} |
| 2 | 3 | {1,2} | no | {1,2,3} | no | - | {1,2,3} |
| 3 | 1 | {1,2,3} | YES | return true | - | - | - |
Return true.
Q1 (apply). Trace nums = [1, 2, 1], k = 1. What is returned?
truefalseQ2 (analyze). What happens when k = 0, and why is no special case needed?
false; each value is evicted the instant it is added, so no two distinct indices can ever be 0 apartIndexOutOfBoundsExceptiontrue for any arrayQ3 (transfer). Suppose instead of a boolean you had to return the smallest index distance between any equal-valued pair within k. What minimal change captures that?
Map<Integer, Integer> of every index instead of just the last k values –
works but wastes memory and obscures the sliding-window idea.k apart.remove(nums[right - k - 1]) or evicting before the size
check breaks the “window holds last k” invariant.abs(i - j) < k (strict) instead of <= k.k = 0. The guard size > k handles it; right - k is never negative
because eviction only runs after at least k + 1 adds.