HashMap深度了解

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亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。

  • HashMap:

与 HashSet 对应,也是无序的,O(1)。

  • LinkedHashMap:

这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。
以插入的顺序排序。

  • TreeMap:

是有序的,本质是用二叉搜索树来实现的。以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.


TOP K 问题(高频面试题)

给一组词,统计出现频率最高的 k 个。
比如说 “I love leetcode, I love coding” 中频率最高的 2 个就是 I 和 love 了。

思路

统计下所有词的频率,然后按频率排序取最高的前 k 个呗。

细节:

用 HashMap 存放单词的频率,用 minHeap/maxHeap 来取前 k 个。

实现:

  1. 建一个 HashMap ,遍历整个数组,相应的把这个单词的出现次数 + 1.
    这一步时间复杂度是 O(n).

  2. 用 size = k 的 minHeap 来存放结果,定义好题目中规定的比较顺序
    a. 首先按照出现的频率排序;
    b. 频率相同时,按字母顺序。

  3. 遍历这个 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;
    }
}

你可能感兴趣的:(java,java)