Combining this answer with ideas from this thread, notably this answer to create an efficient but readable solution, you may use
static boolean unorderedEquals(Collection<?> coll1, Collection<?> coll2) {
if(coll1.size() != coll2.size()) return false;
Map<Object, Integer> freq = new HashMap<>();
for(Object o: coll1) freq.merge(o, 1, Integer::sum);
for(Object o: coll2)
if(freq.merge(o, -1, Integer::sum) < 0) return false;
return true;
}
The first loop creates a frequency map like in the linked answer, but instead of building a second map, to perform an expensive comparison, the second loop decreases the counts on each occurrence, returning immediately, if a count became negative. The merge
method smoothly handles the case of absent keys.
Since it has been checked right at the beginning of the method that both lists have the same size, after increasing and decreasing, the total count must be zero. Since we have proven that there are no negative numbers, as we returned immediately for them, there can’t be positive non-zero values either. So we can return true
after the second loop without further checks.
Supporting arbitrary Iterable
s, which differ from Collection
in not necessarily having a size()
method, is a bit trickier, as we can’t do the pre-check then and hence, have to maintain the count:
static boolean unorderedEquals(Iterable<?> iter1, Iterable<?> iter2) {
Map<Object, Integer> freq = new HashMap<>();
int size = 0;
for(Object o: iter1) {
freq.merge(o, 1, Integer::sum);
size++;
}
for(Object o: iter2)
if(--size < 0 || freq.merge(o, -1, Integer::sum) < 0) return false;
return size == 0;
}
If we want avoid the boxing overhead, we have to resort to a mutable value for the map, e.g.
static boolean unorderedEquals(Collection<?> coll1, Collection<?> coll2) {
if(coll1.size() != coll2.size()) return false;
Map<Object, int[]> freq = new HashMap<>();
for(Object o: coll1) freq.computeIfAbsent(o, x -> new int[1])[0]++;
int[] absent = { 0 };
for(Object o: coll2) if(freq.getOrDefault(o, absent)[0]-- == 0) return false;
return true;
}
But I don’t think that his will pay off. For small numbers of occurrences, boxing will reuse the Integer
instances whereas we need a distinct int[]
object for each distinct element when using mutable values.
But using compute
might be interesting for the Iterable
solution, when using it like
static boolean unorderedEquals(Iterable<?> coll1, Iterable<?> coll2) {
Map<Object, int[]> freq = new HashMap<>();
for(Object o: coll1) freq.computeIfAbsent(o, x -> new int[1])[0]++;
int[] absent = {};
for(Object o: coll2)
if(freq.compute(o, (key,c) -> c == null || c[0] == 0? absent:
--c[0] == 0? null: c) == absent) return false;
return freq.isEmpty();
}
which removes entries from the map when their count reaches zero, so we only have to check the map for emptiness at the end.