我几乎可以肯定我在某个地方遇到过这样的问题,但我找不到它。为了通过在LinkedList上等于迭代来找到一个密钥,并且这个过程是O(n),HashMap如何获得性能是O(1)。
编辑:
我发现get()性能实际上是在没有碰撞时O(1)和每个键碰撞时O(n)之间。这是对的吗?
答案 0 :(得分:2)
get
的预期效果为O(1)
。在假设hashCode
方法在HashMap
的桶之间统一分配密钥的情况下计算预期性能,因此每个链表的平均大小(即每个桶中的平均条目数)非常小,所以我们可以假设每个这样的列表都可以在一个小常量的时间内遍历。
在最糟糕的情况下,如果错误hashCode
将所有密钥映射到同一个存储区,get
将采用O(n)
。
答案 1 :(得分:2)
基于散列的对象将根据散列值确定它们将在哪个存储桶中存储键值对。在每个桶中,存在一对结构(在HashMap情况下为LinkedList)。
如果哈希值通常相同,那么存储桶通常会相同,因此性能会下降很多,让我们看一个例子:
考虑这个课程
package hashTest;
import java.util.Hashtable;
public class HashTest {
public static void main (String[] args) {
Hashtable<MyKey, String> hm = new Hashtable<>();
long ini = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
MyKey a = new HashTest().new MyKey(String.valueOf(i));
hm.put(a, String.valueOf(i));
}
System.out.println(hm.size());
long fin = System.currentTimeMillis();
System.out.println("tiempo: " + (fin-ini) + " mls");
}
private class MyKey {
private String str;
public MyKey(String i) {
str = i;
}
public String getStr() {
return str;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean equals(Object o) {
if (o instanceof MyKey) {
MyKey aux = (MyKey) o;
if (this.str.equals(aux.getStr())) {
return true;
}
}
return false;
}
}
}
请注意,类MyKey中的hashCode总是返回'0'作为哈希。哈希码定义(http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#hashCode())没问题。如果我们运行该程序,这就是结果
100000
tiempo: 62866 mls
性能非常差,现在我们要更改MyKey哈希码代码:
package hashTest;
import java.util.Hashtable;
public class HashTest {
public static void main (String[] args) {
Hashtable<MyKey, String> hm = new Hashtable<>();
long ini = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
MyKey a = new HashTest().new MyKey(String.valueOf(i));
hm.put(a, String.valueOf(i));
}
System.out.println(hm.size());
long fin = System.currentTimeMillis();
System.out.println("tiempo: " + (fin-ini) + " mls");
}
private class MyKey {
private String str;
public MyKey(String i) {
str = i;
}
public String getStr() {
return str;
}
@Override
public int hashCode() {
return str.hashCode() * 31;
}
@Override
public boolean equals(Object o) {
if (o instanceof MyKey) {
MyKey aux = (MyKey) o;
if (this.str.equals(aux.getStr())) {
return true;
}
}
return false;
}
}
}
请注意,只有MyKey中的哈希码发生了变化,现在当我们运行代码时,结果是
100000
tiempo: 47 mls
现在有一个令人难以置信的更好的表现,只需稍作改动。一种非常常见的做法是返回哈希码乘以素数(在本例中为31),使用在equals方法中使用的相同哈希码成员,以确定两个对象是否相同(在这种情况下只是str)。
获得最佳性能的关键是在充当HashMap中的Key的类中选择最佳hashcode
和equals
实现。
答案 2 :(得分:2)
@Eran的回答解释了HashMap
平均给出O(1)
&#34;&#34;的原因。他没有说明的关键点是HashMap
会自动调整哈希数组的大小,并在数组大小与条目数之比超过(可配置的)加载因子时重新分配哈希链。
如果哈希函数和键表现良好,则哈希链很短,搜索链的平均时间为O(1)
。
有三种情况会导致分析失败:
如果散列函数较差,您可能会发现许多/大多数键具有相同的散列值,并最终位于相同的散列链中。在最糟糕的情况下,可能导致O(N)
次搜索时间。
应用程序可能必须处理一组偶然都具有相同哈希码的键。如果某人故意选择散列到相同哈希码的键以使某些哈希链变长,您也可以得到它。 (想想......拒绝服务攻击。)
HashMap
对象的散列数组受Java阵列的最大大小限制。当阵列达到最大大小时,不再可以调整大小。因此,对于非常大的地图(数十亿条目),查找时间从O(1)
切换到O(N)
(非常小C
)。
这些场景中的每一个都可能是一个问题,因此在Java 8中,它们对HashMap
的实现方式进行了一些重大更改。在Java 8版本中,如果哈希链足够长,HashMap
将从使用链的链表切换到使用平衡二叉树。这会将最差情况行为从O(N)
更改为O(logN)
次查找。需要注意的是,这只适用于链中的键全部实现Comparable
。