【问题标题】:How can I make Cartesian product with Java 8 streams?如何使用 Java 8 流制作笛卡尔积?
【发布时间】:2015-11-14 21:45:42
【问题描述】:

我有以下集合类型:

Map<String, Collection<String>> map;

我想从每个 Key 集合中的单个值创建每个 map.size() 的唯一组合。

例如,假设地图如下所示:

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

我想要得到的结果是List&lt;Set&lt;String&gt;&gt; 结果,看起来类似于(排序并不重要,它只需要是包含所有可能组合的“完整”结果):

{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 流来解决。

【问题讨论】:

    标签: java java-8 java-stream cartesian-product


    【解决方案1】:

    带有 forEach 的 Java 8 中的笛卡尔积:

    List<String> listA = Arrays.asList("0", "1");
    List<String> listB = Arrays.asList("a", "b");
    
    List<String> cartesianProduct = new ArrayList<>();
    listA.forEach(a -> listB.forEach(b -> cartesianProduct.add(a + b)));
    
    System.out.println(cartesianProduct);
    // Output: [0a, 0b, 1a, 1b]
    

    【讨论】:

      【解决方案2】:

      map-and-reduce 方法在一个流中嵌套循环

      一个外部流可以很容易地转换为parallel - 这在某些情况下可以减少计算时间。内部迭代是通过循环实现的。

      Try it online!

      /**
       * @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] 
      

      另见:How to get Cartesian product from multiple lists?

      【讨论】:

        【解决方案3】:

        您可以使用Stream.reduce方法如下。

        Try it online!

        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]
        

        另见:String permutations using recursion in Java

        【讨论】:

          【解决方案4】:

          虽然它不是 Stream 解决方案,但 Guava 的 com.google.common.collect.Sets 可以为您做到这一点。

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

          【讨论】:

            【解决方案5】:

            我编写了一个实现Iterable 的类,并且只在内存中保存当前项目。如果需要,The Iterablethe 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;
              }
            }
            

            【讨论】:

              【解决方案6】:

              主要对列表进行操作的解决方案,使事情变得简单得多。它在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);
                              });
                  }
              }
              

              【讨论】:

                【解决方案7】:

                这是另一个解决方案,它没有像 Tagir 的示例那样使用来自 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 API 功能(Collection.forEach 不是 Stream API 的一部分)。您可以用旧的 for-in 循环替换 .forEach,您的代码将与 Java 5 兼容。另请注意,您将所有组合存储在内存中。虽然这对 OP 来说似乎没问题,但如果输入较大,它可能会出现问题。最后,没有简单的方法可以并行化它。
                【解决方案8】:

                一个更简单的答案,适用于您只想获得两个集合元素的笛卡尔积的更简单情况。

                这是一些使用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)))));
                

                【讨论】:

                  【解决方案9】:

                  循环创建组合列表

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

                  【讨论】:

                    【解决方案10】:

                    使用消费者函数类、List&lt;T&gt; 和 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);
                    }
                    

                    【讨论】:

                      【解决方案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));
                      }
                      

                      看起来像递归,但更复杂:它不直接调用自身,而是通过调用外部方法的 lambda。参数:

                      • values:原始值的List(在您的情况下为new ArrayList&lt;&gt;(map.values))。
                      • offset:此列表中的当前偏移量
                      • prefix:长度偏移的当前前缀(如果offset == 0,则为null)。它包含当前从集合list.get(0)list.get(1)list.get(offset-1) 中选择的元素。
                      • supplier:创建结果集合的工厂方法。

                      当我们到达值列表的末尾 (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)。

                      【讨论】:

                        猜你喜欢
                        • 2015-12-14
                        • 1970-01-01
                        • 1970-01-01
                        • 2013-12-25
                        • 1970-01-01
                        • 2016-11-09
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多