哈希表:
面试中出场频率很高,工作中的出场频率也很高。
- 哈希表是基于数组演化而来,哈希表高效的秘密就在于数组的随机访问能力。哈希是用空间换时间。
- 针对输入的每个数字(key)都要进行一个数学运算的变换,把key经过一定的数学变换转成下标的过程,称为“哈希函数”/“散列函数”。哈希函数有一个重要特点:两个相同的key,计算得到的hash值一定相同。
- 哈希函数:key % 数组长度 = 下标的值
- 如果两个不同的key,通过哈希函数计算之后得到了相同的hash值,此时这种情况称为“哈希冲突”/“哈希碰撞”。
那么要如何处理哈希冲突?
-
尽量避免(降低冲突概率):选择更合适的hash函数。
例如:如果把数组长度设定为素数,那么冲突改路就会降低不少。负载因子:哈希表中保存的元素个数 / 数组的长度。
负载因子越小,冲突概率就越小。 -
正面应对(当冲突了之后通过一定的方式解决冲突)
a). 闭散列:发现冲突之后,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么就在当前冲突位置周围找到一个空闲位置来插入元素。- 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
- 二次探测:从发生冲突的位置开始,以2的次方进行探测,直到找到下一个空位置为止。
b). 开散列/哈希桶:首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶的元素通过一个单链表链接起来,个链表的头结点存储在哈希表中。
通俗的说:开散列的数组中的每个节点相当于一个链表的头结点,当出现hash冲突的时候就把新的元素插入到链表中。
无论是第一种方式还是第二种方式,当哈希冲突比较严重的时候,插入查找删除的效率仍然会很低。
如果冲突变得特别严重:
- 扩容(降低负载因子)成本比较高(不太推荐)。
- 针对开散列来说(链表),把链表替换成更高效的数据结构。比如二叉搜索树或者哈希表。
哈希函数一般如何来计算呢?
- 如果key为整数,hash值计算方式主要就是通过除留余数的方式,一般会搭配一些其他的数学。
- 如果key为字符串,如何计算hash值呢?
md5 sha1 常用的字符串哈希算法。
md5 的重要特点:
1.无论输入的字符串有多长,最终得到的md5值都是固定长度的。
2.如果有两个字符串,大部分内容都相同,只有某个字符不相同,此时计算生成的md5值也会差别很大。(这一点就决定了md5算出的hash值最终冲突概率比较小)
3.通过字符串来计算hash值,很高效,但是反之不行。(给定md5值,找出某个字符串的值是这个md5,这个计算量非常巨大,理论上是不可行的)。
sha1也同样具备。
哈希表就是HashMap/HashSet的底层实现。
Java中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方
法,而且要做到 equals 相等的对象,hashCode 一定是一致的。