Find every value that appears twice
You're given an array nums of length n where every value lies between 1 and
n and each value shows up once or twice. I'd like you to return every value that shows up twice, in
increasing order.
Your solution should run in O(n) time and use only constant extra space apart from the list you return. You're allowed to change the array while you work.
Examples
| Input | Output | Why |
|---|---|---|
nums = [4, 3, 2, 7, 8, 2, 3, 1] | [2, 3] | Both 2 and 3 turn up twice and the other values only once. |
nums = [1, 1, 2] | [1] | There's a single repeat, which is the 1. |
nums = [1] | [] | A single value can't be repeated, so the list is empty. |
Constraints
- 1 ≤ n ≤ 10⁵
- 1 ≤ nums[i] ≤ n
- each value appears once or twice
Hints
Hint 1
Ask yourself what a hash set would be doing for you here, because all it records is one yes or no for each value from 1 to n.
Hint 2
You already own n slots and there are n values you might have to remember, so slot v − 1 can hold the yes or no for value v.
Hint 3
Every value in the array is positive, so you can flip the sign of nums[v − 1] the first time you meet v and still read the value back with an absolute value later on.
Where people lose marks
- Reading
nums[i]where you meant|nums[i]|. Once an earlier step has flipped that slot the value is negative, the indexv − 1goes below zero, and depending on the language you either crash or read garbage. - Checking whether slot
v − 1is negative after you've already flipped it. Then every value looks like a duplicate the first time you meet it, so do the check first and the flip second. - Leaving the array negated when the caller still needs it. If that matters, finish with one more pass that takes the absolute value of every slot.
Dry run on [4,3,2,7,8,2,3,1]
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 / 10nums4031227384253617found[]
There are 8 values and all of them lie between 1 and 8, so slot v − 1 will hold the mark for value v, and a negative sign there means we've met v already.
-
Step 2 / 10nums40i3122-7384253617v4found[]
Slot 0 holds 4, slot 3 was positive, so this is the first 4 we've met and we flip slot 3 to -7.
-
Step 3 / 10nums4031i-22-7384253617v3found[]
Slot 1 holds 3, slot 2 was positive, so this is the first 3 we've met and we flip slot 2 to -2.
-
Step 4 / 10nums40-31-22i-7384253617v2found[]
Slot 2 holds 2, slot 1 was positive, so this is the first 2 we've met and we flip slot 1 to -3.
-
Step 5 / 10nums40-31-22-73i8425-3617v7found[]
Slot 3 holds 7, slot 6 was positive, so this is the first 7 we've met and we flip slot 6 to -3.
-
Step 6 / 10nums40-31-22-7384i25-36-17v8found[]
Slot 4 holds 8, slot 7 was positive, so this is the first 8 we've met and we flip slot 7 to -1.
-
Step 7 / 10nums40-31-22-738425i-36-17v2found[2]
Slot 5 holds 2 and slot 1 is already negative, so this is the second 2 and it goes on the list.
-
Step 8 / 10nums40-31-22-738425-36i-17v3found[2, 3]
Slot 6 holds 3 and slot 2 is already negative, so this is the second 3 and it goes on the list.
-
Step 9 / 10nums-40-31-22-738425-36-17iv1found[2, 3]
Slot 7 holds 1, slot 0 was positive, so this is the first 1 we've met and we flip slot 0 to -4.
-
Step 10 / 10nums-40-31-22-738425-36-17answer[2, 3]
After one pass the values seen twice are [2, 3], and the only extra memory was the answer list.
How to think about it
You'll usually find this one called Find All Duplicates in an Array. Without the space limit
you'd keep a set of the values you've met and report any value that's already in it, and that's all the set is
doing: storing one bit for each value between 1 and n.
The array has exactly n slots, so I'd store that bit inside it. All the values start positive,
which leaves the sign free to carry information. As you walk along you read v = |nums[i]| (the
absolute value, since an earlier step may already have flipped this slot) and look at slot
v − 1. If it's still positive you're meeting v for the first time, so you make it
negative. If it's already negative then you've met v before and it goes on the answer list.
Each value appears at most twice, so it gets reported at most once and the list has no repeats. Because the slots are marked in the order the array is read, the list comes out in the order of second sightings, which is why the reference solutions sort it at the end. That sort costs O(d log d) for d duplicates, and if you'd like to stay strictly linear you can make a second pass over the slots and collect the indices instead.
Complexity. Time O(n) · Space O(1) besides the output
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 all_duplicates(nums):
out = []
for x in nums:
j = abs(x) - 1 # an earlier step may have flipped this slot
if nums[j] < 0:
out.append(abs(x)) # second time we've met this value
else:
nums[j] = -nums[j] # first time: mark it
return sorted(out)#include <algorithm>
#include <cstdlib>
#include <vector>
using namespace std;
vector<int> solve(vector<int> nums) {
vector<int> out;
for (size_t i = 0; i < nums.size(); i++) {
int j = abs(nums[i]) - 1; // an earlier step may have flipped this slot
if (nums[j] < 0) out.push_back(j + 1);
else nums[j] = -nums[j]; // first time: mark it
}
sort(out.begin(), out.end());
return out;
}import java.util.*;
class Solution {
public int[] allDuplicates(int[] nums) {
List<Integer> out = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
int j = Math.abs(nums[i]) - 1; // an earlier step may have flipped this slot
if (nums[j] < 0) out.add(j + 1);
else nums[j] = -nums[j]; // first time: mark it
}
int[] r = new int[out.size()];
for (int i = 0; i < r.length; i++) r[i] = out.get(i);
Arrays.sort(r);
return r;
}
}function allDuplicates(nums) {
const out = [];
for (let i = 0; i < nums.length; i++) {
const j = Math.abs(nums[i]) - 1; // an earlier step may have flipped this slot
if (nums[j] < 0) out.push(j + 1); // second time we've met this value
else nums[j] = -nums[j]; // first time: mark it
}
return out.sort((a, b) => a - b);
}fn solve(nums: &[i32]) -> Vec<i32> {
let mut a = nums.to_vec();
let mut out = Vec::new();
for i in 0..a.len() {
let j = (a[i].abs() - 1) as usize; // an earlier step may have flipped this slot
if a[j] < 0 {
out.push(j as i32 + 1);
} else {
a[j] = -a[j]; // first time: mark it
}
}
out.sort();
out
}import qualified Data.IntMap.Strict as M
-- Pure Haskell has no cheap in-place sign flip, so this counts with a map. The O(1)-space
-- version needs a mutable array in ST; the marking idea is the same.
solve :: [Int] -> [Int]
solve xs = [v | (v, c) <- M.toAscList counts, c == 2]
where
counts = M.fromListWith (+) [(x, 1 :: Int) | x <- xs]
Variants to try
- Find every value between 1 and n that's missing. (Same marking pass, then the slots that are still positive give the missing values.)
- Values can now appear any number of times. (Mark by adding n to slot v − 1 so that the count can be read back as nums[v − 1] div n.)
- Exactly one value is repeated and you mustn't change the array. (Floyd's cycle detection on i → nums[i] finds it in O(n) time and O(1) space.)
Next problems
Smallest positive integer that is missing
A hash set solves it in one line and is not allowed. The array can be its own hash table, once you see that only n + 1 answers are possible.
Open the problem → Easy15 minFind every number from 1 to n that is missing
It's the same sign marking you'd use to find duplicates, only this time you read the answer from the slots that never got marked.
Open the problem → Easy10 minAre two words anagrams?
Two words are anagrams when every letter occurs equally often in both. One table of counts, added for one word and taken away for the other.
Open the problem →Write a solution and run it against the real test table.
Keep going
Smallest positive integer that is missing
A hash set solves it in one line and is not allowed. The array can be its own hash table, once you see that only n + 1 answers are possible.
Open the problem → Easy15 minFind every number from 1 to n that is missing
It's the same sign marking you'd use to find duplicates, only this time you read the answer from the slots that never got marked.
Open the problem → Easy10 minAre two words anagrams?
Two words are anagrams when every letter occurs equally often in both. One table of counts, added for one word and taken away for the other.
Open the problem → Arrays and hashingThe pattern behind it
Trade memory for time: a hash map answers "have I seen this?" in O(1), and most array problems reduce to asking it the right question.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.