概述

排列是集合中元素的重新排列。换句话说,它是集合所有可能的顺序变化。

在这个教程中,我们将学习如何使用第三方库轻松在Java中生成排列。特别是,我们将关注字符串中的排列。

2. 排列

有时,我们需要检查一个字符串值的所有可能排列,这通常出现在令人费解的在线编程练习中,而在日常工作中较少遇到。例如,字符串"abc"有六种不同的方式来排列内部字符:"abc"、"acb"、"cab"、"bac"、"bca"和"cba"。

有几个定义明确的算法可以帮助我们为特定的String值创建所有可能的排列。例如,最著名的可能是Heap算法。然而,它相当复杂且不易理解。再加上递归方法,情况会变得更糟。

3. 优雅解决方案

为生成排列编写算法需要编写自定义逻辑。实现过程中容易出错,并且很难长期确保其正确性。此外,没有必要重写已经写过的东西。

另外,在处理String值时,如果不谨慎,可能会因创建太多实例而填满String池。

以下是当前提供此类功能的库:

  • Apache Commons
  • Guava
  • CombinatoricsLib

让我们尝试使用这些库找出一个String值的所有排列。我们将关注这些库是否支持懒惰遍历排列,并处理输入值中的重复项。

下面的例子将使用Helper.toCharacterList方法。这个方法封装了将String转换为Character列表的复杂性:

static List<Character> toCharacterList(final String string) {
    return string.chars().mapToObj(s -> ((char) s)).collect(Collectors.toList());
}

同时,我们还将使用一个辅助方法将Character列表转换为String

static String toString(Collection<Character> collection) {
    return collection.stream().map(s -> s.toString()).collect(Collectors.joining());
}

4. Apache Commons

首先,将Maven依赖commons-collections4添加到项目中:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

总体来说,Apache提供了简单的API。CollectionUtils迫切地创建排列,因此在处理长String值时要小心:

public List<String> eagerPermutationWithRepetitions(final String string) {
    final List<Character> characters = Helper.toCharacterList(string);
    return CollectionUtils.permutations(characters)
        .stream()
        .map(Helper::toString)
        .collect(Collectors.toList());
}

然而,为了采用懒惰的方法,我们应该使用PermutationIterator

public List<String> lazyPermutationWithoutRepetitions(final String string) {
    final List<Character> characters = Helper.toCharacterList(string);
    final PermutationIterator<Character> permutationIterator = new PermutationIterator<>(characters);
    final List<String> result = new ArrayList<>();
    while (permutationIterator.hasNext()) {
        result.add(Helper.toString(permutationIterator.next()));
    }
    return result;
}

此库不处理重复项,所以输入字符串"aaaaaa"会产生720个排列,这通常是不希望看到的。此外,PermutationIterator没有获取排列数量的方法。在这种情况下,我们需要根据输入大小单独计算它们。

5. Guava

首先,为Guava库添加Maven依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
</dependency>

Guava允许使用Collections2创建排列。API使用起来很简单:

public List<String> permutationWithRepetitions(final String string) {
    final List<Character> characters = Helper.toCharacterList(string);
    return Collections2.permutations(characters).stream()
        .map(Helper::toString)
        .collect(Collectors.toList());
}

Collections2.permutations的结果是一个PermutationCollection,可以方便地访问排列。所有排列都是懒惰创建的。

此外,这个类还提供了创建无重复排列的API:

public List<String> permutationWithoutRepetitions(final String string) {
    final List<Character> characters = Helper.toCharacterList(string);
    return Collections2.orderedPermutations(characters).stream()
        .map(Helper::toString)
        .collect(Collectors.toList());
}

然而,这些方法的问题是它们被标记为@Beta注解,这意味着这个API在未来版本中可能会更改。

6. CombinatoricsLib

要在项目中使用它,请添加Maven依赖combinatoricslib3

<dependency>
    <groupId>com.github.dpaukov</groupId>
    <artifactId>combinatoricslib3</artifactId>
    <version>3.3.3</version>
</dependency>

尽管这是一个小库,但它提供了许多组合学工具,包括排列。API本身非常直观,并利用Java流。让我们从特定的StringCharacter列表创建排列:

public List<String> permutationWithoutRepetitions(final String string) {
    List<Character> chars = Helper.toCharacterList(string);
    return Generator.permutation(chars)
      .simple()
      .stream()
      .map(Helper::toString)
      .collect(Collectors.toList());
}

上述代码创建了一个生成器,它将为字符串提供排列。排列将被懒惰地获取。因此,我们只创建了一个生成器并计算了预期的排列数。

同时,使用此库,我们可以确定处理重复项的策略。以字符串"aaaaaa"为例,我们将得到一个排列,而不是720个相同的排列。

public List<String> permutationWithRepetitions(final String string) {
    List<Character> chars = Helper.toCharacterList(string);
    return Generator.permutation(chars)
      .simple(TreatDuplicatesAs.IDENTICAL)
      .stream()
      .map(Helper::toString)
      .collect(Collectors.toList());
}

TreatDuplicatesAs允许我们定义如何处理重复项。

7. 总结

处理组合学,尤其是排列,有很多方法。这些库都能在很大程度上帮助我们。值得尝试所有选项,然后决定哪个最适合你的需求。尽管有时人们被鼓励自己编写所有代码,但没有理由在已经有良好功能的地方浪费时间。

如往常一样,示例代码可在GitHub上的源代码仓库中找到。