287. Find The Duplicate Number - Explanation
Description
You are given an array of integers nums containing n + 1 integers. Each integer in nums is in the range [1, n] inclusive.
There is exactly one repeated integer in nums, and every other integer appears at most once.
Return the repeated integer.
Example 1:
Input: nums = [1,2,3,2,2]
Output: 2Example 2:
Input: nums = [1,2,3,4,4]
Output: 4Follow-up: Can you solve the problem without modifying the array nums and using extra space?
Constraints:
1 <= n <= 10,000nums.length == n + 11 <= nums[i] <= n
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(n) time and O(1) space without modifying the input array, where n is the size of the input array.
Hint 1
A naive approach would be to use a hash set, which provides O(1) average-time lookups but requires O(n) extra space. To avoid extra space, consider treating the array like a linked list: from an index i, the next index is nums[i]. Every value is in the range 1 to len(nums) - 1, so every value is also a valid index.
Hint 2
Since one value is repeated, multiple indices point to the same next index. Following the indices starting from index 0 must therefore lead to a cycle, whose entry corresponds to the duplicate value. Which algorithm can detect a cycle using constant extra space without modifying the array?
Hint 3
Use Floyd's cycle detection algorithm. Initialize a slow and a fast pointer at index 0. Repeatedly move the slow pointer one step with slow = nums[slow] and the fast pointer two steps with fast = nums[nums[fast]] until they meet. Is this first meeting point necessarily the duplicate?
Hint 4
The first meeting point may be anywhere inside the cycle. After the pointers meet, initialize a second slow pointer at index 0. Move both slow pointers one step at a time using the values in nums. The index where they meet is the cycle entry, which is the duplicate number.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Array Manipulation - Working with array indices and understanding value-to-index mappings
- Hash Sets - Using sets for O(1) lookup to detect duplicates
- Floyd's Cycle Detection - The fast and slow pointer technique for detecting cycles in linked list-like structures
- Binary Search on Value Range - Applying binary search to count-based problems rather than sorted arrays
1. Sorting
Intuition
If we sort the array, any duplicate numbers will appear next to each other.
So after sorting, we just scan once and check if any two consecutive elements are equal.
The first equal pair we find is the duplicate.
Algorithm
- Sort the array.
- Loop through the array from index
0ton - 2. - For each index
i, check ifnums[i] == nums[i + 1].- If yes, return that value (this is the duplicate).
- If no duplicate is found (theoretically impossible for this problem), return
-1.
Time & Space Complexity
- Time complexity:
- Space complexity: or depending on the sorting algorithm.
2. Hash Set
Intuition
We can detect duplicates by remembering which numbers we have already seen.
As we scan the array, each new number is checked:
- If it's not in the set, we add it.
- If it is already in the set, that number must be the duplicate.
A set gives constant-time lookup, so this approach is simple and efficient.
Algorithm
- Create an empty hash set
seen. - Loop through each number in the array:
- If the number is already in
seen, return it (this is the duplicate). - Otherwise, insert the number into
seen.
- If the number is already in
- If no duplicate is found (should not happen in this problem), return
-1.
Time & Space Complexity
- Time complexity:
- Space complexity:
3. Array
Intuition
Since the values in the array are from 1 to n, we can use an array to track whether we've seen a number before.
Each number directly maps to an index (num - 1).
- If that index is already marked, we've seen the number before → it's the duplicate.
- Otherwise, we mark it as seen.
This avoids using a hash set while still providing fast lookups.
Algorithm
- Create an array
seenof sizenfilled with zeros. - For each number
numin the input array:- Check if
seen[num - 1]is already set to1.- If yes, return
num(duplicate found).
- If yes, return
- Otherwise, set
seen[num - 1] = 1.
- Check if
- Return
-1if no duplicate is found (though the problem guarantees one).
Time & Space Complexity
- Time complexity:
- Space complexity:
4. Negative Marking
Intuition
Since every value is between 1 and n, each number corresponds to an index in the array (num - 1).
We can use the array itself as a marking tool:
- When we see a number, we go to its corresponding index and flip the sign of the value there.
- If we ever visit an index that is already negative, it means we've visited this number before → it's the duplicate.
This method avoids extra memory and uses the input array as a tracking structure.
Algorithm
- Loop through every number
numin the array. - Compute its corresponding index:
idx = abs(num) - 1. - If
nums[idx]is already negative:- A duplicate has been found, return
abs(num).
- A duplicate has been found, return
- Otherwise:
- Mark the index by multiplying
nums[idx]by-1.
- Mark the index by multiplying
- If no duplicate is found (though guaranteed), return
-1.
Time & Space Complexity
- Time complexity:
- Space complexity:
5. Binary Search
Intuition
This method uses binary search on the value range, not on the array itself.
If all numbers from 1 to mid appeared at most once, then the count of numbers <= mid should be <= mid.
But if the count is greater than mid, it means the duplicate must be in the range [1, mid], because too many numbers fall into that range.
So we repeatedly:
- Count how many values are
<= mid. - Shrink the search space based on whether this count is "too large."
Eventually, low == high, and that value is the duplicate.
Algorithm
- Let
low = 1andhigh = n - 1(value range). - While
low < high:- Compute
mid = (low + high) // 2. - Count how many numbers in the array are
<= mid. - If the count is greater than
mid, the duplicate lies in[low, mid], so sethigh = mid. - Otherwise, it lies in
[mid + 1, high], so setlow = mid + 1.
- Compute
- When the loop ends,
lowis the duplicate. - Return
low.
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
n = len(nums)
low, high = 1, n - 1
while low < high:
mid = low + (high - low) // 2
lessOrEqual = sum(1 for num in nums if num <= mid)
if lessOrEqual <= mid:
low = mid + 1
else:
high = mid
return lowTime & Space Complexity
- Time complexity:
- Space complexity:
6. Bit Manipulation
Intuition
Every number from 1 to n−1 should appear exactly once, but in the array, one number appears twice.
So for each bit position, we compare:
- How many times this bit is set among all numbers in the array.
- How many times this bit should be set among the numbers
1ton-1.
If a bit appears more times in the array than expected, that bit must belong to the duplicate number.
By combining all such bits, we reconstruct the duplicate.
Algorithm
- Let
res = 0to build the duplicate number bit by bit. - For each bit position
bfrom0to31:- Compute
mask = 1 << b. - Count how many numbers in
numshave this bit set, call itx. - Count how many numbers from
1ton - 1should have this bit set, call ity.
- Compute
- If
x > y, it means the duplicate has this bit set, so add it to the answer:res |= mask.
- After processing all bits, return
res.
Time & Space Complexity
- Time complexity:
- Space complexity:
7. Fast And Slow Pointers
Intuition
Treat the array like a linked list, where each index points to the next index given by its value.
Because one number is duplicated, two indices will point into the same chain, creating a cycle — exactly like a linked list with a loop.
Using Floyd’s Fast & Slow Pointer technique:
- The slow pointer moves one step at a time.
- The fast pointer moves two steps at a time.
- If there’s a cycle, they will eventually meet.
Once they meet, we start a new pointer from the beginning:
- Move both pointers one step at a time.
- The point where they meet again is the duplicate number (the entry point of the cycle).
Algorithm
- Initialize two pointers
slow = 0andfast = 0. - Move:
slow = nums[slow]fast = nums[nums[fast]]
untilslow == fast.
- Start a new pointer
slow2 = 0. - Move both:
slow = nums[slow]slow2 = nums[slow2]
until they meet.
- The meeting point is the duplicate number.
- Return that number.
Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Modifying the Array When Not Allowed
Some problem variants require not modifying the input array. Solutions like negative marking or in-place sorting alter the original data. Always check the constraints before choosing an approach that mutates the input.
Off-by-One Errors in Index Mapping
Since values range from 1 to n but array indices start at 0, forgetting to subtract 1 when mapping values to indices causes out-of-bounds errors or incorrect results. For example, value n maps to index n-1, not index n.
Misunderstanding Floyd's Cycle Detection Entry Point
In the fast and slow pointer approach, the meeting point of the two pointers is not the duplicate number. After they meet, you must start a new pointer from index 0 and move both pointers one step at a time until they meet again. This second meeting point is the cycle entry, which corresponds to the duplicate value.
Sign in to join the discussion