Reputation: 127
Let's say I've a HashMap
which contains key as a String
and value as a Set
of Integer
s (Map<String, Set<Integer>>
).
And say that the map is populated with following values:
Map<String, Set<Integer>> map = new HashMap<>();
map.put("w1", Set.of(1,3,4,6,7));
map.put("w2", Set.of(2,3,4,5,7));
map.put("w3", Set.of(1,2,3,5,7));
How can I find common set of values across all keys using Streams in Java? e.g.: In this case, the common set of values across all keys is Set.of(3,7)
.
Upvotes: 1
Views: 1389
Reputation: 21114
This can be solved using Stream.reduce
:
Set<Integer> commonElements = map.values().stream()
.reduce((s1, s2) -> s1.stream()
.filter(s2::contains)
.collect(Collectors.toUnmodifiableSet()))
.orElseThrow(NoSuchElementException::new);
Each step of the reduction will create a new Set
containing only the elements in common between the two sets.
One easy optimization that I haven't made here (in the name of simplicity for the example) would be to conditionally swap s1
and s2
such that the smaller set is being streamed over, since that occurs in linear time based on the size of the set, but each contains
check occurs in constant time.
Upvotes: 1
Reputation: 28978
That could be done by utilizing the collect()
operation.
The logic behind this approach is to get a random set from the collection of values, wrap it with a new set to avoid mutating existing data, and then combine all sets using retainlAll()
method one by one.
Even if values are represented by immutable sets (like in the example in the question) that will not be an issue because this approach guarantees that a map passed as an argument will be preserved intact. The only collection that will be mutated is a locally created set.
Stream-based implementation might look like:
public static <K, V> Set<V> getIntersection(Map<K, Set<V>> map) {
return map.values().stream()
.collect(
() -> getFirstSet(map),
Set::retainAll,
Set::retainAll
);
}
Method get getFirstSet()
is responsible for retrieving a random set from the collection of values. In the case of an empty map it'll return an empty set, otherwise, it'll yield a new set containing the elements of the first set in the stream.
public static <K, V> Set<V> getFirstSet(Map<K, Set<V>> map) {
return map.values().stream()
.map(HashSet::new) // create a new Set with the same contents to avoid modifying existing one
.findFirst()
.orElseGet(HashSet::new);
}
The same logic could be implemented iteratively:
public static <K, V> Set<V> getIntersection(Map<K, Set<V>> map) {
Set<V> intersection = getFirstSet(map);
for (Set<V> next: map.values()) {
intersection.retainAll(next);
}
return intersection;
}
Usage example:
public static void main(String[] args) {
Map<String, Set<Integer>> map =
Map.of("key1", Set.of(1,3,4,6,7),
"key2", Set.of(2,3,4,5,7),
"key3", Set.of(1,2,3,5,7));
System.out.println(getIntersectionStream(map));
System.out.println(getIntersectionLoop(map));
}
Output:
[3, 7]
[3, 7]
Upvotes: 3
Reputation: 828
In my opinion, this is the best solution:
map.values().stream().reduce((s1, s2) -> {
Set<Integer> s3 = new HashSet<>(s2);
s3.retainAll(s1);
return s3;
}).orElse(new HashSet<>());
It is a stream on map values reduced by intersecting the sets.
Upvotes: 2
Reputation: 40034
I would first recommend to not use streams and just do it simply as shown below. This does remove one element of the map. You could just do map.get("w1")
and do one redundant retainAll
.
Notice since you used Map.of
which creates an immutable set I had to make a copy to allow result
to be modified.
Set<Integer> result = new HashSet<>(map.remove("w1"));
for (Set<Integer> set : map.values()) {
result.retainAll(set);
}
Here is a stream solution.
To avoid having to use an initializer for a reduce operation I would use the reducing Collector
. This collector returns an Optional
so orElse
had to be used to retrieve the set and allow for an empty map.
Set<Integer> result = map.values().stream()
.collect(Collectors.reducing((a, b) -> {
a.retainAll(b);
return a;
}))..orElse(new HashSet<>());
System.out.println(result);
Both of the above would print
[3, 7]
The solutions shown presume a properly populated Map
. A full solution would include checks for
Upvotes: 0
Reputation: 15136
This simple version uses stream with set operations to populate intersections
by finding one of the members and retainAll
to match all the others:
Set<Integer> intersection = new HashSet<>();
map.values().stream().limit(1).forEach(intersection::addAll);
map.values().stream().forEach(intersection::retainAll);
Upvotes: 0
Reputation: 972
My approach is to first group the different values and count them. Then only keep those whose count is equal to the number of entries in the map. That works because each set can only contains the value once.
map.values().stream().flatMap(Set::stream)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream().filter(e -> e.getValue() == map.size())
.map(Map.Entry::getKey).collect(Collectors.toSet());
Upvotes: 2
Reputation: 9013
Set<Integer> commonValues(Map<String, Set<Integer>> map) {
if (map.isEmpty()) {
return new HashSet<>();
}
Set<Integer> intersection = new HashSet<>(map.values().iterator().next());
map.values().forEach(v -> intersection.retainAll(v));
return intersection;
}
Upvotes: 2
Reputation: 19926
A generic and potentially efficient solution could be:
public static <T> Set<T> retain(Map<?, Set<T>> map) {
Iterator<Set<T>> it = map.values().iterator();
if (!it.hasNext()) {
return new HashSet<>();
}
Set<T> result = new HashSet<>(it.next());
while (it.hasNext() && !result.isEmpty()) {
result.retainAll(it.next());
}
return result;
}
Note the !result.isEmpty()
which is an early-exit condition. I.e. if the result is empty, then the sets have no elements in common.
Note: this was inspired by the answer of MikeFHay.
If you really want to use streams, and can guarantee that the Set
s are mutable, then you may also use the reduce()
terminal operation:
public static <T> Set<T> retain(Map<?, Set<T>> map) {
return map.values().stream()
.reduce((a, b) -> {
a.retainAll(b);
return a;
})
.orElse(Set.of());
}
But be aware that this modifies and returns the first set in the map.
Upvotes: 0
Reputation: 26046
First note that using stream is not always the cleanest way.
My idea would be to get the first set and iterate over the rest to check if all of them contain it:
Set<Integer> res = map.values().iterator().next().stream()
.filter(item -> map.values().stream().allMatch(set -> set.contains(item)))
.collect(Collectors.toSet());
This is a concise solution but it checks the first set twice. You can also add a check to see if the map contains any entries.
Upvotes: 3