Как я могу сделать декартово произведение с потоками Java 8?
У меня есть следующий тип коллекции:
Map<String, Collection<String>> map;
Я хотел бы создавать уникальные комбинации каждого из map.size()
от одного значения в коллекции для каждого ключа.
Например, предположим, что карта выглядит следующим образом:
A, {a1, a2, a3, ..., an}
B, {b1, b2, b3, ..., bn}
C, {c1, c2, c3, ..., cn}
Результат, который я хотел бы получить List<Set<String>>
результат, похожий на (порядок не важен, он просто должен быть "полным" результатом, состоящим из всех возможных комбинаций):
{a1, b1, c1},
{a1, b1, c2},
{a1, b1, c3},
{a1, b2, c1},
{a1, b2, c2},
{a1, b2, c3},
...
{a2, b1, c1},
{a2, b1, c2},
...
{a3, b1, c1},
{a3, b1, c2},
...
{an, bn, cn}
Это в основном проблема подсчета, но я хотел бы посмотреть, возможно ли решение с использованием потоков Java 8.
11 ответов
Вы можете решить эту проблему с помощью рекурсивного flatMap
цепь.
Во-первых, поскольку нам нужно перемещаться назад и вперед по значениям карты, лучше скопировать их в ArrayList
(это не глубокая копия, в вашем случае это ArrayList
только из 3 элементов, поэтому использование дополнительной памяти невелико).
Во-вторых, чтобы сохранить префикс ранее посещенных элементов, давайте создадим неизменяемый помощник. Prefix
учебный класс:
private static class Prefix<T> {
final T value;
final Prefix<T> parent;
Prefix(Prefix<T> parent, T value) {
this.parent = parent;
this.value = value;
}
// put the whole prefix into given collection
<C extends Collection<T>> C addTo(C collection) {
if (parent != null)
parent.addTo(collection);
collection.add(value);
return collection;
}
}
Это очень простой неизменяемый связанный список, который можно использовать так:
List<String> list = new Prefix<>(new Prefix<>(new Prefix<>(null, "a"), "b"), "c")
.addTo(new ArrayList<>()); // [a, b, c];
Далее, давайте создадим внутренний метод, который связывает flatMaps:
private static <T, C extends Collection<T>> Stream<C> comb(
List<? extends Collection<T>> values, int offset, Prefix<T> prefix,
Supplier<C> supplier) {
if (offset == values.size() - 1)
return values.get(offset).stream()
.map(e -> new Prefix<>(prefix, e).addTo(supplier.get()));
return values.get(offset).stream()
.flatMap(e -> comb(values, offset + 1, new Prefix<>(prefix, e), supplier));
}
Выглядит как рекурсия, но она более сложная: она не вызывает себя напрямую, а передает лямбду, которая вызывает внешний метод. Параметры:
- значения:
List
исходных значений (new ArrayList<>(map.values)
в твоем случае). - смещение: текущее смещение в этом списке
- префикс: текущий префикс смещения длины (или
null
еслиoffset == 0
). Содержит текущие выбранные элементы из коллекций.list.get(0)
,list.get(1)
вплоть доlist.get(offset-1)
, - поставщик: фабричный метод для создания результирующей коллекции.
Когда мы достигли конца списка значений (offset == values.size() - 1
), мы отображаем элементы последней коллекции из значений в окончательную комбинацию, используя поставщика. В противном случае мы используем flatMap
который для каждого промежуточного элемента увеличивает префикс и вызывает comb
метод снова для следующего смещения.
Наконец, вот публичный метод использования этой функции:
public static <T, C extends Collection<T>> Stream<C> ofCombinations(
Collection<? extends Collection<T>> values, Supplier<C> supplier) {
if (values.isEmpty())
return Stream.empty();
return comb(new ArrayList<>(values), 0, null, supplier);
}
Пример использования:
Map<String, Collection<String>> map = new LinkedHashMap<>(); // to preserve the order
map.put("A", Arrays.asList("a1", "a2", "a3", "a4"));
map.put("B", Arrays.asList("b1", "b2", "b3"));
map.put("C", Arrays.asList("c1", "c2"));
ofCombinations(map.values(), LinkedHashSet::new).forEach(System.out::println);
Мы собираем индивидуальные комбинации для LinkedHashSet
еще раз, чтобы сохранить порядок. Вместо этого вы можете использовать любую другую коллекцию (например, ArrayList::new
).
Декартово произведение в Java 8 с forEach:
List<String> listA = new ArrayList<>();
listA.add("0");
listA.add("1");
List<String> listB = new ArrayList<>();
listB.add("a");
listB.add("b");
List<String> cartesianProduct = new ArrayList<>();
listA.forEach(a -> listB.forEach(b -> cartesianProduct.add(a + b)));
cartesianProduct.forEach(System.out::println);
//Output : 0a 0b 1a 1b
Более простой ответ для более простой ситуации, когда вы просто хотите получить декартово произведение элементов двух коллекций.
Вот код, который использует flatMap
чтобы сгенерировать декартово произведение двух коротких списков:
public static void main(String[] args) {
List<Integer> aList = Arrays.asList(1,2,3);
List<Integer> bList = Arrays.asList(4,5,6);
Stream<List<Integer>> product = aList.stream().flatMap(a ->
bList.stream().flatMap(b ->
Stream.of(Arrays.asList(a, b)))
);
product.forEach(p -> { System.out.println(p); });
// prints:
// [1, 4]
// [1, 5]
// [1, 6]
// [2, 4]
// [2, 5]
// [2, 6]
// [3, 4]
// [3, 5]
// [3, 6]
}
Если вы хотите добавить больше коллекций, просто вложите потоки дальше:
aList.stream().flatMap(a ->
bList.stream().flatMap(b ->
cList.stream().flatMap(c ->
Stream.of(Arrays.asList(a, b, c))))
);
Решение, которое в основном работает со списками, что значительно упрощает работу. Это делает рекурсивный вызов в flatMap
, отслеживая элементы, которые уже были объединены, и коллекции элементов, которые все еще отсутствуют, и предлагает результаты этой вложенной рекурсивной конструкции в виде потока списков:
import java.util.*;
import java.util.stream.Stream;
public class CartesianProduct {
public static void main(String[] args) {
Map<String, Collection<String>> map =
new LinkedHashMap<String, Collection<String>>();
map.put("A", Arrays.asList("a1", "a2", "a3", "a4"));
map.put("B", Arrays.asList("b1", "b2", "b3"));
map.put("C", Arrays.asList("c1", "c2"));
ofCombinations(map.values()).forEach(System.out::println);
}
public static <T> Stream<List<T>> ofCombinations(
Collection<? extends Collection<T>> collections) {
return ofCombinations(
new ArrayList<Collection<T>>(collections),
Collections.emptyList());
}
private static <T> Stream<List<T>> ofCombinations(
List<? extends Collection<T>> collections, List<T> current) {
return collections.isEmpty() ? Stream.of(current) :
collections.get(0).stream().flatMap(e ->
{
List<T> list = new ArrayList<T>(current);
list.add(e);
return ofCombinations(
collections.subList(1, collections.size()), list);
});
}
}
Хотя это не потоковое решение, Com.google.common.collect.Sets от Guava сделает это за вас.
Set<List<String>> result = Sets.cartesianProduct(Set.of("a1","a2"), Set.of("b1","b2"), Set.of("c1","c2" ))
Вот еще одно решение, которое не использует столько функций от Streams
как пример Тагира; однако я считаю, что это будет более простым:
public class Permutations {
transient List<Collection<String>> perms;
public List<Collection<String>> list(Map<String, Collection<String>> map) {
SortedMap<String, Collection<String>> sortedMap = new TreeMap<>();
sortedMap.putAll(map);
sortedMap.values().forEach((v) -> perms = expand(perms, v));
return perms;
}
private List<Collection<String>> expand(List<Collection<String>> list, Collection<String> elements) {
List<Collection<String>> newList = new LinkedList<>();
if (list == null) {
elements.forEach((e) -> {
SortedSet<String> set = new TreeSet<>();
set.add(e);
newList.add(set);
});
} else {
list.forEach((set) ->
elements.forEach((e) -> {
SortedSet<String> newSet = new TreeSet<>();
newSet.addAll(set);
newSet.add(e);
newList.add(newSet);
}));
}
return newList;
}
}
Вы можете удалить Sorted
префикс, если вас не интересует порядок элементов; хотя, я думаю, что легче отлаживать, если все отсортировано.
Использование:
Permutations p = new Permutations();
List<Collection<String>> plist = p.list(map);
plist.forEach((s) -> System.out.println(s));
Наслаждайтесь!
Ты можешь использовать Stream.reduce
метод следующим образом:
Map<String, List<String>> map = new LinkedHashMap<>();
map.put("A", List.of("a1", "a2", "a3"));
map.put("B", List.of("b1", "b2", "b3"));
map.put("C", List.of("c1", "c2", "c3"));
List<List<String>> cartesianProduct = map.values().stream()
// represent each list element as a singleton list
.map(list -> list.stream().map(Collections::singletonList)
.collect(Collectors.toList()))
// reduce the stream of lists to a single list by
// sequentially summing pairs of elements of two lists
.reduce((list1, list2) -> list1.stream()
// combinations of inner lists
.flatMap(first -> list2.stream()
// merge two inner lists into one
.map(second -> Stream.of(first, second)
.flatMap(List::stream)
.collect(Collectors.toList())))
// list of combinations
.collect(Collectors.toList()))
// List<List<String>>
.orElse(Collections.emptyList());
// column-wise output
int rows = 9;
IntStream.range(0, rows)
.mapToObj(i -> IntStream.range(0, cartesianProduct.size())
.filter(j -> j % rows == i)
.mapToObj(j -> cartesianProduct.get(j).toString())
.collect(Collectors.joining(" ")))
.forEach(System.out::println);
Выход:
[a1, b1, c1] [a2, b1, c1] [a3, b1, c1]
[a1, b1, c2] [a2, b1, c2] [a3, b1, c2]
[a1, b1, c3] [a2, b1, c3] [a3, b1, c3]
[a1, b2, c1] [a2, b2, c1] [a3, b2, c1]
[a1, b2, c2] [a2, b2, c2] [a3, b2, c2]
[a1, b2, c3] [a2, b2, c3] [a3, b2, c3]
[a1, b3, c1] [a2, b3, c1] [a3, b3, c1]
[a1, b3, c2] [a2, b3, c2] [a3, b3, c2]
[a1, b3, c3] [a2, b3, c3] [a3, b3, c3]
См. Также: Перестановки строк с использованием рекурсии в Java
Карта-и-снижение подхода с вложенными циклами в пределах одного потока
Один внешний поток можно легко преобразовать в parallel
- в некоторых случаях это может сократить время вычислений. Внутренние итерации реализуются с помощью циклов.
/**
* @param map a map of lists
* @param <T> the type of the elements
* @return the Cartesian product of map values
*/
public static <T> List<List<T>> cartesianProduct(Map<T, List<T>> map) {
// check if incoming data is not null
if (map == null) return Collections.emptyList();
return map.values().stream().parallel()
// non-null and non-empty lists
.filter(list -> list != null && list.size() > 0)
// represent each list element as a singleton list
.map(list -> {
List<List<T>> nList = new ArrayList<>(list.size());
for (T e : list) nList.add(Collections.singletonList(e));
return nList;
})
// summation of pairs of inner lists
.reduce((list1, list2) -> {
// number of combinations
int size = list1.size() * list2.size();
// list of combinations
List<List<T>> list = new ArrayList<>(size);
for (List<T> inner1 : list1)
for (List<T> inner2 : list2) {
List<T> inner = new ArrayList<>();
inner.addAll(inner1);
inner.addAll(inner2);
list.add(inner);
}
return list;
}).orElse(Collections.emptyList());
}
public static void main(String[] args) {
Map<String, List<String>> map = new LinkedHashMap<>();
map.put("A", Arrays.asList("A1", "A2", "A3", "A4"));
map.put("B", Arrays.asList("B1", "B2", "B3"));
map.put("C", Arrays.asList("C1", "C2"));
List<List<String>> cp = cartesianProduct(map);
// column-wise output
int rows = 6;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cp.size(); j++)
System.out.print(j % rows == i ? cp.get(j) + " " : "");
System.out.println();
}
}
Выход:
[A1, B1, C1] [A2, B1, C1] [A3, B1, C1] [A4, B1, C1]
[A1, B1, C2] [A2, B1, C2] [A3, B1, C2] [A4, B1, C2]
[A1, B2, C1] [A2, B2, C1] [A3, B2, C1] [A4, B2, C1]
[A1, B2, C2] [A2, B2, C2] [A3, B2, C2] [A4, B2, C2]
[A1, B3, C1] [A2, B3, C1] [A3, B3, C1] [A4, B3, C1]
[A1, B3, C2] [A2, B3, C2] [A3, B3, C2] [A4, B3, C2]
См. Также: Как получить декартово произведение из нескольких списков?
Я написал класс, реализующий Iterable
и удерживая в памяти только текущий элемент. Iterable
а также Iterator
может быть преобразован в Stream
при желании
class CartesianProduct<T> implements Iterable<List<T>> {
private final Iterable<? extends Iterable<T>> factors;
public CartesianProduct(final Iterable<? extends Iterable<T>> factors) {
this.factors = factors;
}
@Override
public Iterator<List<T>> iterator() {
return new CartesianProductIterator<>(factors);
}
}
class CartesianProductIterator<T> implements Iterator<List<T>> {
private final List<Iterable<T>> factors;
private final Stack<Iterator<T>> iterators;
private final Stack<T> current;
private List<T> next;
private int index = 0;
private void computeNext() {
while (true) {
if (iterators.get(index).hasNext()) {
current.add(iterators.get(index).next());
if (index == factors.size() - 1) {
next = new ArrayList<>(current);
current.pop();
return;
}
index++;
iterators.add(factors.get(index).iterator());
} else {
index--;
if (index < 0) {
return;
}
iterators.pop();
current.pop();
}
}
}
public CartesianProductIterator(final Iterable<? extends Iterable<T>> factors) {
this.factors = StreamSupport.stream(factors.spliterator(), false)
.collect(Collectors.toList());
if (this.factors.size() == 0) {
index = -1;
}
iterators = new Stack<>();
iterators.add(this.factors.get(0).iterator());
current = new Stack<>();
computeNext();
}
@Override
public boolean hasNext() {
if (next == null && index >= 0) {
computeNext();
}
return next != null;
}
@Override
public List<T> next() {
if (!hasNext()) {
throw new IllegalStateException();
}
var result = next;
next = null;
return result;
}
}
В цикле создать комбинированный список
List<String> cartesianProduct(List<List<String>> wordLists) {
List<String> cp = wordLists.get(0);
for (int i = 1; i < wordLists.size(); i++)
{
List<String> secondList = wordLists.get(i);
List<String> combinedList = cp.stream().flatMap(s1 -> secondList.stream().map(s2 -> s1 + s2))
.collect(Collectors.toList());
cp = combinedList;
}
return cp;
}
Используйте класс потребительских функций, список и foreach
public void tester(){
String[] strs1 = {"2","4","9"};
String[] strs2 = {"9","0","5"};
//Final output is {"29", "49, 99", "20", "40", "90", "25", "45", "95"}
List<String> result = new ArrayList<>();
Consumer<String> consumer = (String str) -> result.addAll(Arrays.stream(strs1).map(s -> s+str).collect(Collectors.toList()));
Arrays.stream(strs2).forEach(consumer);
System.out.println(result);
}