You need to iterate over the integers and keep track of integers you've already seen. For this you need an efficient data structure which have good runtime complexity for add
and contains
operations.
For instance you can use a has set to track seen integers:
Set<Integer> duplicateIntegers = new LinkedHashSet<>();
Set<Integer> seenIntegers = new HashSet<>();
for (Integer integer : integers) {
if (!seenIntegers.add(integer)){
duplicateIntegers.add(integer);
}
}
Here we iterate over N
integers, add it to seenIntegers
and check if current integer is already there, which is amortized O(1)
. So at the end it is O(N)
on time and O(N)
on additional space.
However O(1)
for HashSet.add
is amortised (see here what it actually means). Since we deal with integers and they are not so many, we can achieve a honest-to-goodness O(1)
using more additional space. We only need 2^32 bits which is just 512Mb. For this we can use BitSet
. Actually, two BitSet
s because we need 2^32 bits but BitSet
can only be initialized with max value of int which is 2^31-1.
BitSet seenNonNegativeIntegers = new BitSet(Integer.MAX_VALUE);
BitSet seenNegativeIntegers = new BitSet(Integer.MAX_VALUE);
Set<Integer> duplicateIntegers = new LinkedHashSet<>();
for (Integer integer : integers) {
int i = integer.intValue();
if (i >= 0) {
if (seenNonNegativeIntegers.get(i)) {
duplicateIntegers.add(integer);
}
seenNonNegativeIntegers.set(i);
} else if (i < 0) {
int index = -(i + 1);
if (seenNegativeIntegers.get(index)) {
duplicateIntegers.add(integer);
}
seenNegativeIntegers.set(index);
}
}
This is also O(N)
, but based on honest-not-amortised O(1)
. Theoretically this must be the most optimal solution from the runtime complexity point of view. Practically, however it might still be slower that HashSet
because we have to do get
and set
instead of just one add
.
On an interview I'd probably present the first solution, mentioning the second one and discussing runtime complexity vs. additional space requirements.