map是一种key、value形式的键值对,将hash表和map结合即形成了HashMap。 在Java中HashMap的数据是以Entry数组的形式存放的,HashMap通过对key进行hash运算得到一个数组下标,然后将数据存放到Entry数组对应的位置,又因为不同的key进行hash运算可能会得到一个相同的数组下标,为了解决碰撞覆盖冲突,所以Entry本身又是一个链表的结构,即以后不同的key相同数组下标的数据的next会被赋值为已存在Entry链表,新的Entry会替换数组值。
今天上课,老师布置了一道java题,是统计文件中的重复单词;我想到用hashMap的方法,觉得这样的话时间复杂度为O(n);
但是被老师否决了,说是还需要遍历;后来我仔细看了一下hashmap的源代码,觉得老师的说法并不是正确的,应该就是o(n);
因为hashMap本身就是用空间来换时间的方法;它可以根据key,生成相应的hashcode,然后根据hashcode,利用indexof直接定位到链表的某个位置;
当然这是和其中的hash因子有关的,如果产生冲突必然要进行再处理;但是基于本题,冲突的事情应该是不需要考虑的;
老师的意思是现将文件中的所有单词进行排序,然后在进行统计;这样的方法最低的时间复杂度也是需要o(nlogn)的;所以我觉得还是hashMap效率更高!
最后附上hashMap的源代码,如有大神有更完美的见解,欢迎赐教,本菜虚心学习之!
源码如下:
HashMap 的put方法的源码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
V put(K key, V value) {
if
(key ==
null
)
return
putForNullKey(value);
// HashMap接收key为null的数据
int
hash = hash(key.hashCode());
//对key的hashCode再进行hash运算
int
i = indexFor(hash, table.length);
//根据hash值和entry数组的大小计算出新增数据应该存放的数组位置
for
(Entry<k,v> e = table[i]; e !=
null
; e = e.next) {
// for循环遍历找到的数组下标的entry,如果hash值和key都相等,则覆盖原来的value值
Object k;
if
(e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(
this
);
return
oldValue;
}
}
modCount++;
//如果上面for循环没有找到相同的hash和key,则增加一个entry
addEntry(hash, key, value, i);
return
null
;
}</k,v>
|
1
2
3
4
5
6
7
|
void
addEntry(
int
hash, K key, V value,
int
bucketIndex) {
Entry<k,v> e = table[bucketIndex];
//找到下标的entry
//new 一个新的entry,赋值给当前下标数组
table[bucketIndex] =
new
Entry<k,v>(hash, key, value, e);
if
(size++ >= threshold)
resize(
2
* table.length);
}</k,v></k,v>
|
1
2
3
4
5
6
|
Entry(
int
h, K k, V v, Entry<k,v> n) {
value = v;
next = n;
//即将原来数组下标对应的entry赋值给新的entry的next
key = k;
hash = h;
}</k,v>
|
(1)hash值相同且key相等数据将被覆盖。
(2)添加新的entry时,将已存在的数据下标的entry(可能是null)赋值给新entry的next,新entry将替换原数组下标的值。
HashMap的get方法源码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
V get(Object key) {
//key为null时特别处理
if
(key ==
null
)
return
getForNullKey();
int
hash = hash(key.hashCode());
//indexFor(hash, table.length) 根据hash值和数组长度计算出下标,然后遍历Entry链表
for
(Entry<k,v> e = table[indexFor(hash, table.length)];
e !=
null
;
e = e.next) {
Object k;
if
(e.hash == hash && ((k = e.key) == key || key.equals(k)))
return
e.value;
}
return
null
;
}</k,v>
|
总结
- 一个对象当HashMap的key时,必须覆盖hashCode()和equals()方法,hashCode()的返回值尽可能的分散。
- 当HashMap的entry的数组足够大,key的hash值足够分散时,即是可以实现一个entry数组下标最多只对应了一个entry,此时get方法的时间复杂度可以达到O(1)。
- 在数组长度和get方法的速度上要达到一个平衡。数组比较长碰撞出现的概率就比较小,所以get方法获取值时就比较快,但浪费了比较多的空间;当数组长度没有冗余时,碰撞出现的概率比较大,虽然节省了空间,但会牺牲get方法的时间。
- HashMap有默认的装载因子loadFactor=0.75,默认的entry数组的长度为16。装载因子的意义在于使得entry数组有冗余,默认即允许25%的冗余,当HashMap的数据的个数超过12(16*0.75)时即会对entry数组进行第一次扩容,后面的再次扩容依次类推。
- HashMap每次扩容一倍,resize时会将已存在的值从新进行数组下标的计算,这个是比较浪费时间的。在平时使用中,如果能估计出大概的HashMap的容量,可以合理的设置装载因子loadFactor和entry数组初始长度即可以避免resize操作,提高put的效率。
- HashMap不是线程安全的,多线程环境下可以使用Hashtable或ConcurrentHashMap。