所以最初,我有以下代码:
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毫秒。
答案 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);
算法将变得更快!