深刻理解hashmap(二)理论篇

以前有过一篇介绍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.

对于数组扩容来讲,其实没啥好说的,你们都会,可是哈希表的扩容还涉及到从新计算哈希值,这样数据在扩容 之后的哈希表里的位置 和以前的位置 就有可能不一样。这个步骤叫作从新计算哈希值。

因此动态扩容是一个比较耗时的操做:从新申请新的数组空间,从新申请计算哈希值(也就是得出在数组中的位置),最后 把老数组的数据拷贝到新数组(解决哈希冲突的链表里的值也可能要搬迁到新数组里面)

java中的LinkedHashMap是用链表实现的哈希表吗,否则为啥带个Linked的关键字?

废话,固然不是。哈希表的基础存储必定是用数组,不然没法实现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;
    }
复制代码

相关文章
相关标签/搜索