从2套中找到共同元素的最佳方法是什么?

时间:2019-06-07 05:56:27

标签: java algorithm for-loop set

最近我接受了一次采访,并被问到一个问题。

我有2套唱片,每套唱片约有100万条。 我必须在2组中找到相同的元素。

我的答复:

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

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

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

3 个答案:

答案 0 :(得分:2)

首先:为了确定两个集合的交集,您绝对必须查看两个集合中至少一个的所有条目(以判断它是否在另一个集合中) 。周围没有任何魔法可以告诉您小于 O(min(size(s1),size(s2))。Period。

接下来要告诉采访者的事情是:“ 100万个条目。你一定是在开玩笑。现在是2019年。任何体面的硬件在不到一秒钟的时间内就会处理两次100万”。

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

关于编码,您看到了: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万个条目时,顺序流会更快。

因此,我最初提到提到“ 100万个条目不是性能问题”。这是一条非常重要的声明,因为它告诉采访者您不是那些浪费时间来微优化不存在的绩效问题的人之一。

答案 1 :(得分:1)

您可以使用

  

CollectionUtils

来自Apache

CollectionUtils.intersection(Collection a,Collection b)

答案 2 :(得分:0)