java.util.Collections.contains()的执行速度比线性搜索快吗?

时间:2012-10-20 04:46:33

标签: java collections linear-search

我一直在寻找各种不同的搜索集合,集合集合等方式。我做了很多愚蠢的小测试来验证我的理解。这是令我高兴的一个(下面的源代码)。

简而言之,我生成N个随机整数并将它们添加到列表中。该列表未排序。然后我使用Collections.contains()在列表中查找值。我故意寻找一个我知道不会存在的值,因为我想确保探测整个列表空间。我在这个搜索时间。

然后我手动执行另一次线性搜索,遍历列表的每个元素并检查它是否与我的目标匹配。我也是这次搜索的时间。

平均而言,第二次搜索比第一次搜索长33%。根据我的逻辑,第一次搜索也必须是线性的,因为列表是未排序的。我能想到的唯一可能性(我立即丢弃)是Java正在为我的列表制作一个排序的副本,仅用于搜索,但(1)我没有授权使用内存空间和(2)我会认为如此大的N,可以节省更多的时间。

因此,如果两个搜索都是线性的,那么它们都应该花费相同的时间。不知怎的,Collections类已经优化了这个搜索,但我无法弄清楚如何。那么......我错过了什么?

import java.util.*;

public class ListSearch {

    public static void main(String[] args) {

        int N = 10000000; // number of ints to add to the list
        int high = 100; // upper limit for random int generation

        List<Integer> ints;
        int target = -1; // target will not be found, forces search of entire list space

        long start;
        long end;

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 1)... ");
        if (ints.contains(target)) {
            // nothing
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");

        System.out.println();

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 2)... ");
        for (Integer i : ints) {
            // nothing
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");
    }
}

编辑:以下是此代码的新版本。有趣的是,现在我的手动线性循环比contains方法执行16%更快(注意:两者都是为了有意搜索整个列表空间,所以我知道它们是相同的迭代次数)。我无法解释这16%的收益......更加困惑。

import java.util.*;

public class ListSearch {

    public static void main(String[] args) {

        int N = 10000000; // number of ints to add to the list
        int high = 100; // upper limit for random int generation

        List<Integer> ints;
        int target = -1; // target will not be found, forces search of entire list space

        long start;
        long end;

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 1)... ");
        if (ints.contains(target)) {
            System.out.println("hit");
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");

        System.out.println();

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 2)... ");
        for (int i = 0; i < N; i++) {
            if (ints.get(i) == target) {
                System.out.println("hit");
            }
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");
    }
}

3 个答案:

答案 0 :(得分:6)

您的比较代码有问题,这会扭曲您的结果。

这会搜索target

    if (ints.contains(target)) {
        // nothing
    }

但这不是!

    for (Integer i : ints) {
        // nothing
    }

实际上,您只是迭代列表元素而不进行测试。

话虽如此,由于以下一个或多个原因,第二个版本比第一个版本慢:

  • 第一个版本将使用简单的for循环和索引迭代后备数组。第二个版本等同于以下内容:

    Iterator<Integer> it = ints.iterator();
    while (it.hasNext()) {
        Integer i = (Integer) it.next();
    }
    

    换句话说,每次循环都涉及2次方法调用和类型转换 1

  • 第一个版本会在获得匹配后立即返回true。由于您的实现中存在错误,第二个版本将每次迭代整个列表。事实上,考虑到Nhigh的选择,这种影响最有可能是导致性能差异的主要原因。

1 - 实际上,并不完全清楚JIT编译器将对所有这些做什么。它理论上可以 内联方法调用,推断出typcaset是不必要的,甚至可以优化掉整个循环。另一方面,有些因素可能会抑制这些优化。例如,ints被声明为List<Integer>,它可以禁止内联......除非JIT能够推断出实际类型总是相同的。


您的结果可能因其他原因而失真。您的代码未考虑JVM预热。请阅读此问题以获取更多详细信息:How do I write a correct micro-benchmark in Java?

答案 1 :(得分:3)

区别在于:

使用contains时,它使用对象的内部数组并进行如下搜索:

    for (int i = 0; i < size; i++)
        if (searchObject.equals(listObject[i]))
            return true;
    return false;

这里当它试图获取ith元素时,它直接从内部数组中获取第i个元素对象。

当你自己写作时,你写的是:

    for (Integer i : ints) {
        // nothing
    }

相当于:

   for(Iterator<Integer> iter= ints.iterator(); iter.hasNext(); ) {
         Integer i = iter.next();
   }

执行的步骤比contains更多。

答案 2 :(得分:1)

所以我不完全确定你在测试任何东西。 Javac(编译器)非常聪明,可以意识到你的for循环和if语句中没有任何代码。在这种情况下,Java将从其编译中删除该代码。你可能会得到时间的原因是因为你实际上是在计算打印字符串所需的时间。根据系统的运行情况,系统输出时间可能会有很大差异。在编写时序测试时,任何I / O都可能创建无效测试。

首先,我会从你的时间内删除字符串打印。

其次,ArrayList.contains是线性的。它不会像你一样使用特殊的for循环。你的循环有一些从集合中获取迭代器然后迭代它的开销。这就是特殊的for循环在幕后工作的方式。

希望这有帮助。