HashSet与ArrayList的contains性能对比

54

当处理大量数据时,我经常发现自己会做以下事情:

HashSet<String> set = new HashSet<String> ();
//Adding elements to the set
ArrayList<String> list = new ArrayList<String> (set);

类似于将集合的内容“倒入”列表中。我通常这样做,因为我添加的元素经常包含我想要去除的重复项,这似乎是一种简单的去重方法。

仅考虑这个目标(避免重复),我也可以这样写:

ArrayList<String> list = new ArrayList<String> ();
// Processing here
if (! list.contains(element)) list.add(element);
//More processing here

因此没有必要将这个集合“倒入”列表中。但是,在插入每个元素之前,我会进行一项小检查(我假设HashSet也会这样做)

这两种可能性中哪一种明显更有效?


你的问题的第一部分是错误的。你是把列表转换成集合来去除重复项,而不是反过来,对吗? - MirMasej
你为什么不测试一下呢?顺便问一句,为什么要把集合转换成列表呢?对于大型数组来说,直接遍历集合很可能会更快。 - luk32
你好,感谢您的评论。在这种情况下,我使用数据填充我的集合(以避免重复),然后将其转储到列表中,这样我就可以有效地获得一个没有重复项的列表。 如果我不需要列表,实际上我就不会创建一个,但有时之后会应用排序,并且我处理的一些代码需要列表。 - Jorge
6个回答

105

使用set与使用list相比性能更好(列表的时间复杂度为O(n^2),而集合的时间复杂度为O(n)),这是很正常的,因为集合的成员关系(即contains操作)是集合的核心目的。

HashSetcontains操作的时间复杂度为O(1),而列表则为O(n),因此如果您经常需要运行contains操作,就不应使用列表。


10
如果列表只包含少量元素,会怎样? - Ivan Balashov
8
复杂性计算并不适用于有界问题。其目的是了解当问题规模增加到无穷大时,计算变慢的程度。话虽如此,我并不认为使用列表比哈希集合在“包含”操作上有任何优势。当然,通常情况下集合的内存开销更大,但如果您只有几个元素,那又何必费心呢?对于有界数据集,存在更高效的集合实现(例如 EnumSet),但通常一个简单的哈希集合已经足够满足典型的性能要求。 - Dici
6
通常,我们已经有了一个临时列表,需要运行 .contains。问题是,从哪个大小开始创建 Set 有意义?当元素不足10个时,两者的性能都在1-2微秒之间,但我们需要花时间来创建一个 Set。无论如何,如果有人感兴趣,这里是一个快速的基准测试 https://gist.github.com/ibalashov/0138e850e58942569a636dffa75f0bb9 - Ivan Balashov
@Dici确切地说,它是摊销的O(1)。这与重复项几乎没有关系,List::contains无论如何都会停在第一个重复项;这更多地涉及到HashSet的哈希结构,在这里它提供了很大的提升。 - Eugene
@ Eugene 我对实现哈希表的各种方法非常清楚,但在这个答案中我所指的并不令人惊讶。OP 在这里使用集合成员身份(为了避免重复)更高效是因为集合这个数据结构就是为此而生的。尽管措辞可能不太好。 - Dici

18
< p > ArrayList 使用数组来存储数据,ArrayList.contains 方法的复杂度为 O(n)。因此,反复搜索数组将具有O(n^2)的复杂度。

HashSet 使用哈希机制将元素存储到它们各自的桶中。对于长列表的值,HashSet 的操作速度更快。它将在 O(1) 时间内找到元素。


9
我已经做了一次测试,请检查结果:
对于HashSet、TreeSet、ArrayList和LinkedList中相同的字符串项目,以下是50000个UUID、500万个UUID、500万个UUID的结果:
1. 50000个UUID
- 搜索项: e608c7d5-c861-4603-9134-8c636a05a42b (索引25000) - hashSet.contains(item) ? TRUE 0 ms - treeSet.contains(item) ? TRUE 0 ms - arrayList.contains(item) ? TRUE 2 ms - linkedList.contains(item) ? TRUE 3 ms
2. 500万个UUID
- 搜索项:61fb2592-3186-4256-a084-6c96f9322a86(索引25000) - hashSet.contains(item) ? TRUE 0 ms - treeSet.contains(item) ? TRUE 0 ms - arrayList.contains(item) ? TRUE 1 ms - linkedList.contains(item) ? TRUE 2 ms
3. 500万个UUID
- 搜索项:db568900-c874-46ba-9b44-0e1916420120(索引2500000) - hashSet.contains(item) ? TRUE 0 ms - treeSet.contains(item) ? TRUE 0 ms - arrayList.contains(item) ? TRUE 33 ms - linkedList.contains(item) ? TRUE 65 ms
基于以上结果,使用ArrayList与Set没有太大区别。也许您可以尝试修改此代码,并将“String”替换为您的“Object”,然后查看差异...
    public static void main(String[] args) {
        Set<String> hashSet = new HashSet<>();
        Set<String> treeSet = new TreeSet<>();
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();

        List<String> base = new ArrayList<>();

        for(int i = 0; i<5000000; i++){
            if(i%100000==0) System.out.print(".");
            base.add(UUID.randomUUID().toString());
        }

        System.out.println("\nBase size : " + base.size());
        String item = base.get(25000);
        System.out.println("SEARCHED ITEM : " + item);

        hashSet.addAll(base);
        treeSet.addAll(base);
        arrayList.addAll(base);
        linkedList.addAll(base);

        long ms = System.currentTimeMillis();
        System.out.println("hashSet.contains(item) ? " + (hashSet.contains(item)? "TRUE " : "FALSE") + (System.currentTimeMillis()-ms) + " ms");
        System.out.println("treeSet.contains(item) ? " + (treeSet.contains(item)? "TRUE " : "FALSE") + (System.currentTimeMillis()-ms) + " ms");
        System.out.println("arrayList.contains(item) ? " + (arrayList.contains(item)? "TRUE " : "FALSE") + (System.currentTimeMillis()-ms) + " ms");
        System.out.println("linkedList.contains(item) ? " + (linkedList.contains(item)? "TRUE " : "FALSE") + (System.currentTimeMillis()-ms) + " ms");
    }

6
根据以上结果,使用数组列表和集合之间并没有太大的区别。根据你提供的数据,显然情况并非如此;对于500万个UUID,在元素位于集合中间时,与TreeSet或HashSet相比,ArrayList至少慢33倍。 - Abhishek Divekar
1
这个基准测试太小了,无法得出结论,而且你对它所显示的内容的解释是错误的,正如abhi所提到的那样。 - Dici
2
经典的小时间差假设:2-3毫秒听起来不算多。现在想象一下,你的代码在一个紧密的循环中迭代通过10,000个项目,对每个项目执行“包含”操作。这额外的2-3毫秒刚刚导致了额外的20-30秒延迟!!!我曾经遇到过这种情况,在客户端应用程序中削减特定操作的2-3毫秒,从而实现了令人难以置信的性能提升。只需选择您的优化:没有必要在每小时调用一次的操作上节省2毫秒,但在短时间内数千次调用的操作上节省2毫秒...当然可以! - Volksman
如果我的数学没错的话,根据你的结果,在所有测试中,HashSet和TreeSet比ArrayList和LinkedList快得无限多:2ms/0ms -> 无限。 - cquezel

5
如果您不需要列表,我建议使用Set。如果顺序无关紧要且要忽略重复项,则这是要使用的自然集合。
如果您需要一个没有重复项的List,也可以实现。
private Set<String> set = new HashSet<>();
private List<String> list = new ArrayList<>();


public void add(String str) {
    if (set.add(str))
        list.add(str);
}

这样列表将只包含唯一值,原始插入顺序得以保留,且操作的时间复杂度是O(1)。


4
如果顺序很重要,可以使用 LinkedHashSet;如果需要排序,则可以使用 TreeSet - Dici
如此简单而优雅!我喜欢! - Jorge
@Jorge 注意:Set.add(x) 只有在第一次添加时才返回 true。 - Peter Lawrey
@PeterLawrey,你在评论中提到的注意事项非常重要。它起作用了! :) - Shashanth

1
你可以将元素添加到列表本身。 然后,进行去重 -
HashSet<String> hs = new HashSet<>(); // new hashset
hs.addAll(list); // add all list elements to hashset (this is the dedup, since addAll works as a union, thus removing all duplicates)
list.clear(); // clear the list
list.addAll(hs); // add all hashset elements to the list

如果你只需要一个去重的集合,你也可以在另一个集合上使用addAll()方法,这样它将只包含唯一值。

1

我对 Java 17 中的 TreeSet、HashSet 和 ArrayList 进行了一项关于“contains”方法的小规模测试,使用了随机字符串。

结果表明:在集合中包含 5 个元素左右时,三种数据结构的效率差不多。 当元素数量小于等于 4 时,ArrayList 的速度更快。 当元素数量大于等于 6 时,HashMap 的速度更快。

直觉上,我认为这个 5 的值会高得多,并且 TreeSet 在较小的大小下会比 HashSet 更快。


了解整数之间的关系也很有趣,因为ArrayList中的.contains()使用equals(),而Map中的.contains()首先使用hashCode(),它是一个整数。与比较字符串相比,完全比较整数要慢得多,因此,在整数ArrayList中的.contains()可能会比任何Map实现更快,即使每个数字都是唯一的,对于超过5个条目的情况尤其如此。 - Dreamspace President
1
@DreamspacePresident 另外,String类的hashCode()是惰性计算的事实确实没有帮助我的“ballpark”测试。 - cquezel

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