哈希集代码的意外运行时间

2022-09-01 15:58:42

所以最初,我有这个代码:

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 次操作。如果一秒钟内通常有 100,000,000 个操作,为什么我的代码需要 4 秒才能运行?

此外,如果注释掉该行,则代码仅需要 16 毫秒。如果内部 for 循环被注释掉(但不是 ),则代码只需要 8 毫秒。hashSet.remove(i);hashSet.remove(i);


答案 1

您已经创建了一个边际用例,其中算法降级为二次复杂度。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);
        }
    }

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

由于具有平凡(即哈希码等于数字本身),因此事实证明,连续整数主要占据哈希表中的连续桶:数字0转到第一个存储桶,数字1转到第二个存储桶,依此类推。IntegerhashCode

现在,您将删除从 0 到 99999 的连续数字。在最简单的情况下(当存储桶包含单个键时),键的删除实现为将存储桶数组中的相应元素清空。请注意,删除后不会压缩或重新哈希处理该表。

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

尝试从另一端删除密钥:

hashSet.remove(100_000 - i);

算法将变得快得多!


答案 2

推荐