如何使用 Java 8 个流制作笛卡尔积?

How can I make Cartesian product with Java 8 streams?

我有以下 collection 类型:

Map<String, Collection<String>> map;

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

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

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

我想得到的结果是 List<Set<String>> 结果,看起来类似于(排序并不重要,它只需要是包含所有可能组合的 'complete' 结果):

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

您可以使用递归 flatMap 链来解决这个问题。

首先,因为我们需要通过地图值来回移动,最好将它们复制到 ArrayList(这不是深层复制,在您的情况下它是 ArrayList of 3只有元素,所以额外的内存使用率很低)。

其次,为了维护以前访问过的元素的前缀,让我们创建一个不可变的助手 Prefix class:

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。参数:

  • 值:原始值的 List(在您的情况下为 new ArrayList<>(map.values))。
  • 偏移量:当前列表中的偏移量
  • prefix:长度偏移的当前前缀(或 null if offset == 0)。它包含当前从集合 list.get(0)list.get(1)list.get(offset-1).
  • 中选择的元素
  • 供应商:创建结果集合的工厂方法。

当我们到达值列表的末尾 (offset == values.size() - 1) 时,我们使用供应商将最后一个集合的元素从值映射到最终组合。否则我们使用 flatMap 为每个中间元素扩大前缀并为下一个偏移再次调用 comb 方法。

最后是 public 使用此功能的方法:

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

这是另一个解决方案,它没有像 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));

尽情享受吧!

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

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

Java8 中的笛卡尔积与 forEach:

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]

在循环中创建组合列表

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

我写了一个 class 实现 Iterable,并且只在内存中保存当前项目。如果需要,The Iterable as well as the 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());
    iterators = new Stack<>();
    current = new Stack<>();
    if (this.factors.size() == 0) {
      index = -1;
    } else {
      iterators.add(this.factors.get(0).iterator());
      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;
  }
}

虽然它不是 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"));

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

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

您可以使用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]

另请参阅:

在一个流中使用嵌套循环的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?