【问题标题】:Java: how to partition a collection into equivalence classes?Java:如何将集合划分为等价类?
【发布时间】:2016-09-04 19:15:49
【问题描述】:

我有一个项目列表(!):

  • 一个
  • B
  • C
  • D
  • E
  • ...

我想将它们分组:

  • [A、C、D]
  • [B, E]
  • ...

组由以下人员定义:

  • 根据自定义函数 f(a, b) -> boolean,组中的所有项目都相等
  • f(a, b) = f(b, a)

问题:是否有现成的 API 可以这样做?

<T> List<List<T>> group(Collection<T> collection, BiFunction<T, T, Boolean> eqF);

更新。这个问题完全不适用于您可以定义一些质量作为分组依据的场景!在这种情况下,Java 8 Collectors.groupingBy 是最简单的答案。

我正在使用多维向量,等式函数看起来像:

  • 指标(a,b)

在这种情况下,定义哈希等于解决初始任务:)

【问题讨论】:

  • 如果用于对元素进行分组的自定义函数返回布尔值,你怎么能拥有超过 2 个组?
  • @Tunaki - 这称为划分为等价类。假设对象是整数并且相等(真/假)是模 3 计算的(即,如果它们具有相同的余数,则它们相等)。然后从 1 到 100 的整数将最终分到三个桶中,即使这是一个二元相等性测试。
  • 例如有一个 F 元素,它不等于任何其他元素。它由一个元素组成一组。
  • 回应你的更新:你确定你这里有等价关系吗?通常,metrics(a,b) &lt; threshold 是不可传递的,除非您的空间具有满足某些异常约束的结构。
  • 如果测试甚至不是对称的,那么您就没有度量标准。分组项目 (1) 与您真正想要完成的事情以及 (2) 甚至在理论上可能如何相关?听起来您正在寻求与您的潜在问题实际上无关的问题 Y 的解决方案。您可能想要删除此问题并发布一个描述您实际追求的问题的其他问题。

标签: java grouping


【解决方案1】:

您的场景听起来像是groupingBy 收集器的一个很好的用例。通常,您提供一个提取限定符的函数,而不是提供一个相等函数。然后将元素映射到列表中的这些限定符。

Map<Qualifier, List<T>> map = list.stream()
    .collect(Collectors.groupingBy(T::getQualifier));

Collection<List<T>> result = map.values();

如果T 的身份是您的限定符,您可以使用Function.identity() 作为参数。

但是,当您的限定符超过T 的1 个字段时,这将成为一个问题。您可以使用元组类型来为T 创建一个备用标识,但这只是到此为止,因为每个字段数量都需要一个单独的元组类。


如果您想使用groupingBy,您确实需要为T 创建一个温和的备用身份,因此您不必更改TequalshashCode 方法。

要创建正确的身份,您需要实现 equalshashCode(或者总是返回 0 以获得哈希码,但会降低性能)。据我所知,没有这方面的 API 类,但我做了一个简单的实现:

interface AlternateIdentity<T> {    
    public static <T> Function<T, AlternateIdentity<T>> mapper(
            BiPredicate<? super T, Object> equality, ToIntFunction<? super T> hasher) {
        return t -> new AlternateIdentity<T>() {
            @Override
            public boolean equals(Object other) {
                return equality.test(t, other);
            }

            @Override
            public int hashCode() {
                return hasher.applyAsInt(t);
            }
        };
    }
}

你可以这样使用:

Collection<List<T>> result
    = list.stream()
        .collect(Collectors.groupingBy(
            AlternateIdentity.mapper(eqF, hashF)
        ))
        .values();

其中eqF 是您的函数,hashF 是一个哈希码函数,它对与eqF 测试相同的字段进行哈希处理。 (同样,您也可以在 hashF 中返回 0,但正确实现会加快速度。)

【讨论】:

    【解决方案2】:

    您可以使用散列在线性时间内完成此操作。

    为此,您需要首先在对象中实现hashCode() 函数,以便它为相等的元素返回相等的哈希值(例如,通过对其实例属性的哈希码进行异或运算)。然后,您可以使用集合的哈希表对元素进行分组。

    Map<Integer, Set<T>> hashMap = new HashMap<>();
    for (T element : collection) {
        if (!hashMap.containsKey(element.hashCode())
             hashMap.put(element.hashCode(), new HashSet<T>());
        hashMap.get(element.hashCode()).add(element);
    }
    

    由于相等的元素产生相同的哈希,它们将被插入到同一个等价类中。

    现在,您可以使用hashMap.values(); 获取所有等价类(作为集合)的集合

    【讨论】:

    • 编写适当的哈希码并不容易。 (您的建议不起作用,因为如果自定义相等函数这么说,具有不同字段的项目需要使用相同的哈希码。)此外,如果 OP 需要处理相同的项目列表,这将不起作用不同的平等测试。
    • 强制依赖特定的hashCode 函数很糟糕,真的很糟糕。 equals/hashCode 应该被外部化。
    【解决方案3】:

    我很确定标准 API 中没有任何内容。您可以尝试第三方收集类,例如 Trove 的 TCustomHashSet。 (有趣的是,根据this related thread 的评论,Guava 小组(目前)拒绝了类似的课程。请参阅讨论here。)

    另一种方法是推出您自己的解决方案。如果您没有太多项目,我建议采用蛮力方法:保留项目列表的列表,并且对于每个新项目,遍历列表列表并查看它是否等于第一个元素名单。如果是,则将新项目添加到匹配列表中,如果不是,则将新列表添加到列表列表中,并将该项目作为唯一成员。计算复杂度不是很好,这就是为什么我只推荐在项目数量很少或执行时间性能根本不成问题的情况下。

    第二种方法是修改您的项目类以实现自定义相等功能。但是要将它与基于哈希的集合类一起使用,您还需要覆盖hashcode()。 (如果您不使用基于散列的集合,您不妨使用蛮力方法。)如果您不想(或不能)修改项目类(例如,您想使用各种相等测试),我建议创建一个可以使用相等(和哈希码)策略参数化的包装类。 (这是修改您的项目类和使用 Trove 类之间的一种方式。)

    【讨论】:

    • 我认为您应该更深入地了解 Guava:它具有 Equivalence 类/接口,并且在这种情况下可能真的很有帮助。仅仅因为 Guava 不接受它(到目前为止),他们的 API 不能用于实现 OP 的用例。
    • @OlivierGrégoire - 是的,我不太了解番石榴。你应该写下你的想法作为答案。但是根据 OP 对问题的最新编辑,我认为这些方法中的任何一种都不会奏效。 (请参阅我对该问题的第二条评论。)我怀疑这是XY problem
    • 是的,它闻到了 XY 问题,但我认为该解决方案可以很好地工作。另外,番石榴解决方案基本上是乔恩的答案。我正在写他写的内容并在他之后发布并删除了我的答案,因为他的答案更好。
    • @OlivierGrégoire - 从 OP 刚刚描述的情况来看,我认为这些解决方案中的任何一个都不会奏效,无论是我的还是 Jom 的。考虑一个一维案例,如果两个数字彼此相差在 0.5 以内,则它们是“相同的”。 (OP 说这个函数甚至不是对称的,这意味着这已经是过度简化了。)然后 0.0 和 0.3 属于同一个桶;同样是 0.3 和 0.6。但是 0.0 和 0.6 属于不同的桶。所以要么 0.3 进入两个桶(我们没有分区),要么违反了桶约束。
    【解决方案4】:

    这是一个简单的字符串分组示例。如果要分组的对象更复杂,则需要提供除 identity() 之外的其他函数。

    public class StreamGroupingBy
    {
    
       public static void main( String[] args )
       {
          List<String> items = Arrays.asList(  
                  "a", "b", "c", "d", 
                  "a", "b", "c",
                  "a", "b", 
                  "a", "x" );
    
          Map<String,List<String>> result = items.stream().collect(
                  Collectors.groupingBy( Function.identity() ) );
          System.out.println( result );
       }
    }
    

    输出:

    {a=[a, a, a, a], b=[b, b, b], c=[c, c], d=[d], x=[x]}
    

    【讨论】:

      【解决方案5】:

      我还建议实施散列机制。你可以用 Guava FluentIterable 做类似的事情:

      FluentIterable.from(collection)
          .index(new Function<T, K>() {
              K apply(T input) {
                  //transform T to K hash
              }
          })//that would return ImmutableListMultimap<K, T>
          .asMap()//that would return Map<K, Collection<T>>
          .values();//Collection<Collection<T>>
      

      【讨论】:

        猜你喜欢
        • 2017-03-27
        • 1970-01-01
        • 2015-01-10
        • 1970-01-01
        • 1970-01-01
        • 2012-01-24
        • 1970-01-01
        • 2023-03-31
        • 1970-01-01
        相关资源
        最近更新 更多