Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Treat the array as a linked list where index i points to nums[i]. Because values are in [1, n] and there are n+1 of them, a duplicate creates a cycle. Run Floyd's tortoise-and-hare to find the cycle, then the cycle entrance is the duplicate — O(n) time, O(1) space, and the array is left unmodified.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can reframe a bounded-value array as a linked list and apply Floyd's two-phase cycle detection to hit O(1) space without mutating the input.
Where candidates slip
Reaching for a hash set or sort (violating the no-extra-space / no-modify constraints), or running only phase 1 and returning the meeting point instead of the cycle entrance.
How to stand out
Explain why a cycle must exist (pigeonhole on values in [1, n]), then derive why phase 2 lands exactly on the entrance — the algebra of the distances, not hand-waving.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewFind the duplicateExtra space allowed?YesNoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Hash Set (Baseline)
1Hint 1Explore hintHide hint
What's the simplest way to detect the first value you've already seen?
2Hint 2Explore hintHide hint
Walk the array, adding each value to a set; the first value already present is the duplicate.
3Hint 3Explore hintHide hint
This is O(n) time but O(n) space — and the problem forbids extra space. How could you detect a repeat without storing seen values?
A · Core idea
Scan the array, inserting each value into a hash set. The moment you try to insert a value that's already there, that value is the duplicate.
Why it's the natural first answer. 'Find the repeated element' screams 'remember what you've seen', and a hash set gives O(1) membership. It's correct, simple, and a good baseline to state.
Why it fails the constraints. The problem explicitly caps extra space at O(1), and the set grows to O(n). So while the time is optimal, the space disqualifies it. Mentioning it shows you know the easy route and understand precisely which rule rules it out — a better signal than pretending the constraint isn't there.
The bridge to the real answer. To drop the space to O(1) without mutating the array, you need structure already present in the data. The clue is the value range [1, n]: every element is itself a valid index, which means the array can be read as a linked list — and detecting a repeat becomes detecting a cycle, which Floyd's does in O(1) space.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
nums = [1, 3, 4, 2, 2]Hash-map state
insert 2 (first)
2Continue scanning
2 not present → add
2C · Code with teaching comments
Reference implementation previewpublic int findDuplicate(int[] nums) { Set<Integer> seen = new HashSet<>(); for (int x : nums) { if (!seen.add(x)) { // add returns false if already present return x;Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The hash set trades space for simplicity. When an interviewer forbids that space, look for exploitable structure in the input — here, values being valid indices is the door to the pointer-chasing solution.
ComplexityO(n) time, O(n) space — One pass with O(1) average set operations → O(n) time. The set can hold up to n values → O(n) space, which the constraints disallow.
E · Edge cases and pitfalls3Using O(n) space when the problem forbids it.Explore edge casesHide edge cases
Pitfalls
- Using O(n) space when the problem forbids it.
- Returning the index instead of the value.
- Assuming it generalizes when values aren't in [1, n] — it does, but the optimal doesn't.
Approach 2 of 2 · Optimal
Floyd's Cycle Detection on the Array (Optimal)
1Hint 1Explore hintHide hint
Read nums[i] as 'the next index to jump to from i'. Since every value is in [1, n], that's always a valid jump. What shape does following the jumps trace out?
2Hint 2Explore hintHide hint
Because two indices share the same value (the duplicate), two arrows point into the same node — that creates a cycle. Finding the duplicate becomes finding the cycle's entrance.
3Hint 3Explore hintHide hint
Phase 1: tortoise (one step) and hare (two steps) until they meet inside the cycle. Phase 2: reset one pointer to nums[0], advance both one step at a time; they meet at the cycle entrance, which is the duplicate.
A · Core idea
Reinterpret the array as a function f(i) = nums[i], i.e. a linked list where node i links to node nums[i]. Then run Floyd's two-phase cycle detection; the cycle's entrance node is the duplicated value.
Why a cycle must exist. There are n + 1 slots but values only range over {1, …, n}, so by the pigeonhole principle some value repeats. In the linked-list view, a repeated value means two different nodes both link to the same node — two arrows converging. Following links forward from index 0 can therefore never escape to infinity; it must eventually revisit a node, i.e. loop. The starting index 0 is not part of the cycle (nothing points to it as a target since values are ≥ 1 and we begin there), so it sits on the 'tail' leading into the loop — exactly the linked-list-with-a-cycle shape Floyd's was designed for.
Phase 1 — find a meeting point. Move slow one step (slow = nums[slow]) and fast two steps (fast = nums[nums[fast]]). Inside a cycle the fast pointer gains one position on the slow pointer each iteration, so it eventually laps and lands on it. When slow == fast, you're guaranteed to be somewhere inside the cycle — but not necessarily at its entrance.
Phase 2 — why resetting finds the entrance. Let a be the distance from the start to the cycle entrance, b the distance from the entrance to the meeting point, and c the rest of the cycle (so the cycle length is b + c). When they meet, slow has walked a + b and fast has walked a + b + k(b + c) for some integer k; since fast moved twice as far, 2(a + b) = a + b + k(b + c), which simplifies to a = (k−1)(b+c) + c. In words: the distance from the start to the entrance equals the distance from the meeting point to the entrance (modulo whole loops). So if you put one pointer back at nums[0] and advance both one step at a time, they collide exactly at the entrance — the duplicate.
Why this beats the alternatives. It's O(n) time like the hash set but O(1) space, and unlike sorting it never touches the array. That precise combination — fast, constant space, read-only — is why interviewers pose this problem: it filters for candidates who can see the hidden linked-list structure rather than reach for the default tools.
Why duplicates-more-than-twice are fine. Extra occurrences just add more arrows into the cycle entrance; the entrance node is unchanged, so the algorithm still returns it.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
nums = [1, 3, 4, 2, 2]Interactive diagram
Array viewed as a linked list with a cycle
phase 1 startIndices link via nums[i]; two arrows into node 2 form the cycle, whose entrance (2) is the duplicate.
2C · Code with teaching comments
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
The entire trick is the reframe: a bounded-value array is a linked list, a duplicate is a cycle, and the duplicate value is the cycle entrance. Floyd's then gives O(1) space for free. The phase-2 reset working is a clean distance-algebra result, not magic — be ready to derive a = c (mod cycle).
ComplexityO(n) time, O(1) space — Phase 1 and phase 2 each traverse O(n) nodes before meeting (the pointers cover at most a constant multiple of the list length). No auxiliary storage beyond two index variables → O(1) space.
E · Edge cases and pitfalls7nums = [1, 1] — n=1, two slots both 1. Cycle is immediate; phase 2 returns 1.Explore edge casesHide edge cases
Edge cases handled
nums = [1, 1]n=1, two slots both 1. Cycle is immediate; phase 2 returns 1.nums = [2, 2, 2, 2, 2]Duplicate appears 5 times; the cycle entrance is still 2.nums = [3, 1, 3, 4, 2]Entrance node is 3; algorithm returns 3 without modifying the array.
Pitfalls
- Returning the phase-1 meeting point instead of running phase 2 to the entrance.
- Mis-stepping the hare: it must be nums[nums[fast]], a true double step.
- Assuming index 0 can be inside the cycle — it's on the tail because values are ≥ 1.
- Falling back to a set or sort and violating the stated constraints.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int findDuplicate(int[] nums) { // Phase 1: find a meeting point inside the cycle int slow = nums[0]; int fast = nums[nums[0]]; while (slow != fast) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Stopping at the meeting point
Phase 1 only guarantees a node inside the cycle, not the entrance. You must run phase 2 (reset + equal-speed walk) to reach the duplicate.
Explore the code comparisonHide the code comparison
Stopping at the meeting point
Phase 1 only guarantees a node inside the cycle, not the entrance. You must run phase 2 (reset + equal-speed walk) to reach the duplicate.
Don't write this
while (slow != fast) { slow = nums[slow]; fast = nums[nums[fast]]; }
return slow; // meeting point, NOT the entranceWrite this instead
while (slow != fast) { slow = nums[slow]; fast = nums[nums[fast]]; }
slow = nums[0];
while (slow != fast) { slow = nums[slow]; fast = nums[fast]; }
return slow;2Fast pointer not double-stepping
The hare must advance twice per iteration (nums[nums[fast]]). Single-stepping makes both pointers identical and they never meaningfully meet inside the cycle.
Explore the code comparisonHide the code comparison
Fast pointer not double-stepping
The hare must advance twice per iteration (nums[nums[fast]]). Single-stepping makes both pointers identical and they never meaningfully meet inside the cycle.
Don't write this
while slow != fast:
slow = nums[slow]
fast = nums[fast] # only one step — no relative speedWrite this instead
while slow != fast:
slow = nums[slow]
fast = nums[nums[fast]]3Violating the constraints with a set
It's correct but uses O(n) space, which the problem forbids. Under the stated rules, Floyd's is the expected answer.
Explore the code comparisonHide the code comparison
Violating the constraints with a set
It's correct but uses O(n) space, which the problem forbids. Under the stated rules, Floyd's is the expected answer.
Don't write this
seen = set()
for x in nums:
if x in seen:
return x
seen.add(x) # O(n) space — disallowedWrite this instead
slow = fast = nums[0]
# ... Floyd's two-phase cycle detection in O(1) spaceRevise
Carry the pattern into the interview
Pattern to remember
Array as implicit linked list + Floyd's cycle detection (tortoise & hare)
Code pattern to memorize
slow = fast = nums[0]
repeat: slow = nums[slow]; fast = nums[nums[fast]] until slow == fast
slow2 = nums[0]
while slow2 != slow: slow = nums[slow]; slow2 = nums[slow2]
return slowRules to remember
- 1The mapping
i → nums[i]is a valid linked list because every value is in[1, n], so it's always a legal index (never points out of bounds, never to index 0 from itself in a way that breaks the start). - 2Start both pointers at
nums[0](or conceptually at index 0 then take one step). The duplicate is the entrance of the cycle, not just any node in it. - 3Phase 1 finds a meeting point inside the cycle; Phase 2 resets one pointer to the start and walks both at speed 1 — they meet at the entrance. This is Floyd's, not random.
- 4This does not modify the array and uses O(1) space — that's the whole reason it beats the sort and the seen-set.
- 5It finds the duplicate value, not its index, and works even if the duplicate appears more than twice.
Use this pattern when
- Values are confined to [1, n] with n+1 elements → a cycle is guaranteed
- You're forbidden from modifying the array AND must use O(1) extra space
- You recognize 'each element points to another index' as a linked-list traversal
- Exactly one duplicated value (possibly repeated) is promised
Choose another pattern when
- You're allowed O(n) extra space → a hash set is simpler and just as fast
- You're allowed to sort / mutate → sort then scan adjacent, or index-marking, is easier to code
- Values aren't bounded to [1, n] → the linked-list framing breaks; use hashing
Interview takeaway
The leap is seeing an array of bounded values as a linked list whose cycle entrance is the duplicate. Once you reframe it that way, Floyd's algorithm delivers O(n) time and O(1) space without touching the input — the exact constraint combo interviewers use to rule out the easy answers.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternFloyd's tortoise-and-hare applied to an array reinterpreted as a linked list. The transferable insight: when values are bounded to valid indices, i → nums[i] is a traversal, duplicates become cycles, and constant-space cycle detection replaces hashing. The phase-2 reset relies on the distance identity a ≡ c (mod cycle length).
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Linked List Cycle IIThe same Floyd's two-phase algorithm on an actual linked list — find where the cycle begins.
- Contains DuplicateWhen extra space is allowed, a set answers 'is there any duplicate' in O(n) time.
- Missing NumberSame [0..n] value structure; XOR or sum identities pinpoint the missing element in O(1) space.