web-dev-qa-db-de.com

Wie kann ich mit Java 8-Streams ein kartesisches Produkt erstellen?

Ich habe den folgenden Sammeltyp:

Map<String, Collection<String>> map;

Ich möchte aus jedem Wert in der Auflistung für jeden Schlüssel eindeutige Kombinationen aus jedem von map.size() erstellen.

Angenommen, die Karte sieht folgendermaßen aus:

A, {a1, a2, a3, ..., an}
B, {b1, b2, b3, ..., bn}
C, {c1, c2, c3, ..., cn}

Das Ergebnis, das ich erhalten möchte, wäre ein List<Set<String>>-Ergebnis, das ähnlich aussieht (Reihenfolge ist nicht wichtig, es muss nur ein "komplettes" Ergebnis sein, das aus allen möglichen Kombinationen besteht):

{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}

Dies ist im Grunde ein Zählproblem, aber ich würde gerne sehen, ob eine Lösung mit Java 8-Streams möglich ist.

23
Alex Paransky

Sie können dies mit der rekursiven flatMap-Kette lösen. 

Da wir uns zunächst um die Map-Werte hin und her bewegen müssen, ist es besser, sie in die Variable ArrayList zu kopieren (dies ist nicht die tiefe Kopie. In Ihrem Fall sind es nur ArrayList aus 3 Elementen, sodass der zusätzliche Speicherbedarf gering ist).

Zweitens, um ein Präfix von zuvor besuchten Elementen beizubehalten, erstellen wir eine unveränderliche Hilfsvariable Prefix-Klasse:

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;
    }
}

Dies ist eine sehr einfache unveränderliche verknüpfte Liste, die folgendermaßen verwendet werden kann:

List<String> list = new Prefix<>(new Prefix<>(new Prefix<>(null, "a"), "b"), "c")
                          .addTo(new ArrayList<>()); // [a, b, c];

Als Nächstes erstellen wir die interne Methode, mit der flatMaps verkettet werden:

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));
}

Sieht aus wie eine Rekursion, aber sie ist komplexer: Sie ruft sich nicht direkt auf, sondern passiert Lambda, das die äußere Methode aufruft. Parameter:

  • werte: die List der ursprünglichen Werte (in Ihrem Fall new ArrayList<>(map.values)).
  • offset: der aktuelle Offset innerhalb dieser Liste
  • präfix: das aktuelle Präfix des Längenversatzes (oder null wenn offset == 0). Es enthält aktuell ausgewählte Elemente aus den Kollektionen list.get(0), list.get(1) bis list.get(offset-1).
  • lieferant: die werksmethode zum erstellen der resultierenden sammlung.

Wenn wir das Ende der Werteliste (offset == values.size() - 1) erreicht haben, ordnen wir die Elemente der letzten Sammlung von den Werten bis zur endgültigen Kombination mithilfe des Lieferanten zu. Ansonsten verwenden wir die flatMap, die für jedes Zwischenelement das Präfix vergrößert und die comb-Methode für den nächsten Versatz erneut aufruft.

Zum Schluss noch eine öffentliche Methode, um diese Funktion zu verwenden:

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);
}

Ein Anwendungsbeispiel:

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);

Wir sammeln wieder einzelne Kombinationen der LinkedHashSet, um die Reihenfolge zu erhalten. Sie können stattdessen eine andere Sammlung verwenden (z. B. ArrayList::new).

15
Tagir Valeev

Eine Lösung, die hauptsächlich auf Listen basiert und die Dinge wesentlich einfacher macht. Es führt einen rekursiven Aufruf in flatMap durch, verfolgt die Elemente, die bereits kombiniert wurden, und die Sammlungen von Elementen, die noch fehlen, und bietet die Ergebnisse dieser verschachtelten rekursiven Konstruktion als Listenstrom an: 

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);
            });
    }
}
8
Marco13

Kartesisches Produkt in Java 8 mit 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 
4
Miklos Toth

Hier ist eine andere Lösung, die nicht so viele Funktionen aus Streams wie Tagirs Beispiel verwendet. Ich glaube jedoch, dass es einfacher ist:

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;
    }
}

Sie können das Präfix Sorted entfernen, wenn Sie nicht an der Anordnung von Elementen interessiert sind. Ich denke, es ist einfacher zu debuggen, wenn alles sortiert ist.

Verwendungszweck:

Permutations p = new Permutations();
List<Collection<String>> plist = p.list(map);
plist.forEach((s) -> System.out.println(s));

Genießen!

3
Marin

Eine einfachere Antwort für eine einfachere Situation, in der Sie nur das kartesische Produkt der Elemente zweier Kollektionen haben möchten.

Hier ist ein Code, der flatMap verwendet, um das kartesische Produkt zweier Kurzlisten zu generieren:

    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]
    }

Wenn Sie weitere Sammlungen hinzufügen möchten, verschachteln Sie die Streams einfach einen Wurf weiter:

        aList.stream().flatMap(a -> 
          bList.stream().flatMap(b ->
            cList.stream().flatMap(c ->
               Stream.of(Arrays.asList(a, b, c))))
          );
1
Jurgen Vinju

Guavas com.google.common.collect.Sets ist zwar keine Stream-Lösung, erledigt dies jedoch für Sie

Set<List<String>> result = Sets.cartesianProduct(Set.of("a1","a2"), Set.of("b1","b2"), Set.of("c1","c2" ))
1

Kombinierte Liste erstellen

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;
}
0
ravthiru

Ich habe eine Klasse geschrieben, die Iterable implementiert und nur das aktuelle Element im Speicher hält. Die Iterable sowie die Iterator können auf Wunsch in eine Stream umgewandelt werden.

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;
    }
}
0
ominug

Verwenden Sie eine Consumer-Funktionsklasse, eine Liste und ein 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);

}
0
Olufemi