Milliseconds for a million elements means nanoseconds per elements. One nanosecond may be half a dozen instructions. That’s difficult. Let’s set a goal of few (less than ten) milliseconds.
For a generic algorithm that handles all kind of data this is tough. But let’s say your million numbers were picked at random from 0 to M-1, with M = ten million or 50 million, “at random” means some numbers but not many turn up repeatedly.
We create a bitset with M elements, then process the numbers in sequence. If a number X is not in the bit set, then we set bit X. With M = 10 million that’s what happens in 90% of all cases. If X is already set, then we do two things: We add X to a hash set if not present yet, and either initialise a counter to 2 if X wasn’t present, or increase the counter. We also maintain an array that tells us how often each count >= 2 happened.
This is all more time consuming than just checking and setting one bit, but is much more rare, so instead of nanoseconds we can spend tens of nanoseconds.
After processing all numbers, we determine how many numbers with “largest count” we want and extract them.
If we have more than say 200 million numbers, we set a bit for x % 100,000,000 do only one in 100 would get a count of 2. If M is smaller, the number of different X occurring twice or more often will still be much less than a million.
So the idea is to remove numbers occurring 0 or 1 times very quickly, and hoping the number of duplicates is much less (it’s less than 500,000 in the worst case, but likely less).
unordered_map
takes0.06
secs for $m=10^6$ and0.15
secs for $m=10^9$. whiledense_hash_map
from github.com/sparsehash/sparsehash takes takes0.03
secs for $m=10^6$ and0.04
secs for $m=10^9$. $\endgroup$