Java 8中的散列值优化函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这段代码叫做“扰动函数”。大家都知道上面代码里的key.hashCode()调用的是key键值类型自带的哈希函数,返回int型散列值。
理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
与 HashSet 对应,也是无序的,O(1)。
这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。
以插入的顺序排序。
是有序的,本质是用二叉搜索树来实现的。以key的hashCode来排序。
1. 如果不同的元素算出了相同的哈希值,那么该怎么存放呢?
答:这就是哈希碰撞,即多个 key 对应了同一个桶。防止哈希碰撞的最有效方法,就是扩大哈希值的取值空间。
2. HashMap 中是如何保证元素的唯一性的呢?即相同的元素会不会算出不同的哈希值呢?
答:通过 hashCode() 和 equals() 方法来保证元素的唯一性。如果出现哈希碰撞,equals()可以区分。
put(key,value) 首先hash(key)获得数组下标,再调用equals()方法检查Key是否相同。如果key相同则,更新value值。如果不同,则在链表末尾插入Entry(即Entry->next = 新Entry)
3. 如果 pairs(key相同) 太多,buckets(存储空间-桶) 太少怎么破?
答:Rehasing. 也就是碰撞太多的时候,会把数组扩容至两倍(默认)。所以这样虽然 hash 值没有变,但是因为数组的长度变了,所以算出来的 index 就变了,就会被分配到不同的位置上了,就不用挤在一起了,小伙伴们我们江湖再见~
那什么时候会 rehashing 呢?
也就是怎么衡量桶里是不是足够拥挤要扩容了呢?
答:load factor. 即用 pair 的数量除以 buckets 的数量,也就是平均每个桶里装几对。负载因子为 0.75f,如果超过了这个值就会 rehashing.
==和String重写后的equal()
答:
我们常用的比较大小的符号之 ==
如果是 primitive type,那么 == 就是比较数值的大小;
如果是 reference type,那么就比较的是这两个 reference 是否指向了同一个 object。
equal()是比较数值或者指向的值是否相同。
若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
注: 因为equals在objectclass中是通过“==”实现的。equals() 方法就是比较这两个 references 是否指向了同一个 object.所以如果需要比较String对象的值是否相同,需要重写该方法。
但无论是怎么实现的,都需要遵循文档上的约定,也就是对不同的 object 会返回唯一的哈希值。
一般来说哈希冲突有两大类解决方式
1. Separate chaining
2. Open addressing
Java 中采用的是第一种 Separate chaining,即在发生碰撞的那个桶后面再加一条“链”来存储,那么这个“链”使用的具体是什么数据结构,不同的版本稍有不同:
在 JDK1.6 和 1.7 中,是用链表存储的,这样如果碰撞很多的话,
就变成了在链表上的查找,worst case 就是 O(n);
在 JDK 1.8 进行了优化,当链表长度较大时(超过 8),会采用红黑树来存储,
这样大大提高了查找效率。
为啥用8?因为通常情况下,链表长度很难达到8,但是特殊情况下链表长度为8,
哈希表容量又很大,造成链表性能很差的时候,只能采用红黑树提高性能,这是一种应对策略。
第二种方法 open addressing 也是非常重要的思想,因为在真实的分布式系统里,有很多地方会用到 hash 的思想但又不适合用 seprate chaining。
这种方法是顺序查找,如果这个桶里已经被占了,那就按照“某种方式”继续找下一个没有被占的桶,直到找到第一个空的。
这种方式叫做 Linear probing 线性探查,就像上图所示,一个个的顺着找下一个空位。当然还有其他的方式,比如去找平方数,或者 Double hashing.
给一组词,统计出现频率最高的 k 个。
比如说 “I love leetcode, I love coding” 中频率最高的 2 个就是 I 和 love 了。
统计下所有词的频率,然后按频率排序取最高的前 k 个呗。
用 HashMap 存放单词的频率,用 minHeap/maxHeap 来取前 k 个。
建一个 HashMap
这一步时间复杂度是 O(n).
用 size = k 的 minHeap 来存放结果,定义好题目中规定的比较顺序
a. 首先按照出现的频率排序;
b. 频率相同时,按字母顺序。
遍历这个 map,如果
a. minHeap 里面的单词数还不到 k 个的时候就加进去;
b. 或者遇到更高频的单词就把它替换掉。
第一步是 O(n),第三步是 nlog(k),所以加在一起时间复杂度是 O(nlogk).
用了一个额外的 heap 和 map,空间复杂度是 O(n).
class Solution {
public List topKFrequent(String[] words, int k) {
// Step 1 用map存放词语和频率
Map map = new HashMap<>();
for (String word : words) {
Integer count = map.getOrDefault(word, 0);
count++;
map.put(word, count);
}
// Step 2 PriorityQueue无界优先级队列 按题目定义比较规则
PriorityQueue> minHeap = new PriorityQueue<>(k+1, new Comparator>() {
@Override
public int compare(Map.Entry e1, Map.Entry e2) {
if(e1.getValue() == e2.getValue()) {
return e2.getKey().compareTo(e1.getKey());
}
return e1.getValue().compareTo(e2.getValue());
}
});
// Step 3 遍历
List res = new ArrayList<>();
for(Map.Entry entry : map.entrySet()) {
minHeap.offer(entry);
if(minHeap.size() > k) {
minHeap.poll();
}
}
while(!minHeap.isEmpty()) {
res.add(minHeap.poll().getKey());
}
Collections.reverse(res);
return res;
}
}