【问题标题】:What is the best way to find common elements from 2 sets?从 2 个集合中找到共同元素的最佳方法是什么?
【发布时间】:2019-10-22 15:05:01
【问题描述】:

最近我参加了一次面试,被问到一个问题。

我有 2 套,每套大约有 100 万条记录。 我必须在 2 个集合中找到共同的元素。

我的回答:

我将创建一个新的空集。我给了他下面的解决方案,但他对此并不满意。他说有 100 万条记录,所以解决方案不会很好。

public Set<Integer> commonElements(Set<Integer> s1, Set<Integer> s2) {
    Set<Integer> res = new HashSet<>();
     for (Integer temp : s1) {
        if(s2.contains(temp)) {
            res.add(temp);
        }
     }
     return res;
}

那么解决这个问题的更好方法是什么?

【问题讨论】:

  • s1.retainAll(s2)?
  • @a_horse_with_no_name 怎么这么快?它仍然遍历整个集合,并检查contains。此外,它修改了s1,而不是返回一个新集合。
  • 如果它们是SortedSets,您可以迭代这两个集合,推进一个迭代器或另一个迭代器,直到迭代器指向相同的元素。
  • 比较你的实现方式,比如Guava implements set intersection(在计算视图方面略有不同,但大体上是相同的)。它以大致相同的方式进行。

标签: java algorithm for-loop set


【解决方案1】:

首先:为了确定两个集合的交集,您绝对必须查看两个集合中至少一个集合的所有条目(以确定它是否在另一个集合中) .没有什么魔法可以告诉你不到 O(min(size(s1), size(s2))。期间。

接下来要告诉面试官:“100 万个条目。你一定是在开玩笑。现在是 2019 年。任何像样的硬件都可以在不到一秒的时间内处理两个 100 万套”。 (当然:这只适用于比较 便宜 的对象,例如这里的整数实例。如果 oneRecord.equals(anotherRecord) 是一个超级昂贵的操作,那么 2022 年 100 万个条目可能仍然是个问题) .

然后您简要提到有各种内置方法可以解决此问题,以及各种 3rd 方库。但是您避免了其他两个答案所犯的错误:指向确实计算相交的库根本不是您作为该问题的“解决方案”出售的东西。

你看,关于编码:java Set 接口有一个 easy 解决方案:s1.retainAll(s2) 计算两个集合的连接,因为它从 s1 中删除所有元素 不在 s2 中。

显然,你必须在采访中提到这将修改 s1。

如果要求修改 s1 或 s2,则您的解决方案是一种可行的方法,并且对于运行时成本无能为力。如果是这样,您可以为这两个集合调用size(),然后迭代条目较少的那个。

或者,你可以这样做

Set<String> result = new HashSet<>(s1);
return result.retain(s2);

但最后,你必须迭代一个集合,并为每个元素确定它是否在第二个集合中。

当然,对此类问题的真正答案总是总是向面试官表明您能够将问题分解为不同的方面。你概述了基本的限制,你概述了不同的解决方案并讨论它们的优缺点。以我为例,我希望你坐下来写一个这样的程序:

public class Numbers {    
    private final static int numberOfEntries = 20_000_000;
    private final static int maxRandom = numberOfEntries;

    private Set<Integer> s1;
    private Set<Integer> s2;

    @Before
    public void setUp() throws Exception {
        Random random = new Random(42);
        s1 = fillWithRandomEntries(random, numberOfEntries);
        s2 = fillWithRandomEntries(random, numberOfEntries);
    }

    private static Set<Integer> fillWithRandomEntries(Random random, int entries) {
        Set<Integer> rv = new HashSet<>();
        for (int i = 0; i < entries; i++) {
            rv.add(random.nextInt(maxRandom));
        }
        return rv;
    }

    @Test
    public void classic() {
        long start = System.currentTimeMillis();
        HashSet<Integer> intersection = new HashSet<>();
          s1.forEach((i) -> {
           if (s2.contains(i))
             intersection.add(i);
        });
        long end = System.currentTimeMillis();
        System.out.println("foreach duration: " + (end-start) + " ms");
        System.out.println("intersection.size() = " + intersection.size());
    }


    @Test
    public void retainAll() {
        long start = System.currentTimeMillis();
        s1.retainAll(s2);
        long end = System.currentTimeMillis();
        System.out.println("Retain all duration: " + (end-start) + " ms");
        System.out.println("intersection.size() = " + s1.size());
    }

    @Test
    public void streams() {
        long start = System.currentTimeMillis();
        Set<Integer> intersection = s1.stream().filter(i -> s2.contains(i)).collect(Collectors.toSet());
        long end = System.currentTimeMillis();
        System.out.println("streaming: " + (end-start) + " ms");
        System.out.println("intersection.size() = " + intersection.size());
    }

    @Test
    public void parallelStreams() {
        long start = System.currentTimeMillis();
        Set<Integer> intersection = s1.parallelStream().filter(i -> s2.contains(i)).collect(Collectors.toSet());
        long end = System.currentTimeMillis();
        System.out.println("parallel streaming: " + (end-start) + " ms");
        System.out.println("intersection.size() = " + intersection.size());
    }
}

这里的第一个观察结果:我决定运行 2000 万 个条目。我从 200 万开始,但所有三个测试的运行时间都远低于 500 毫秒。这是在我的 Mac Book Pro 上打印的 2000 万张:

foreach duration: 9304 ms
intersection.size() = 7990888 
streaming: 9356 ms
intersection.size() = 7990888
Retain all duration: 685 ms
intersection.size() = 7990888
parallel streaming: 6998 ms
intersection.size() = 7990888

正如预期的那样:所有相交的大小都相同(因为我设置了随机数生成器的种子以获得可比较的结果)。

令人惊讶的是:就地修改 s1 ... 是迄今为止最便宜的选择。它以 10 倍的 因素 击败流媒体。另请注意:这里的并行流媒体更快。当运行 100 万个条目时,sequential 流更快。

因此我最初提到要提到“100 万个条目不是性能问题”。这是一个非常重要的声明,因为它告诉面试官,你不是那些浪费时间对不存在的性能问题进行微观优化的人。

【讨论】:

  • 超级!现在鼓掌一分钟.. :-)
  • @Nirmalya 但是你让我想到了......要稍微编辑一下答案。
  • 我理解 equals/hash 复杂性的重要性,但这适用于任何类型的比较。即使是 DB 引擎,使用 10-elem-Primary-Key 的时间也会比使用 1-elem-Primary-Key 的时间多。所以,那里。
  • 是的。但是,这个问题专门询问了有关 Integer 对象集的问题。
【解决方案2】:

你可以使用

CollectionUtils

它来自 apache

CollectionUtils.intersection(Collection a,Collection b)

【讨论】:

    【解决方案3】:

    【讨论】:

    • 没有“那个”答案。正如 GhostCat 所指出的,这是 an 答案,但可能不是必需的,因为它会更改 s1 的内容。希望面试官不是在寻找答案,而是在寻找候选人对所做选择的问题的理解程度。
    • @AndyTurner 如此真实。我想,我今天可能会花更多的时间来思考这个问题,而不是他采访中的 OP……
    猜你喜欢
    • 1970-01-01
    • 2018-07-16
    • 2012-03-21
    • 2018-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多