以前有过一篇介绍java中hashmap使用的,深刻理解hashmap,比较侧重于 代码分析,没有从理论上分析hashmap,今天把hashmap的理论部分补充一下(以后应该还有两篇补充 一篇讲红黑树一篇讲多线程)。java
简单来讲 散列函数主要就是:将一个二进制串 经过必定的算法计算之后 获得一个新的二进制串。这个计算的方法就是散列函数。 也叫哈希函数,获得的值就是哈希值android
那么要设计一个散列函数还须要几个特性: 1.经过哈希值不能获得原始的值。 这个不少人都清楚,比方说咱们的密码都是md5之后存在服务器的,不然数据库被盗, 你们的密码就都完蛋了,这个md5 其实就是一种哈希算法。git
2.对于原始值来讲,由于计算机中的任何对象,都是一串二进制值,因此要求 哪怕是有一个bit的不一样,得出来的哈希值也 应该不一样。算法
3.知足上面2个条件之后,最好散列冲突的几率要小,而且这个算法的速度要快。数据库
那么哈希表和哈希函数的关系就显而易见:利用数组这种结构随机访问数据的时间复杂度为o(1)的优势,咱们将数据 通过哈希算法计算之后获得一个key值,这个key值就对应的数组的位置。 这样之后咱们查找数据 只要把数据计算出来 key值就能够获得想要数组的位置,天然查找的效率就是o(1)了。数组
因此哈希表其主要目的其实就是为了解决快速查找的问题。其应用场景也主要围绕这个功能展开。这里简单举个例子:缓存
1.负载均衡bash
最简单的负载均衡咱们能够想到,无非就是创建一张表,表里面 对应着 客户ip地址 和服务器ip地址。那这样每次有客户端请求 进来,咱们都去这个表里面查到对应应该分配的服务器ip,而后再把客户请求发到这个服务器ip上。那么很明显这样作 很是很差,第一这个表会无限大,消耗存储空间,第二 表大的时候查询效率也会变低,第三 服务器扩容之后处理起来很麻烦。服务器
那这里若是用散列函数来作就简单多了,咱们只要把客户ip地址 通过散列算法之后 得出一个值,而后对服务器的个数取模 就能够很快的创建这个 key-value关系。网络
更多的例子好比网络协议里面的crc校验,p2p的下载算法,甚至git中的commit id都是利用散列函数来作。
简单来讲,散列函数无论设计的有多优秀,散列冲突都必定没法避免。由于咱们容量是有限的。你们能够百度下抽屉原理, 举个例子,咱们有5个橘子,你只有4个抽屉,那你一定会有一个抽屉里面有2个橘子。
对于哈希算法也是同样,由于咱们哈希算法的出来的值是固定长度,因此确定数量是有限的,好比说md5出来的值 就是128个bit。固定长度。若是你有超过这个长度的数据要通过md5算法计算哈希值,那么确定至少会有重复的!
主要有两种方法,一种是开放寻址法(java中的ThreadLocalMap),一种是链表法(hashmap)。其中前者如今用的很少,有兴趣的同窗能够学学看。 咱们重点讲链表法。所谓链表法其实就是 在发生散列冲突的时候,把相同哈希值的数据存放在链表中。
链表你们都知道的,查找复杂度就是o(n)了,因此可想而知,若是你脸很差哈希冲突的次数过多,那咱们o(1)的 哈希表的查找效率就会降低到o(n),jdk新版本优化的hashmap就是优化了这个问题,当这个解决冲突的链表长度 大于8的时候,就会自动转成红黑树(二叉搜索树的一种),红黑树的查找效率是o(logn),你们以前看二分查找的 时候应该知道这个效率是很高的。查找大概42亿的数据也不过就32次左右。(红黑树后面咱们再单独讲)
通常而言,装载因子这个值越大,那么就意味着 对于一个哈希表来讲,若是元素过多的状况下,装载因子大的哈希表 空闲位置就越少,那么哈希冲突的几率就越大。对于大部分采用链表法来解决哈希冲突的 哈希表来讲,哈希冲突几率大 那么 就会致使 链表过长,这样查找的效率就会无限变低。
因此当咱们发现装载因子已通过大的时候,咱们就能够扩容这个哈希表,好比java里的hashmap扩容就是扩容一倍的大小, 比方说数组长度一开始16,扩容之后就变成32.
对于数组扩容来讲,其实没啥好说的,你们都会,可是哈希表的扩容还涉及到从新计算哈希值,这样数据在扩容 之后的哈希表里的位置 和以前的位置 就有可能不一样。这个步骤叫作从新计算哈希值。
因此动态扩容是一个比较耗时的操做:从新申请新的数组空间,从新申请计算哈希值(也就是得出在数组中的位置),最后 把老数组的数据拷贝到新数组(解决哈希冲突的链表里的值也可能要搬迁到新数组里面)
废话,固然不是。哈希表的基础存储必定是用数组,不然没法实现o(1)的查询效率。可是LinkedHashMap和普通hashmap最大的区别就是LinkedHashMap除了维护了一个数组之外,还维护了一个额外的双向链表。熟悉android的人都知道,不少开源的图片缓存框架里面的LRU算法都是用的LinkedHashMap来作数据结构,比方说对于一个图片缓存框架来讲,当缓存到达MAX的时候,就须要把 最近最少使用的图片移出缓存。而后把新来的放进缓存中,这个过程就是一个简单的LRU算法,而用LinkedHashMap则能够轻松的 完成这个需求(LinkedHashMap具体怎么调用就不说了,这里只说实现的原理以及和hashmap有什么不一样)
简单来讲,HashMap的 结构以下:
基础存储用数组,若是有一样的哈希值的数据那么就用单链表串起来。因此hashmap的存储基本结构就是四个字段
hash值---------->key------>value------->next
其中next指针就是用来 若是出现重复hash值哈希冲突的状况,用于构造单链表的。
而LinkedHashMap,为了实现LRU,还额外实现了一套双链表来保证。也就是说:
LinkedHashMap的基础存储也是用数组,只不过,除了用数组,他还单独维护了一个双向链表,这个双向链表就把 整个 (数组+单链表是java中哈希表的基础实现)给串起来,而实现LRU的数据结构就是 双向链表。
因此你们能够猜到LinkedHashMap的存储基本结构是
双链表中的before指针-->hash值---------->key------>value------->next---->双链表中的after指针。
额,生产环境上其实有不少地方都在用hashmap,你们能够自行搜索一下,这里仅奉送一个简单的leetcode算法题。
两数求和问题:
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
你能够假设每一个输入只对应一种答案,且一样的元素不能被重复利用。
示例:
给定 nums = [2, 7, 11, 15], target = 9
由于 nums[0] + nums[1] = 2 + 7 = 9 因此返回 [0, 1]
正常状况咱们想的是 双循环暴力遍从来解决,复杂度很容易就O(n2),其实用hashmap 能够很方便的解决 两数,三数,甚至是四数求和问题。 对于两数求和问题来讲,用map的复杂度就仅仅只有o(n)了。
既然想速度快一点只遍历一次,那么其实 既然已经肯定了target的值,那么遍历一次,咱们只要找一下 是否有target-数组[i]的值便可。
/**
* 算法核心思想:new一个map,map的key是数组元素的值,value是数组元素的位置也就是俗称的index
* 而后咱们遍历数组的时候 用target的值 --当前数组的值(wanted的值) 就是咱们想要的值。若是在map里面找到了,
* 那么就直接返回当前数组的index和 map里面这个wanted的值的value(value就是数组的index)便可。
*
* 若是map里找不到这个wanted的值,那么就把当前这个数组的元素放到map里面便可
* @return
*/
public int[] twoSum(int[] nums, int target) {
Map map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
int wanted = target - nums[i];
if (map.containsKey(wanted)) {
return new int[]{i, (int) map.get(wanted)};
}
map.put(nums[i], i);
}
return null;
}
复制代码