HashSet代码的意外运行时间

时间:2019-12-29 18:26:30

标签: java performance for-loop hashset

所以最初,我有以下代码:

import java.util.*;

public class sandbox {
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 100_000; i++) {
            hashSet.add(i);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < 100_000; i++) {
            for (Integer val : hashSet) {
                if (val != -1) break;
            }

            hashSet.remove(i);
        }

        System.out.println("time: " + (System.currentTimeMillis() - start));
    }
}

在我的计算机上运行嵌套的for循环大约需要4秒钟,我不明白为什么要花这么长时间。外层循环运行100,000次,内层for循环运行1次(因为hashSet的任何值永远不会为-1),并且从HashSet中删除项目的次数为O(1),因此应进行约200,000次操作。如果通常每秒进行一亿次操作,我的代码为什么要花4秒钟才能运行?

此外,如果将行hashSet.remove(i);注释掉,则代码仅需16毫秒。 如果内部的for循环已注释掉(但没有注释hashSet.remove(i);),则代码仅需8毫秒。

1 个答案:

答案 0 :(得分:22)

您已经创建了HashSet的边际用例,该算法会降级为二次复杂度。

这是耗时很长的简化循环:

for (int i = 0; i < 100_000; i++) {
    hashSet.iterator().next();
    hashSet.remove(i);
}

async-profiler表明,几乎所有时间都花在java.util.HashMap$HashIterator()构造函数中:

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
--->        do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

突出显示的行是一个线性循环,用于搜索哈希表中的第一个非空存储桶。

由于Integer具有平凡的hashCode(即hashCode等于数字本身),因此可以证明,连续整数主要占据哈希表中的连续存储桶:数字0进入第一个桶,数字1进入第二个桶,依此类推。

现在,您从0到99999中删除连续的数字。在最简单的情况下(存储桶包含单个键时),删除键的实现是使存储桶数组中的相应元素无效。请注意,该表在删除后不会压缩或重新放置。

因此,您从存储桶数组的开头删除的键越多,HashIterator就需要越长的时间来找到第一个非空存储桶。

尝试从另一端删除密钥:

hashSet.remove(100_000 - i);

算法将变得更快!

相关问题