|
| 1 | +# 10.2 Contains Duplicate II |
| 2 | + |
| 3 | +> **Source:** [`src/main/kotlin/array/hashtable/ContainsDuplicate_II.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/array/hashtable/ContainsDuplicate_II.kt) |
| 4 | +> **Pattern:** value -> last index · **Core page** |
| 5 | +
|
| 6 | +## The Problem |
| 7 | + |
| 8 | +Given an integer array `nums` and an integer `k`, return `true` if there exist two **distinct** indices `i` and `j` with `nums[i] == nums[j]` and `|i - j| <= k`. |
| 9 | + |
| 10 | +- Constraints: $1 \le n \le 10^5$; $0 \le k \le 10^5$. |
| 11 | + |
| 12 | +## Examples |
| 13 | + |
| 14 | +``` |
| 15 | +Input: nums = [1,2,3,1], k = 3 -> Output: true (the two 1s are 3 apart) |
| 16 | +Input: nums = [1,0,1,1], k = 1 -> Output: true (adjacent 1s) |
| 17 | +Input: nums = [1,2,3,1,2,3], k = 2 -> Output: false (nearest equal pair is 3 apart) |
| 18 | +``` |
| 19 | + |
| 20 | +## Intuition — for each value, remember only its *most recent* occurrence |
| 21 | + |
| 22 | +For each index `i`, the question is: *"did this exact value appear within the last k positions?"* That's a **sliding-window membership test** with a twist — the window is by *position*, and we need the *distance* to the previous occurrence. |
| 23 | + |
| 24 | +The map stores `value -> index of its most recent occurrence`. For each `nums[i]`: |
| 25 | + |
| 26 | +- if it was seen before at `prev` and `i - prev <= k` → answer is `true`; |
| 27 | +- regardless, **update** `map[nums[i]] = i` — the current index becomes the new "most recent". |
| 28 | + |
| 29 | +**Why store only the most recent index?** Any occurrence older than the most recent is *farther away* than it is. If `i - mostRecent > k`, then `i - anyOlder >= i - mostRecent > k` too — an older occurrence can never satisfy the distance bound if the newest one can't. The "nearest duplicate" is always the most recent one, so one slot per value is enough. (This is the same "keep the best-so-far" compression as the [Chapter 3](../ch03-arrays/index.md) two-pointer minimums.) |
| 30 | + |
| 31 | +**The `k`-window alternative:** a sliding `HashSet` of size `k` (add, and when `i > k`, remove `nums[i-k-1]`) also works and is the more *literal* translation of "window of size k". The map version is one structure and stores strictly less when values repeat. |
| 32 | + |
| 33 | +## Approach 1 — Check all pairs (too slow) |
| 34 | + |
| 35 | +For each value, compare all pairs of its occurrences: $O(n^2)$ in the worst case (all values equal). |
| 36 | + |
| 37 | +## Approach 2 — Value-to-last-index map (the repo's version, optimal) |
| 38 | + |
| 39 | +```kotlin |
| 40 | +class ContainsDuplicate_II { |
| 41 | + /** |
| 42 | + * @param nums input array |
| 43 | + * @param k max allowed distance between equal values |
| 44 | + * @return true iff some value repeats within distance k |
| 45 | + */ |
| 46 | + fun containsNearbyDuplicate(nums: IntArray, k: Int): Boolean { |
| 47 | + val map = mutableMapOf<Int, Int>() // value -> most recent index |
| 48 | + |
| 49 | + for (i in nums.indices) { |
| 50 | + val previousIndex = map[nums[i]] |
| 51 | + |
| 52 | + previousIndex?.let { |
| 53 | + if (i - it <= k) return true // within distance k |
| 54 | + } |
| 55 | + map[nums[i]] = i // refresh the most recent index |
| 56 | + } |
| 57 | + return false |
| 58 | + } |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +```java |
| 63 | +import java.util.*; |
| 64 | + |
| 65 | +public class ContainsDuplicateII { |
| 66 | + /** |
| 67 | + * @param nums input array |
| 68 | + * @param k max allowed distance between equal values |
| 69 | + * @return true iff some value repeats within distance k |
| 70 | + */ |
| 71 | + public boolean containsNearbyDuplicate(int[] nums, int k) { |
| 72 | + Map<Integer, Integer> map = new HashMap<>(); // value -> most recent index |
| 73 | + |
| 74 | + for (int i = 0; i < nums.length; i++) { |
| 75 | + Integer prev = map.get(nums[i]); |
| 76 | + if (prev != null && i - prev <= k) return true; |
| 77 | + map.put(nums[i], i); |
| 78 | + } |
| 79 | + return false; |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +```cpp |
| 85 | +#include <unordered_map> |
| 86 | +#include <vector> |
| 87 | + |
| 88 | +class ContainsDuplicateII { |
| 89 | +public: |
| 90 | + /** |
| 91 | + * @param nums input array |
| 92 | + * @param k max allowed distance between equal values |
| 93 | + * @return true iff some value repeats within distance k |
| 94 | + */ |
| 95 | + bool containsNearbyDuplicate(std::vector<int>& nums, int k) { |
| 96 | + std::unordered_map<int, int> map; // value -> most recent index |
| 97 | + |
| 98 | + for (int i = 0; i < (int)nums.size(); i++) { |
| 99 | + if (map.count(nums[i]) && i - map[nums[i]] <= k) return true; |
| 100 | + map[nums[i]] = i; |
| 101 | + } |
| 102 | + return false; |
| 103 | + } |
| 104 | +}; |
| 105 | +``` |
| 106 | + |
| 107 | +```python |
| 108 | +def contains_nearby_duplicate(nums: list[int], k: int) -> bool: |
| 109 | + """ |
| 110 | + @param nums: input array |
| 111 | + @param k: max allowed distance between equal values |
| 112 | + @return: true iff some value repeats within distance k |
| 113 | + """ |
| 114 | + last = {} # value -> most recent index |
| 115 | + for i, num in enumerate(nums): |
| 116 | + if num in last and i - last[num] <= k: |
| 117 | + return True |
| 118 | + last[num] = i |
| 119 | + return False |
| 120 | +``` |
| 121 | + |
| 122 | +```rust |
| 123 | +use std::collections::HashMap; |
| 124 | + |
| 125 | +impl Solution { |
| 126 | + /// @param nums input array |
| 127 | + /// @param k max allowed distance between equal values |
| 128 | + /// @return true iff some value repeats within distance k |
| 129 | + pub fn contains_nearby_duplicate(nums: Vec<i32>, k: i32) -> bool { |
| 130 | + let mut last: HashMap<i32, usize> = HashMap::new(); // value -> most recent index |
| 131 | + |
| 132 | + for (i, &num) in nums.iter().enumerate() { |
| 133 | + if let Some(&prev) = last.get(&num) { |
| 134 | + if i - prev <= k as usize { return true; } |
| 135 | + } |
| 136 | + last.insert(num, i); |
| 137 | + } |
| 138 | + false |
| 139 | + } |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +## Dry run |
| 144 | + |
| 145 | +**Input:** `nums = [1,0,1,1]`, `k = 1`. |
| 146 | + |
| 147 | +``` |
| 148 | +map = {} |
| 149 | +i=0 (1): not seen. store 1 -> 0. map={1:0} |
| 150 | +i=1 (0): not seen. store 0 -> 1. map={1:0, 0:1} |
| 151 | +i=2 (1): seen at 0: 2-0 = 2 > k=1 -> no. refresh: 1 -> 2. map={1:2, 0:1} |
| 152 | +i=3 (1): seen at 2: 3-2 = 1 <= k=1 -> return true ✓ |
| 153 | +``` |
| 154 | + |
| 155 | +The refresh at `i=2` is the whole trick: the duplicate at index 0 is now *irrelevant* — index 2 is closer to any future 1. The stale `1 -> 0` entry is overwritten, so the distance check always runs against the nearest candidate. |
| 156 | + |
| 157 | +## Complexity |
| 158 | + |
| 159 | +**Time.** One pass, $O(1)$ map ops: |
| 160 | + |
| 161 | +$$ |
| 162 | +T(n) = O(n) |
| 163 | +$$ |
| 164 | + |
| 165 | +**Space.** One entry per distinct value: |
| 166 | + |
| 167 | +$$ |
| 168 | +S(n) = O(n) |
| 169 | +$$ |
| 170 | + |
| 171 | +## Variants & follow-ups |
| 172 | + |
| 173 | +- **Contains Duplicate I / III** — I is "any duplicate at all" (a plain set); III is "distance AND value-difference bounds" (a sorted structure like a `TreeSet` of window values — the $O(\log k)$ per-op version of this page). |
| 174 | +- **Longest Substring Without Repeating Characters** (`src/main/kotlin/string/sliding_window/`) — the same "value -> last index" map, but tracking the *window's left boundary* instead of returning early. |
| 175 | +- **Degree Of An Array** (`src/main/kotlin/array/hashtable/DegreeOfAnArray.kt`) — value -> first/last occurrence maps; the "state per value" move extended to two slots per value. |
| 176 | +- **Interview follow-up:** "Why does overwriting with the latest index never lose the answer?" Distance to the *nearest* duplicate is always via the *most recent* occurrence — any older occurrence is strictly farther. So the single-slot-per-value compression is exact, not approximate. |
0 commit comments