在Java中获取两个集合的对称差最佳方法是什么?

45

我想知道是否有一种快速/清晰的方法来获取两个集合之间的对称差异?

我有:

Set<String> s1 = new HashSet<String>();
s1.add("a");
s1.add("b");
s1.add("c");

Set<String> s2 = new HashSet<String>();
s2.add("b");
我需要类似以下的东西:
Set<String> diff = Something.diff(s1, s2);
// diff would contain ["a", "c"]

只是想澄清一下,我需要对称差集


3
快速且简单:您可以编写Set<String> diff = new HashSet<String>(s1); diff.removeAll(s2);来实现。该语句将创建一个s1的副本diff,并从中删除所有s2中存在的元素,最终得到差异集合。 - misberner
4
@polkageist说:对于S1={"a","b","c"},S2={"b","d"},这个方法会失败。结果应该是{"a","c","d"}。 - amit
3
如果“difference”(参见https://secure.wikimedia.org/wikipedia/en/wiki/Set_difference#Relative_complement)指的是对称差异,则你是正确的。然而,你可以将其表示为(A - B) + (B - A)或(A + B) - (A ∩ B)。我不知道在Java中实现这一点的更快方法。 - misberner
2
Java 8和Java 11:https://dev59.com/kGsz5IYBdhLWcg3wNE1q#52268640 - akhil_mittal
8个回答

52

您可以使用Google Guava库中的一些函数(非常好用,我强烈推荐!):

Sets.difference(s1, s2);
Sets.symmetricDifference(s1, s2);

difference()symmetricDifference()的Javadocs

symmetricDifference()恰好执行你所要求的内容, 但是difference()也经常很有帮助。

这两种方法都返回一个实时视图,但是您可以在结果集上调用.immutableCopy(),以获取一个不可更改的集合。如果您不想要一个视图,但需要一个可以修改的集合实例,请调用.copyInto(s3)。有关这些方法,请参见SetView


1
刚刚阅读了symmetricDifference()的javadoc,我对这个声明有些担忧:“如果set1和set2是基于不同等价关系的集合(如HashSet、TreeSet和IdentityHashMap的keySet),则结果未定义。” 这使得它听起来像是OP的情况下结果是未定义的(Sets.symmetricDifference()方法似乎也不太有用)。 - Gus
2
@Gus JavaDoc 想表达的是,如果您使用具有不同等价关系的两个不同集合来使用该方法,则结果是未定义的。例如,计算 HashSetTreeSet 之间的差异可能会出现问题。使用两个 HashSet、两个 TreeSets 等方法是可以的。 - Philipp Wendler
1
@Gus 此外,如果您使用的是HashSetTreeSet方法,并且元素的compareTo()方法和equals()方法相符(这是它们文档所强烈推荐的),那么也没问题。因此,在特殊情况下,该方法无法保证正确计算差异,但在大多数实际情况下,它都可以正常工作。 - Philipp Wendler
我对“结果未定义”的这个语句也有同样的担忧。感谢@PhilippWendler澄清了这一点! - datv

35

你想要对称差集

public static <T> Set<T> diff(final Set<? extends T> s1, final Set<? extends T> s2) {
    Set<T> symmetricDiff = new HashSet<T>(s1);
    symmetricDiff.addAll(s2);
    Set<T> tmp = new HashSet<T>(s1);
    tmp.retainAll(s2);
    symmetricDiff.removeAll(tmp);
    return symmetricDiff;
}

如果您需要一个库,Apache Commons CollectionUtils 是一个不错的选择。
CollectionUtils.disjunction(s1, s2)

该方法返回一个非泛型的Collection

Guava Sets提供了以下功能:

Sets.symmetricDifference(s1, s2)

该方法返回一个不可修改的泛型Sets.SetView集合。

Guava更为现代化,支持泛型,但这两种方法都可以使用。


谢谢,我实际上正在寻找一个可以为我完成这项工作的库,因为这是我目前所做的。 - Simeon
CollectionUtilsApache Commons Collections v4+ 的一个工具类,支持泛型。 - fdaugan

9
如果你能使用Apache-Commons Collections,你需要使用CollectionUtils.disjunction(Collection a, Collection b)。它返回两个集合的对称差。
如果不能使用,可以通过从两个集合的并集(addAll)中减去交集(retainAll)来得到差集(removeAll)。
Set<String> intersection = new HashSet<String>(set1);
intersection.retainAll(set2);

Set<String> difference = new HashSet<String>();
difference.addAll(set1);
difference.addAll(set2);
difference.removeAll(intersection);

4

循环一个集合并进行比较。

仅循环其中一个集合的时间复杂度为O(n)。考虑以下代码:

for (String key: oldSet) {
    if (newSet.contains(key))
        newSet.remove(key);
    else
        newSet.add(key);
}

newSet将会包含两个集合中的唯一元素。这是一个快速的方法,因为你只需要遍历其中一个集合的元素,而且你不需要创建新的集合,除非你明确需要复制。


1
请注意,对于没有冲突的HashSet,这是O(n)。对于TreeSets,所有三个操作add(key)contains(key)remove(key)都需要搜索一次树,即额外的O(n log n) - kazenorin

2

Java 8 解决方案

我们可以在一个名为SetUtils的类中编写两个实用方法(适用于Java 8及之前版本),如下:

public static <T> Set<T> symmetricDifferenceJava8(final Set<T> setOne, final Set<T> setTwo) {
    Set<T> result = new HashSet<>(setOne);
    setTwo.stream().filter(not(resultSet::add)).forEach(resultSet::remove);
    return result;
}

public static <T> Set<T> symmetricDifference(final Set<T> setOne, final Set<T> setTwo) {
    Set<T> result = new HashSet<T>(setOne);
    for (T element : setTwo) {
        if (!result.add(element)) {
            result.remove(element);
        }
    }
    return result;
}

public static <T> Predicate<T> not(Predicate<T> t) {
    return t.negate();
}

如果元素已经存在,则方法add返回false,而方法negate用于否定谓词。

Java 11

在Java 11中,我们有一个针对谓词的Predicate#not方法,可以使用如下:

public static <T> Set<T> symmetricDifferenceJava11(final Set<T> setOne, final Set<T> setTwo) {
    Set<T> result = new HashSet<>(setOne);
    setTwo.stream().filter(Predicate.not(resultSet::add)).forEach(resultSet::remove);
    return result;
}

1
public class Practice {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<Integer>();
        Set<Integer> set2 = new HashSet<Integer>();
        set1.add(1);
        set1.add(4);
        set1.add(7);
        set1.add(9);

        set2.add(2);
        set2.add(4);
        set2.add(5);
        set2.add(6);
        set2.add(7);

        symmetricSetDifference(set1, set2);
    }

    public static void symmetricSetDifference(Set<Integer>set1, Set<Integer>set2){
        //creating a new set
        Set<Integer> newSet = new HashSet<Integer>(set1);
        newSet.removeAll(set2);
        set2.removeAll(set1);
        newSet.addAll(set2);
        System.out.println(newSet);
    }

}


1
尽管这段代码可能回答了问题,但提供关于它为什么和/或如何回答问题的额外上下文会显著提高其长期价值。请编辑您的答案以添加一些解释。 - Toby Speight

1
public static <T> Set<T> symmetricDifference(Set<? extends T> a, Set<? extends T> b) {
    return Stream.of(a, b).flatMap(Collection::stream).filter(d -> !(a.contains(d) && b.contains(d)))
            .collect(Collectors.toSet());
}

你的回答与问题不相关。你的代码是用于计算差集,而问题是关于对称差分的。 - TomerBu
1
@TomerBu 更正了答案。 - krishna T
1
不错!这是我的实现:public static <T> Set<T> symmetricDifference(Set<? extends T> a, Set<? extends T> b) {//将a的值复制到resultSet Set resultSet = new HashSet<>(a); //将b中的所有项添加到resultSet,并记住返回false的项(交集) b.stream().filter(Predicate.not(resultSet::add)) //将b添加到resultSet并仅保留交集 .forEach(resultSet::remove);//从resultSet中删除交集 return resultSet;}但你的更好(只有一行 :-) - TomerBu

0
public class Practice {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<Integer>();
        Set<Integer> set2 = new HashSet<Integer>();
        set1.add(1);
        set1.add(4);
        set1.add(7);
        set1.add(9);

        set2.add(2);
        set2.add(4);
        set2.add(5);
        set2.add(6);
        set2.add(7);

        symmetricSetDifference(set1, set2);
    }

    public static void symmetricSetDifference(Set<Integer>set1, Set<Integer>set2){
        //creating a new set
        Set<Integer> newSet = new HashSet<Integer>(set1);
        newSet.removeAll(set2);
        set2.removeAll(set1);
        newSet.addAll(set2);
        System.out.println(newSet);
    }

如果 ab 是集合
a - b

是所有在a中但不在b中的内容。

>>> a = {1,2,3}
>>> b = {1,4,5}
>>> 
>>> a - b
{2, 3}
>>> b - a
{4, 5}

a.symmetric_difference(b) 返回的是两个集合中仅出现一次的元素,例如 a - bb - a 的并集。

>>> a.symmetric_difference(b)
{2, 3, 4, 5}
>>> (a - b).union(b - a)
{2, 3, 4, 5}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接