Table of Contents
Problem Description
Given an array n + 1 containing nums integers, where the numbers are all in the range [1, n] (inclusive of 1 and n), it is known that at least one duplicate integer exists.
Assuming that nums contains only one duplicate integer, return this duplicate number.
Your designed solution must not modify the array nums, and must use only constant-level O(1) extra space.
Example 1:
Input: nums = [1,3,4,2,2]Output: 2Example 2:
Input: nums = [3,1,3,4,2]Output: 3Example 3:
Input: nums = [3,3,3,3,3]Output: 3Hints and Constraints:
1 <= n <= 10^5nums.length == n + 11 <= nums[i] <= nnumscontains only one integer that appears twice or more, and all other integers appear only once
Why at least one duplicate must exist
This is a direct application of the pigeonhole principle (drawer principle):
Place
n + 1“pigeons” inton“nests”; at least one nest must contain two pigeons.
In this problem, the “pigeons” are the n + 1 elements in the array, and the “nests” are the n numbers in the value range 1..n. The number of elements is one more than the number of possible values, so some number must appear at least twice—the duplicate is guaranteed to exist.
Solution 1: Binary search on the value range (O(n log n) time / O(1) space)
Core insight
Intuitively, binary search requires a “sorted array”; but the key to this problem is: we are not binary searching over array indices, but over the value range [1, n].
For any candidate value mid ∈ [1, n], count the number of elements in nums satisfying ≤ mid, denoted as cnt:
- If
cnt > mid: this means the range[1, mid]“contains more elements than the range itself.” By the pigeonhole principle, the duplicate must lie in[1, mid], so tighten the right boundary tomid. - If
cnt <= mid: this means the duplicate is not in[1, mid], but in[mid + 1, n], so advance the left boundary tomid + 1.
Why must
cnt > midcontain a duplicate? Because the range[1, mid]has onlymiddistinct numbers in total; if each of them appeared at most once, then≤ midwould contain at mostmidelements; nowcnt > mid, which means some number must appear more than once.
Keep binary searching, and the final converged left is the duplicate number.
Step-by-step illustration
Take nums = [1,3,4,2,2], n = 4 as an example (value range [1,4]):
Initial: left=1, right=4
mid = 2, count elements ≤ 2: [1,2,2] → cnt = 33 > 2 → the duplicate is in [1,2], right = 2
mid = 1, count elements ≤ 1: [1] → cnt = 11 > 1 does not hold → the duplicate is in [2,2], left = 2
left == right == 2 → return 2 ✔Complexity
| Dimension | Analysis |
|---|---|
| Time complexity | O(n log n) — binary search takes O(log n) rounds in total, each round scans the entire array O(n) |
| Space complexity | O(1) — only a few counting variables are used |
| Whether the array is modified | No — read-only throughout |
Solution 2: Fast and slow pointers / Floyd cycle detection (O(n) time / O(1) space)
Core insight: treat the array as a linked list
This is the most ingenious step in this problem—treat the array as a linked list:
- Treat each index
ias a “node” of the linked list; - The
nextpointer of nodeipoints tonums[i], i.e.,i → nums[i].
Since all element values are in [1, n], and the array length is n + 1, therefore:
- No node points to index
0(all values are ≥ 1); - Starting from index
0and followingnext, one must eventually enter a “cycle.”
And the entrance of this cycle is exactly the duplicate number—because the duplicate number d is pointed to by at least two different indices simultaneously, forming “two paths merging into the same node,” which is precisely the characteristic of a cycle entrance.
Thus “finding the duplicate number” is transformed into the familiar problem of “finding the entrance of a cycle in a linked list,” and we can directly apply Floyd cycle detection.
Two-phase algorithm
- Phase 1: Find the meeting point inside the cycle.
slowmoves one step at a time (slow = nums[slow]),fastmoves two steps at a time (fast = nums[nums[fast]]), until the two meet. - Phase 2: Find the cycle entrance. Reset
slowback to index0, thenslowandfastadvance at the same speed (one step at a time); the node where they meet again is the cycle entrance, which is the duplicate number.
Mathematical proof (brief)
Let the distance from the starting point of the linked list to the cycle entrance be a, the distance from the cycle entrance to the first meeting point be b, and the cycle length be L; then the remaining distance from the meeting point to the cycle entrance is c = L - b.
At the first meeting, slow has walked a + b steps, and fast has walked a + b + k·L steps (having gone around k extra loops). Since fast’s speed is twice slow’s:
2(a + b) = a + b + k·L a + b = k·L a = (k-1)·L + (L - b) = (k-1)·L + cIn other words: the distance from the starting point to the cycle entrance after walking a steps equals the distance from the meeting point to the cycle entrance after walking c steps (possibly with a few extra loops). Therefore, letting slow start from the starting point and fast start from the meeting point and advance at the same speed, they will definitely meet at the cycle entrance.
For the complete derivation details, refer to the article “Detecting a cycle in a singly linked list and finding the cycle start” at the end of this post.
Step-by-step illustration
Take nums = [1,3,4,2,2] as an example; the linked list structure is 0 → 1 → 3 → 2 ⇄ 4:
Index: 0 1 2 3 4Value: 1 3 4 2 2
Linked list: 0 → 1 → 3 → 2 ⇄ 4 (2 and 4 form a cycle, the cycle entrance is 2)
Phase 1 (find meeting point):slow=0, fast=0 slow=nums[0]=1, fast=nums[nums[0]]=nums[1]=3 slow=nums[1]=3, fast=nums[nums[3]]=nums[2]=4 slow=nums[3]=2, fast=nums[nums[4]]=nums[2]=4 slow=nums[2]=4, fast=nums[nums[4]]=nums[2]=4 ← meet at 4
Phase 2 (find cycle entrance):reset slow to 0, keep fast at 4 slow=nums[0]=1, fast=nums[4]=2 slow=nums[1]=3, fast=nums[2]=4 slow=nums[3]=2, fast=nums[4]=2 ← meet at 2return 2 ✔Complexity
| Dimension | Analysis |
|---|---|
| Time complexity | O(n) — each of the two phases performs a linear scan |
| Space complexity | O(1) — only two pointers are used |
| Whether the array is modified | No — read-only throughout |
Code Implementation
#include <stdio.h>
/* Approach 1: Binary search on the value range */int findDuplicateBinarySearch(int *nums, int numsSize) { int left = 1, right = numsSize - 1; /* The value range is [1, n], n = numsSize - 1 */
while (left < right) { int mid = left + (right - left) / 2; int cnt = 0; for (int i = 0; i < numsSize; i++) { if (nums[i] <= mid) cnt++; } if (cnt > mid) { right = mid; /* The duplicate is in [left, mid] */ } else { left = mid + 1; /* The duplicate is in [mid+1, right] */ } } return left;}
/* Approach 2: Fast and slow pointers (Floyd's cycle detection) */int findDuplicateFloyd(int *nums, int numsSize) { (void)numsSize; int slow = 0, fast = 0;
/* Phase 1: Find the meeting point inside the cycle */ do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow != fast);
/* Phase 2: Find the cycle entrance (i.e., the duplicate) */ slow = 0; while (slow != fast) { slow = nums[slow]; fast = nums[fast]; } return slow;}#include <vector>using namespace std;
class Solution {public: /* Approach 1: Binary search on the value range */ int findDuplicateBinarySearch(vector<int>& nums) { int n = nums.size() - 1; int left = 1, right = n;
while (left < right) { int mid = left + (right - left) / 2; int cnt = 0; for (int x : nums) { if (x <= mid) cnt++; } if (cnt > mid) { right = mid; // The duplicate is in [left, mid] } else { left = mid + 1; // The duplicate is in [mid+1, right] } } return left; }
/* Approach 2: Fast and slow pointers (Floyd's cycle detection) */ int findDuplicateFloyd(vector<int>& nums) { int slow = 0, fast = 0;
// Phase 1: Find the meeting point inside the cycle do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow != fast);
// Phase 2: Find the cycle entrance (i.e., the duplicate) slow = 0; while (slow != fast) { slow = nums[slow]; fast = nums[fast]; } return slow; }};/* Approach 1: Binary search on the value range */function findDuplicateBinarySearch(nums) { let left = 1; let right = nums.length - 1;
while (left < right) { const mid = left + Math.floor((right - left) / 2); let cnt = 0; for (const x of nums) { if (x <= mid) cnt++; } if (cnt > mid) { right = mid; // The duplicate is in [left, mid] } else { left = mid + 1; // The duplicate is in [mid+1, right] } } return left;}
/* Approach 2: Fast and slow pointers (Floyd's cycle detection) */function findDuplicateFloyd(nums) { let slow = 0; let fast = 0;
// Phase 1: Find the meeting point inside the cycle do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow !== fast);
// Phase 2: Find the cycle entrance (i.e., the duplicate) slow = 0; while (slow !== fast) { slow = nums[slow]; fast = nums[fast]; } return slow;}Solution Comparison
| Solution | Time Complexity | Space Complexity | Modifies Array | Core Idea |
|---|---|---|---|---|
| Binary search on value range | O(n log n) | O(1) | No | Binary search on value range + pigeonhole principle counting |
| Fast and slow pointers | O(n) | O(1) | No | Model the array as a linked list + Floyd’s cycle detection |
- Binary search is more general: it applies as long as there exists a property of “counting by threshold satisfies monotonicity,” and does not depend on the special value range of
[1, n]; the cost is being one notch slower (an extra log n). - Fast and slow pointers is the linear optimal solution, but it is highly technique-dependent: it relies on the precise structure of “value range
[1, n], lengthn + 1” to perfectly model the array as a linked list with a cycle.
Key Points
- The pigeonhole principle is the foundation of correctness: the binary search solution’s “
cnt > midmeans the duplicate is on the left side” is precisely another formulation of the pigeonhole principle. - Binary search does not necessarily mean binary searching indices: this problem demonstrates the important variant of “binary searching on the value range,” turning “finding a number” into “finding the first position that satisfies
cnt > mid.” - Treating the array as a linked list is a stroke of genius: the modeling of
i → nums[i]brings “finding the duplicate number” back to the familiar problem of “finding the cycle entrance.” - Both naturally satisfy the constraints: the entire process only reads the array and uses only O(1) space, with no need for sorting, hash tables, or marking.
Extended Thoughts
- If modifying the array is allowed, there is a more intuitive approach: during traversal, negate the value at the position corresponding to
nums[i]as a “mark,” and when a negative value is encountered again, the duplicate is found (negation marking method, O(n) time). - If O(n) extra space is allowed, simply use a hash table to count occurrences, which is the most straightforward baseline for comparison.
- The linked list modeling in this problem relies on “the value range being exactly
[1, n].” If the value range is broken (for example, containing 0), can it still be modeled as a linked list? Hint: index 0 needs a property of “never being pointed to.”
Related Articles
- Detecting a Cycle in a Singly Linked List and Finding the Cycle Entrance — the standard version of Floyd’s cycle detection on linked lists, with complete mathematical derivation
- Binary Search: From Beginner to Mastery of Boundary Handling — the fundamentals of binary search on value ranges and boundary handling
- Array Two Pointers — an overview of two-pointer patterns such as fast and slow pointers