web-dev-qa-db-fra.com

Comment créer un produit cartésien avec des flux Java 8?

J'ai le type de collection suivant:

Map<String, Collection<String>> map;

J'aimerais créer des combinaisons uniques de chacune des map.size() à partir d'une valeur unique de la collection pour chaque clé.

Par exemple, supposons que la carte ressemble à ceci:

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

Le résultat que j'aimerais obtenir serait un résultat List<Set<String>>, ressemblant à (l'ordre n'est pas important, il doit simplement s'agir d'un résultat 'complet' comprenant toutes les combinaisons possibles):

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

Il s’agit essentiellement d’un problème de comptage, mais j’aimerais voir si une solution est possible avec les flux Java 8.

23
Alex Paransky

Vous pouvez résoudre ce problème en utilisant la chaîne récursive flatMap

Tout d'abord, car nous devons parcourir les valeurs de la carte, il est préférable de les copier dans la variable ArrayList (il ne s'agit pas de la copie intégrale; dans votre cas, il s'agit de ArrayList de 3 éléments uniquement, de sorte que l'utilisation supplémentaire de mémoire est faible).

Deuxièmement, pour conserver un préfixe des éléments précédemment visités, créons une classe immuable d'assistant 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;
    }
}

C'est une liste chaînée immuable très simple qui peut être utilisée comme ceci:

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

Ensuite, créons la méthode interne qui chaîne 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));
}

Cela ressemble à de la récursivité, mais c'est plus complexe: il ne s'appelle pas directement, mais passe lambda, qui appelle la méthode externe. Paramètres:

  • valeurs: la List des valeurs d'origine (new ArrayList<>(map.values) dans votre cas).
  • offset: le décalage actuel dans cette liste
  • préfixe: le préfixe actuel de décalage de longueur (ou null si offset == 0). Il contient les éléments actuellement sélectionnés dans les collections list.get(0), list.get(1) jusqu'à list.get(offset-1).
  • fournisseur: la méthode d'usine pour créer la collection résultante.

Lorsque nous avons atteint la fin de la liste de valeurs (offset == values.size() - 1), nous mappons les éléments de la dernière collection des valeurs à la combinaison finale à l'aide du fournisseur. Sinon, nous utilisons la variable flatMap qui, pour chaque élément intermédiaire, agrandit le préfixe et appelle à nouveau la méthode comb pour le décalage suivant.

Enfin, voici une méthode publique pour utiliser cette fonctionnalité:

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

Un exemple d'utilisation:

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

Nous recueillons à nouveau les combinaisons individuelles dans LinkedHashSet afin de préserver l'ordre. Vous pouvez utiliser toute autre collection à la place (par exemple ArrayList::new).

15
Tagir Valeev

Une solution qui fonctionne principalement sur des listes, ce qui simplifie beaucoup les choses. Il effectue un appel récursif dans flatMap, en gardant une trace des éléments déjà combinés et des collections d'éléments manquants, et offre les résultats de cette construction récursive imbriquée sous forme de flux de listes: 

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

Produit cartésien en Java 8 avec 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

Voici une autre solution, qui n'utilise pas autant de fonctionnalités de Streams que l'exemple de Tagir; Cependant, je crois que c'est plus simple:

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

Vous pouvez supprimer le préfixe Sorted si vous n'êtes pas intéressé par la commande d'éléments. Cependant, je pense qu'il est plus facile de déboguer si tout est réglé.

Usage:

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

Prendre plaisir!

3
Marin

En boucle créer une liste combinée

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

J'ai écrit une classe implémentant Iterable et ne conservant que l'élément actuel en mémoire. La Iterable ainsi que la Iterator peuvent être converties en Stream si vous le souhaitez.

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

Utiliser une classe de fonction consommateur, une liste et un 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