HashMap实现
符号表
符号表概述
我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息。
符号表有时也被称为字典。键就是单词,值就是单词对应的定义,发音和词源。
符号表有时又叫做索引。键就是术语,值就是书中该术语出现的所有页码。
- 无序符号表HashMap
- 有序符号表TreeMap
HashMap
概述
如果所有的键都是小整数,我们可以用一个数组来实现的符号表,将键作为数组的索引,而数组中对应的位置存储键关联的值。
哈希表是这种简单方法的扩展,并且能够处理更加复杂类型的键。我们需要用哈希函数将键转换成数组的索引。
哈希表的核心算法可以分为两步。
- 用哈希函数将键转换为数组中的一个索引。理想情况下不同的键都能转换成不同的索引值。当然这只是理想情况下,所以我们需要处理两个或者多个键都散列到相同索引值的情况 (哈希碰撞)。
- 处理碰撞冲突。
a. 开放地址法
线性探测法, 平方探测法, 再散列法…
b. 拉链法
哈希函数
一个优秀的 hash 算法,满足以下特点:
正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
只要逆向通过哈希得到一个明文,这个明文的哈希是我们给定的哈希,就是逆向。
输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
我们可以简单地把哈希函数理解为在模拟随机映射,然后从随机映射的角度去理解哈希函数。
在数据结构中,对速度比较重视,对抗碰撞性不太看重。所以对哈希函数的要求没那么高,
只要满足下面两点就可以了。
计算速度快。
Hash值平均分布
处理碰撞冲突-拉链法
拉链法:如果我们想在常数时间复杂度内, 完成哈希表的增删查操作,那么我们就得控制链表的平均长度不超过某个值。
这个值我们称之为加载因子,也就是链表平均长度可以达到的最大值。因此,当元素个数达到一定的数目(threshold = size * table.length)的时候,我们就需要对数组进行扩容。
实现API
极简处理:
键不能为null。如果键为null,我们会抛出 NullPointerException.
值不能为null。我们这么规定的原因是:当键不存在的时候,get()方法会返回null。这样做有个好处,我们可以调用get()方法,看其返回值是否为 null 来判断键是否存在哈希表中。
缓存hash值。如果散列值计算很耗时 (比如长字符串)。那么我么可以在结点中用一个 hash 变量来保存它计算的 hash 值。Java 中的 String 就是这样做的。
当数组达到最大值时,并且链表的平均长度达到了最大值。这种情况,我们就破坏加载因子的限制,直接添加元素。
属性,构造方法,内部类Entry
这段代码我注意到的地方:
-
在这里,将Entry设定成static内部类的原因:
1:Entry不依赖于外部类存在。
2:Entry[] table 数组不能是泛型数组,如果Entry是普通成员内部类,Entry依赖于外部类对象存在。外部类对象可能是带泛型的,编译器会报错。 -
为什么要让table的长度是2的整数次幂。
因为key散列的数组的索引,是通过 hash(key) % table.length 求来的,% 是个很麻烦的计算,如果table.length = 2 ^ n, 这个% 就可以简化成 hash & (table.length - 1);
- 如何保证把外部传入的initialCapacity转换成大于等于 initialCapacity的最小的整数次幂
就是下面这串代码***calculateCapacity(int initialCapacity)***,建议大家自己算一下,比如传入个7,二进制是 0000 0111
initialCapacity: 0000 0111
capacity : 0000 0110
capacity |= capacity >> 1 :
0000 0110
0000 0011
等于 0000 0111
capacity + 1 = 8
public class MyHashMapV2<K, V> {
//属性
private static final int DEFAULT_ARRAY_SIZE = 16;
private static final int MAX_ARRAY_SIZE = 2^30;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private Entry[] table;
private int size;
private int threshold;
private float loadFactor;
//内部类
/*
在这里,将Entry设定成static内部类的原因:
1:Entry不依赖于外部类存在。
2:Entry[] table 数组不能是泛型数组,如果Entry是普通成员内部类,Entry依赖于外部类对象存在。外部类对象可能是带泛型的,编译器会报错。
*/
private static class Entry {
Object key;
Object value;
Entry next;
int hash;
public Entry(Object key, Object value, Entry next, int hash) {
this.key = key;
this.value = value;
this.next = next;
this.hash = hash;
}
@Override
public String toString() {
return key +
" = " + value;
}
}
//构造方法
public MyHashMapV2() {
this(DEFAULT_ARRAY_SIZE, DEFAULT_LOAD_FACTOR);
}
public MyHashMapV2(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public MyHashMapV2(int initialCapacity, float loadFactor){
if(initialCapacity <= 0 || initialCapacity > MAX_ARRAY_SIZE) throw new IllegalArgumentException("initialCapacity = " + initialCapacity);
if(loadFactor <= 0) throw new IllegalArgumentException("loadFactor = " + loadFactor);
this.loadFactor =loadFactor;
int capacity = calculateCapacity(initialCapacity);
/*table = new MyHashMapV2.Entry[capacity]; Entry是普通成员内部类时,应该这样用*/
table = new Entry[capacity];
threshold = (int)(table.length * loadFactor);
}
/*1: 为什么要让table的长度是2的整数次幂。因为key散列的数组的索引,是通过 hash(key) % table.length 求来的,% 是个很麻烦的计算,如果table.length = 2 ^ n, 这个% 就可以简化成 hash & (table.length - 1);
2: 如何保证把外部传入的initialCapacity转换成大于等于initialCapacity的最小的整数次幂,就是下面这串代码,建议大家自己算一下,比如传入个7,二进制是 0000 0111
initialCapacity: 0000 0111
capacity : 0000 0110
capacity |= capacity >> 1 :
0000 0110
0000 0011
等于 0000 0111
capacity + 1 = 8
*/
private int calculateCapacity(int initialCapacity) {
int capacity = initialCapacity - 1;
capacity |= capacity >> 1;//保证前两位是1
capacity |= capacity >> 2;//保证前四位是1
capacity |= capacity >> 4;//前八位
capacity |= capacity >> 8;//16
capacity |= capacity >> 16;//32
return capacity + 1;
}
V put(K key, V value)
我注意到的地方:
- 判断是否需要扩容,采用size >= threshold方法,考量如下:
* 1:假设说table.length 是16, factor 是0.75,threshold是 12,那就是如果size >= 12(整个哈希表存了12个键值对及以上),就要扩一次.
* 因为考虑到为了让时间和空间平衡,链表并不一定平均分布,loadFactor表示平均链表长度,当size == threshold时,可能有些链已经很长了
* 2:为什么是 size>= threshold, 而不是 size == threshold, 因为如果是多线程并发,可能会出现size > threshold的情况。
public V put(K key, V value){
if(key == null || value == null){throw new IllegalArgumentException("key or value cannot be null");}
int hash = hash(key);
int index = indexFor(hash, table.length);
//遍历链表,判断key是否存在在链表中,如果存在,更新键值对,并返回历史value
for(Entry node = table[index]; node != null; node = node.next){
if(node.hash == hash && (node.key == key || key.equals(node.key))) {
V oldValue = (V)node.value;
node.value = value;
return oldValue;
}
}
//key没在链表中,或者链表table[inded] == null
addEntry(key, value, hash, index);
return null;
}
private void addEntry(K key, V value, int hash, int index) {
/*判断是否需要扩容,采用size >= threshold方法,考量如下:
* 1:假设说table.length 是16, factor 是0.75,threshold是 12,那就是如果size >= 12(整个哈希表存了12个键值对及以上),就要扩一次.
* 因为考虑到为了让时间和空间平衡,链表并不一定平均分布,loadFactor表示平均链表长度,当size == threshold时,可能有些链已经很长了
* 2:为什么是 size>= threshold, 而不是 size == threshold, 因为如果是多线程并发,可能会出现size > threshold的情况。
* */
if(size >= threshold){
//如果数组已经是最大容量,那就破坏加载因子
if(table.length == MAX_ARRAY_SIZE){
threshold = Integer.MAX_VALUE;
}
else {
int newCapacity = table.length << 1;
grow(newCapacity);
index = indexFor(hash, table.length); //table扩容以后,key的hash值没有变,但是因为table长度变了,所以index会发生变化,
}
}
//链表内插入键值对,采用头插法
Entry node = new Entry(key, value, table[index], hash);
table[index] = node;
size++;
}
private void grow(int newCapacity) {
/*不需要判断,因为在addEntry判断是否需要扩容得到时候,已经判断了。
if(newCapacity > MAX_ARRAY_SIZE){
newCapacity = MAX_ARRAY_SIZE;
}*/
Entry[] newTable = new Entry[newCapacity];
//因为链表中的Entry可能hash值不同,在table没有扩容前,可能在当前索引下,但是扩容以后,也许索引会发生变化,所以要对每一个键值对重新散列。
for(int i = 0; i < table.length; i++){
Entry node = table[i];
while(node != null){
int index = indexFor(node.hash, newCapacity);
Entry next = node.next;
newTable[i] = node;
node.next = newTable[i];
node = next;
}
}
/*需要重新散列,所以不能直接把数组这么赋值过去
for(int i = 0; i < table.length; i++){
newTable[i] = table[i];
}*/
table = newTable;
threshold = (int)(table.length * loadFactor);
}
private int indexFor(int hash, int length) {
return hash & (length - 1);
}
private int hash(K key){
int hash = key.hashCode();
return (hash << 16) ^ (hash >> 16);
}
V delete(K key)
public V delete(K key){
if(key == null){throw new IllegalArgumentException("key cannot be null");}
int hash = hash(key);
int index = indexFor(hash, table.length);
Entry parent = null;
for(Entry node = table[index]; node != null; node = node.next){
if(hash == node.hash && (key == node.key || key.equals(node.key))){
if(parent == null) {
table[index] = table[index].next;
size--;
return (V)node.value;
}
else {
parent.next = node.next;
size--;
return (V)node.value;
}
}
parent = node;
}
return null;
}
V get(K key)
public V get(K key){
if(key == null){throw new IllegalArgumentException("key cannot be null");}
int hash = hash(key);
int index = indexFor(hash, table.length);
for(Entry node = table[index]; node != null; node = node.next){
if(hash == node.hash && (key == node.key || key.equals(node.key))){
return (V)node.value;
}
}
return null;
}
Set keys()
Set<K> keys() {
Set<K> set = new LinkedHashSet<>();
for(Entry node: table){
while(node != null){
set.add((K)node.key);
node = node.next;
}
}
return set;
}
其他方法(clear, contains, isEmpty, size)
public void clear(){
for(int i = 0; i < table.length; i++){
table[i] = null;
}
size = 0;
}
public boolean contains(K key){
return (get(key) != null);
}
public boolean isEmpty(){
return size == 0;
}
int size() {
return size;
}