The repeated number, without changing the array
You're given an array nums of n + 1 integers, each from 1 to n. Exactly one value turns up
more than once (it might turn up several times). Return that value without changing the array and using only
O(1) extra space.
Examples
| Input | Output | Why |
|---|---|---|
nums = [1, 3, 4, 2, 2] | 2 | |
nums = [3, 1, 3, 4, 2] | 3 | |
nums = [3, 3, 3, 3, 3] | 3 | I'd point out that the repeated value can take up most of the array (n is 4 here so 1, 2 and 4 never turn up at all). |
Constraints
- 1 ≤ n ≤ 10⁵
- nums.length = n + 1
- 1 ≤ nums[i] ≤ n
- exactly one value repeats
Hints
Hint 1
A set would work but it's O(n) space, and sorting or marking with signs changes the array, so I'd think of the array as something else. What if index i had an arrow going to index nums[i]?
Hint 2
If you start at index 0 (which nothing points to since the values run from 1 to n) and keep following arrows you'll end up going round a cycle, and the value that two arrows share is where that cycle starts.
Hint 3
That's Floyd's method, where a slow pointer takes one step and a fast one takes two until they meet in the cycle. Then you'd put one of them back at 0 and move both one step at a time, and wherever they meet now is the start.
Where people lose marks
- Starting at index 1, when it's index 0 that nothing points to and that's why it's the only safe place to start. From anywhere else you might already be inside the cycle and you'd miss the way in.
- Returning the place where they met in the first phase, which is somewhere on the cycle and is only the start of it if you're lucky.
- Starting slow and fast both at 0 with a plain while loop, so that they're equal before anything's moved and the loop never runs at all. I'd take the first steps before the loop (as here) or use a do-while.
Dry run on [1,3,4,2,2]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 8nums1031slow4223fast24slow1fast3
Index i points to nums[i]. From 0 the path is 0 → 1 → 3 → 2 and so on. slow takes one step and fast takes two.
-
Step 2 / 8nums10314223slow24fastslow3fast4
slow moves to 3 and fast jumps two arrows to 4.
-
Step 3 / 8nums103142slow2324fastslow2fast4
slow moves to 2 and fast jumps two arrows to 4.
-
Step 4 / 8nums1031422324slow/fastslow4fast4
They've met at index 4, which is somewhere inside the cycle.
-
Step 5 / 8nums10a31422324slowa0slow4
Put one pointer back at 0 and leave the other at the meeting point. Now both take one step at a time.
-
Step 6 / 8nums1031a42slow2324a1slow2
a moves to 1 and slow moves to 2.
-
Step 7 / 8nums10314223a24slowa3slow4
a moves to 3 and slow moves to 4.
-
Step 8 / 8nums103142a/slow2324a2slow2
They meet at 2. That's the start of the cycle and the repeated value.
How to think about it
The trick I'd use is to stop seeing the array as a list of numbers and draw an arrow from each index
i to index nums[i] instead. There are n + 1 indices and every arrow points somewhere in
1..n, so if you start at 0 and keep following arrows you're bound to end up going round a cycle. The repeated value
d has at least two arrows coming into it (one from inside the cycle and one from the path that led you
in) and that makes d exactly the place where the cycle starts.
So now you're looking for the start of a cycle, and Floyd's method does that with two pointers and no extra memory at all. The slow one moves along one arrow at a time and the fast one moves along two, and once they're both in the cycle the fast one gains a step every time so they've got to meet. Say the path into the cycle is a steps long and they meet b steps past its start. The fast one has gone twice as far as the slow one and the extra is a whole number of laps, so a + b is a multiple of the cycle's length, which means that walking a more steps from where they met lands you on the start. You don't know a but you don't need it, because a pointer that sets off from 0 gets there in a steps as well. I'd put one pointer back at 0, move both one step at a time, and return the place they meet.
Complexity. Time O(n) · Space O(1)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def find_duplicate(nums):
slow, fast = nums[0], nums[nums[0]]
while slow != fast: # phase 1: meet somewhere in the cycle
slow = nums[slow]
fast = nums[nums[fast]]
a = 0
while a != slow: # phase 2: walk in to the start of the cycle
a = nums[a]
slow = nums[slow]
return a#include <vector>
int solve(const std::vector<int>& nums) {
int slow = nums[0], fast = nums[nums[0]];
while (slow != fast) { slow = nums[slow]; fast = nums[nums[fast]]; } // phase 1: meet in the cycle
int a = 0;
while (a != slow) { a = nums[a]; slow = nums[slow]; } // phase 2: walk in to its start
return a;
}class Solution {
public int findDuplicate(int[] nums) {
int slow = nums[0], fast = nums[nums[0]];
while (slow != fast) { slow = nums[slow]; fast = nums[nums[fast]]; } // phase 1: meet in the cycle
int a = 0;
while (a != slow) { a = nums[a]; slow = nums[slow]; } // phase 2: walk in to its start
return a;
}
}function findDuplicate(nums) {
let slow = nums[0], fast = nums[nums[0]];
while (slow !== fast) { slow = nums[slow]; fast = nums[nums[fast]]; } // phase 1: meet in the cycle
let a = 0;
while (a !== slow) { a = nums[a]; slow = nums[slow]; } // phase 2: walk in to its start
return a;
}fn solve(nums: &[i64]) -> i64 {
let at = |i: i64| nums[i as usize];
let (mut slow, mut fast) = (at(0), at(at(0)));
while slow != fast { slow = at(slow); fast = at(at(fast)); } // phase 1: meet in the cycle
let mut a = 0;
while a != slow { a = at(a); slow = at(slow); } // phase 2: walk in to its start
a
}import Data.Array
solve :: [Int] -> Int
solve nums = walk 0 meet
where
arr = listArray (0, length nums - 1) nums
f i = arr ! i
-- phase 1: slow takes one step, fast two, until they meet in the cycle
meet = go (f 0) (f (f 0))
go s q | s == q = s
| otherwise = go (f s) (f (f q))
-- phase 2: one from 0, one from the meeting point, one step each
walk a s | a == s = a
| otherwise = walk (f a) (f s)
Variants to try
- Binary search on the value works too in O(n log n): count how many values are at most m. If that's more than m, the duplicate is at most m.
- If you're allowed to change the array, cyclic sort or sign marking does it in O(n) with less to explain.
Next problems
Does the digit-square sequence reach 1
Replace n by the sum of the squares of its digits, again and again. Either you hit 1 or you go round a loop, and a fast and a slow pointer tell you which without storing anything.
Open the problem → Medium15 minWhere a linked list loops back on itself
A list starts at node 0 and might end or might loop. Floyd's two phases tell you which, and where the loop starts, in O(1) space.
Open the problem → Easy10 minMove the zeros to the end
One pointer reads every element, the other marks where the next non-zero belongs. The order of the non-zero values is kept.
Open the problem →Write a solution and run it against the real test table.
Keep going
Does the digit-square sequence reach 1
Replace n by the sum of the squares of its digits, again and again. Either you hit 1 or you go round a loop, and a fast and a slow pointer tell you which without storing anything.
Open the problem → Medium15 minWhere a linked list loops back on itself
A list starts at node 0 and might end or might loop. Floyd's two phases tell you which, and where the loop starts, in O(1) space.
Open the problem → Easy10 minMove the zeros to the end
One pointer reads every element, the other marks where the next non-zero belongs. The order of the non-zero values is kept.
Open the problem → Two pointersThe pattern behind it
Two indices walking a sorted or paired structure, each step ruling out one candidate for good.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.