你们好,本篇文章是《齐姐说数据结构》系列的第三篇,更多数据结构和算法的文章已经整理在个人 Github 上了:https://github.com/xiaoqi6666...java
HashMap 是不管在工做仍是面试中都很是常见常考的数据结构。node
好比 Leetcode 第一题 Two Sum 的某种变种的最优解就是须要用到 HashMap 的,高频考题 LRU Cache 是须要用到 LinkedHashMap 的。git
HashMap 用起来很简单,底层实现也不复杂,先来看几道常见的面试题吧。相信你们多多少少都能回答上来一点,不清楚的地方就仔细阅读本文啦~这篇文章带你深挖到 HashMap 的老祖宗,保证吊打面试官程序员
- == 和 equals() 的区别?
- 为何重写 equals() 就必需要重写 hashCode()?
- Hashtable, HashSet 和 HashMap 的区别和联系
- 处理 hash 冲突有哪些方法?Java 中用的哪种?为何?另外一种方法你在工做中用过吗?在什么状况下用得多?
- 徒手实现一个 HashMap 吧
本文分如下章节:github
在讲 Map 以前,咱们先来看看 Set。面试
集合的概念咱们初中数学就学过了,就是里面不能有重复元素,这里也是同样。算法
Set 在 Java 中是一个接口,能够看到它是 java.util 包中的一个集合框架类,具体的实现类有不少:数组
其中比较经常使用的有三种:安全
HashSet: 采用 Hashmap 的 key 来储存元素,主要特色是无序的,基本操做都是 O(1) 的时间复杂度,很快。数据结构
LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特色就是既拥有了 O(1) 的时间复杂度,又可以保留插入的顺序。
TreeSet: 采用红黑树结构,特色是能够有序,能够用天然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
Map 是一个键值对 (Key - Value pairs),其中 key 是不能够重复的,毕竟 set 中的 key 要存在这里面。
那么与 Set 相对应的,Map 也有这三个实现类:
HashMap: 与 HashSet 对应,也是无序的,O(1)。
LinkedHashMap: 这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,因此既拥有 HashMap 的全部特性还能有顺序。
TreeMap: 是有序的,本质是用二叉搜索树来实现的。
对于 HashMap 中的每一个 key,首先经过 hash function 计算出一个 hash 值,这个hash值就表明了在 buckets 里的编号,而 buckets 其实是用数组来实现的,因此把这个数值模上数组的长度获得它在数组的 index,就这样把它放在了数组里。
那么这里有几个问题:
若是不一样的元素算出了相同的哈希值,那么该怎么存放呢?
答:这就是哈希碰撞,即多个 key 对应了同一个桶。
HashMap 中是如何保证元素的惟一性的呢?即相同的元素会不会算出不一样的哈希值呢?
答:经过 hashCode() 和 equals() 方法来保证元素的惟一性。
若是 pairs 太多,buckets 太少怎么破?
答:Rehasing. 也就是碰撞太多的时候,会把数组扩容至两倍(默认)。因此这样虽然 hash 值没有变,可是由于数组的长度变了,因此算出来的 index 就变了,就会被分配到不一样的位置上了,就不用挤在一块儿了,小伙伴们咱们江湖再见~
那何时会 rehashing 呢?也就是怎么衡量桶里是否是足够 拥挤要扩容了呢?
答:load factor. 即用 pair 的数量除以 buckets 的数量,也就是平均每一个桶里装几对。Java 中默认值是 0.75f,若是超过了这个值就会 rehashing.
若是 key 的 hashCode() 值相同,那么有多是要发生 hash collision 了,也有多是真的遇到了另外一个本身。那么如何判断呢?继续用 equals() 来比较。
也就是说,
hashCode() 决定了 key 放在这个桶里的编号,也就是在数组里的 index;equals() 是用来比较两个 object 是否相同的。
那么该如何回答这道<span style="color:black;font-weight:bold;">经典面试题</span>:
<span style="color:blue;font-weight:bold;">为何重写 equals() 方法,必定要重写 hashCode() 呢?
答:首先咱们有一个假设:任何两个 object 的 hashCode 都是不一样的。
那么在这个条件下,有两个 object 是相等的,那若是不重写 hashCode(),算出来的哈希值都不同,就会去到不一样的 buckets 了,就迷失在茫茫人海中了,再也没法相认,就和 equals() 条件矛盾了,证毕。
撒花~~🎉🎉🎉
接下来咱们再对这两个方法一探究竟:
其实 hashCode() 和 equals() 方法都是在 Object class 这个老祖宗里定义的,Object 是全部 Java 中的 class 的鼻祖,默认都是有的,甩不掉的。
那既然是白给的,咱们先来看看大礼包里有什么,谷歌 Object 的 Oracle 文档:
因此这些方法都是能够直接拿来用的呢~
回到 hashCode() 和 equals(),那么若是这个新的 class 里没有重写 (override) 这两个方法,就是默认继承 Object class 里的定义了。
那咱们点进去来看看 equals() 是怎么定义的:
记笔记:
equals()
方法就是比较这两个 references 是否指向了同一个 object.
嗯???你在逗我吗??那岂不是和 ==
同样了??
补充:
咱们经常使用的比较大小的符号之==
若是是 primitive type,那么 == 就是比较数值的大小;
若是是 reference type,那么就比较的是这两个 reference 是否指向了同一个 object。再补充:
Java 的数据类型能够分为两种:
Primitive type 有且仅有8种:byte, short, int, long, float, double, char, boolean.
其余都是 Reference type.
因此虽然 Java 声称 “Everything is object”,可是仍是有非 object 数据类型的存在的。
我不信,我要去源码里看看它是怎么实现的。
哈,还真是的,绕了这么半天,equals()
就是用 ==
来实现的!
那为何还弄出来这么个方法呢?
<span style="color:blue;font-weight:bold;">答:为了让你 override~
好比通常来讲咱们比较字符串就是想比较这两个字符串的内容的,那么:
str1 = “tianxiaoqi”; str2 = new String(“tianxiaoqi”); str1 == str2; // return false str1.equals(str2); // return true
由于 String 里是重写了 equals() 方法的:
老祖宗留给你就是让你本身用的,若是你不用,那人家也提供了默认的方法,也是够意思了。
好了,咱们再去看 hashCode() 的介绍:
那至于 hashCode() 返回的到底是什么,和本文关联不太大,有兴趣的同窗能够看参考这篇文章参考文章"),结论就是:
返回的并不必定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。
但不管是怎么实现的,都须要遵循文档上的约定,也就是对不一样的 object 会返回惟一的哈希值。
### 哈希冲突详解
通常来讲哈希冲突有两大类解决方式
- Separate chaining
- Open addressing
Java 中采用的是第一种 Separate chaining
,即在发生碰撞的那个桶后面再加一条“链”来存储,那么这个“链”使用的具体是什么数据结构,不一样的版本稍有不一样:
在 JDK1.6 和 1.7 中,是用 链表存储的,这样若是碰撞不少的话,就变成了在链表上的查找,worst case 就是 O(n);在 JDK 1.8 进行了优化,当链表长度较大时(超过 8),会采用红黑树来存储,这样大大提升了查找效率。
(话说,这个还真的喜欢考,已经在屡次面试中被问过了,还有面试官问为何是超过“8”才用红黑树🤔)
第二种方法 open addressing
也是很是重要的思想,由于在真实的分布式系统里,有不少地方会用到 hash 的思想但又不适合用 seprate chaining
。
这种方法是顺序查找,若是这个桶里已经被占了,那就按照“某种方式”继续找下一个没有被占的桶,直到找到第一个的。
空的
如图所示,John Smith 和 Sandra Dee 发生了哈希冲突,都被计算到 152 号桶,因而 Sandra 就去了下一个空位 - 153 号桶,固然也会对以后的 key 发生影响:Ted Baker 计算结果本应是放在 153 号的,但鉴于已经被 Sandra 占了,就只能再去下一个空位了,因此到了 154 号。
这种方式叫作 Linear probing
线性探查,就像上图所示,一个个的顺着找下一个空位。固然还有其余的方式,好比去找平方数,或者 Double hashing.
每种数据结构的基本操做都无外乎<span style="color:orangered;font-weight:bold;">增删改查</span>这四种,具体到 HashMap 来讲,
- 增:put(K key, V value)
- 删:remove(Object key)
- 改:仍是用的 put(K key, V value)
- 查:get(Object key) / containsKey(Object key)
细心的同窗可能发现了,为何有些 key 的类型是 Object,有些是 K 呢?这还不是由于 equals()...
这是由于,在 get/remove 的时候,不必定是用的同一个 object。
还记得那个 str1 和 str2 都是田小齐的例子吗?那好比我先 put(str1, value),而后用 get(str2) 的时候,也是想要到 tianxiaoqi 对应的 value 呀!不能由于我换了身衣服就不认得我了呀!因此在 get/remove 的时候并无很限制 key 的类型,方便另外一个本身相认。
其实这些 API 的操做流程大同小异,咱们以最复杂的 put(K key, V value) 来说:
代码以下:
public V put(K key, V value) { int index = getIndex(key); Node<K, V> node = array[index]; Node<K, V> head = node; while (node != null) { // 原来有这个 key,仅更新值 if (checkEquals(key, node)) { V preValue = node.value; node.value = value; return preValue; } node = node.next; } // 原来没有这个 key,新加这个 node Node<K, V> newNode = new Node(key, value); newNode.next = head; array[index] = newNode; return null; }
至于更多的细节好比加一些 rehashing 啊,load factor 啊,你们能够参考源码。
读完源码你们能够作作 Leetcode 706 题练手,so easy~
### 与 Hashtable 的区别
这是一个年龄暴露贴,HashMap 与 Hashtable 的关系,就像 ArrayList 与 Vector,以及 StringBuilder 与 StringBuffer。
Hashtable 是早期 JDK 提供的接口,HashMap 是新版的;
它们之间最显著的区别,就是 Hashtable 是线程安全的,HashMap 并不是线程安全。
这是由于 Java 5.0 以后容许数据结构不考虑线程安全的问题,由于实际工做中咱们发现没有必要在数据结构的层面上上锁,加锁和放锁在系统中是有开销的,内部锁有时候会成为程序的瓶颈。
因此 HashMap, ArrayList, StringBuilder 再也不考虑线程安全的问题,性能提高了不少,固然,线程安全问题也就转移给咱们程序员了。
另一个区别就是:HashMap 容许 key 中有 null 值,Hashtable 是不容许的。这样的好处就是能够给一个默认值。
在算法面试中,有关 HashMap 的算法题也很常见,好比有名的 Top K 问题,还有 LRU Cache 问题,这两道题都是很是高频的考题,以后也会讲到,还请你们继续关注我吧!
若是你喜欢这篇文章,记得给我点赞留言哦~大家的支持和承认,就是我创做的最大动力,咱们下篇文章见!
我是小齐,纽约程序媛,终生学习者,天天晚上 9 点,云自习室里不见不散!
更多干货文章见个人 Github: https://github.com/xiaoqi6666...