@TOC
一:哈希表
哈希表(又叫散列表,Hash table):是根据关键码值(Key value)而直接进行访问的数据结构。即:它通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数或者哈希函数,存放记录的数据叫做散列表或哈希表。
给定表M,存在函数f(key),对任意给定的关键字值key,带入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希表(Hash table),函数f(key)为哈希函数(hash Function)。
缓存就和hash表有关,Memcached这一缓存技术其实在它的内部就维护了一张很大的哈希表。
数组的存储结构(优点在于查找方便快速):
数组:在内存当中连续开辟的n个单元空间;根据下表可以快速找到所找的元素,但是如果是给定元素去找数组中对应的元素优势就没有那么明显了;插入和删除效率很低
链表(单向链表)的存储结构(优点在于存储的增删操作不会移动其他位置的元素):
链表:(单向链表和双向链表)以单项链表为例,最后一个next为null;插入和删除效率高,但是查找元素效率低
哈希表的存储结构:数组+链表的存储结构即:顺序存储结构 + 链式存储结构(集两者优点于一身,不仅可以快速存还可以快速取):
哈希表:本质主题结构也是一个数组;优势通过下标去找可以很快找到;但是会和链表结合,集两者的优点统一起来;哈希表集合了数组和链表的优点于一身,不仅可以快速存且可以快速取(顺序的存储结构+链式的存储结构),缺点:不支持排序,一般比线性表存储需要更多的空间,并且记录的关键字不能重复。
Hash函数:hash表采用一个映射函数 f :key -> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过hash函数和关键字计算出来的存储位置(注:这里的存储位置只是表中的存储位置,并非实际的物理地址)称作为Hash地址。
哈希函数的构造方法:
1)直接定址法:取关键字或者关键字的某个线性函数为Hash地址,即address(key) = a * key + b;
2)平方取中法:对关键字进行平方运算,然后取结果的中间几位作为Hash地址;address(key) = a * a 取它结果的中间值。
3)折叠法:对关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址,比如:abc:address(key) = a + b + c;
4)除留取余法:如果知道Hash表的最大长度为N,可以取不大于N的最大质数p,然后对关键字进行取余运算,address(key)= key % p;(最常用的方法)
哈希表中解决冲突的方法:
1)开放地址法(即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。即哈希表采用的是线性的存储结构);
2)链地址法(采用数组和链表相结合的方法,将Hash地址相同的记录存储在一张线性链表中,而每张链表的表头的序号即为计算得到的Hash地址,就如上图所画的示意图,这也是现在大多数实现HashMap采用的方法);
Hash表的平均查找长度:
hash表的平均查找长度有两种:包括查找成功时的平均查找长度和查找失败时的平均查找长度;
查找成功时的平均查找长度 = 表中的每个元素查找成功时的比较次数之和 / 表中元素的个数;
查找不成功时的平均查找长度 = 在表中每一种查找不成功时的比较次数之和 / 表的长度;即可以理解为向表中插入一个新的元素,该元素在每个位置都有可能,然后计算出在每个位置能够插入时需要比较的次数 / 表的长度;
Demo:一组关键字{22,13,4,6,9,10,7,8},表的长度为15,Hash函数取除留取余法的方式即address(key) = key % 15;则关键字在表中的存储如下图所示:
这里有一个概念:负载因子 = 表中的记录数 / 哈希表的长度,如果负载因子越小,说明表中空单元还有很多,则发生冲突的可能性越小;而负载因子越大,则发生冲突的可能性就越大,在查找时所耗费的时间就越多。因此,hash表的平均查找长度和负载因此有关。有相关文献证明当负载因子在0.5的时候,hash的性能能有达到最优。但是一般规定负载因子取值为0.75。
二:JDK中的HashMap实现原理
在JDK1.6,JDK1.7中,HashMap采用位桶+ 链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里,但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶 + 链表 + 红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key - value)时,就首先计算元素 key 的 hash 值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,它们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
上面的两幅图结合着理解,你就可以很好地理解了hashMap的存储结构,它很好的展示了HashMap的实现原理。
哈希函数 f 设计的主要目的:找到存储位置(哈希函数的优劣 位置散列分布均匀 而且冲突小)
哈希函数设计的不好:位置散列容易冲突 后果:数组其他位置没有利用到,浪费内存空间;链表太长 快存或者快取都要经历大量的逻辑判断,效率低下
哈希函数是一个笼统地叫法,函数算法是哈希函数里面的一个步骤
三、JDK1.7中HashMap代码解读
1:阅读源码可知,HashMap的底层是一个数组结构,数组的每一项又是一个链表,当新建一个HashMap的时候就会初始化一个数组。而每个数组中的元素就是一个Entry,Entry里面包括key、value对,还有Entry<K, V> next以及hash,所以每个Map.Entry其实就是一个key-value对,它持有一个指向下一个元素的引用Entry<K, V> next,这就构成了链表。其中源码如下:
-
static class Entry<K,V> implements Map.Entry<K,V> {
-
final K key;//键
-
V value;//值
-
Entry<K,V> next;//指向下一个元素的引用
-
int hash;//hash值
-
-
/**
-
* Creates new entry.
-
*/
-
Entry(int h, K k, V v, Entry<K,V> n) {
-
value = v;
-
next = n;
-
key = k;
-
hash = h;
-
}
-
-
public final K getKey() {
-
return key;
-
}
-
-
public final V getValue() {
-
return value;
-
}
-
-
public final V setValue(V newValue) {
-
V oldValue = value;
-
value = newValue;
-
return oldValue;
-
}
-
-
public final boolean equals(Object o) {
-
if (!(o instanceof Map.Entry))
-
return false;
-
Map.Entry e = (Map.Entry)o;
-
Object k1 = getKey();
-
Object k2 = e.getKey();
-
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
-
Object v1 = getValue();
-
Object v2 = e.getValue();
-
if (v1 == v2 || (v1 != null && v1.equals(v2)))
-
return true;
-
}
-
return false;
-
}
-
-
public final int hashCode() {
-
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
-
}
-
-
public final String toString() {
-
return getKey() + "=" + getValue();
-
}
-
-
/**
-
* This method is invoked whenever the value in an entry is
-
* overwritten by an invocation of put(k,v) for a key k that's already
-
* in the HashMap.
-
*/
-
void recordAccess(HashMap<K,V> m) {
-
}
-
-
/**
-
* This method is invoked whenever the entry is
-
* removed from the table.
-
*/
-
void recordRemoval(HashMap<K,V> m) {
-
}
-
}
2、HashMap的存取实现:
HashMap存储元素的思想是(首先在讲解hashMap之前先普及一个知识:HashMap可以存储一个key为null的值,可以存储多个key!=null而value为null的值,但是HashTable不能):在HashMap中put()元素的时候,先判断key是否为null,为null,在存储在规定的位置(table[0]处),否则根据key的hashCode()方法计算hash值,根据hash值得到这个元素在数组中的位置:如果数组中的位置已经存放其他元素了,那么在这个位置将会用链表的形式存放,首先在table[i]处对应的链表进行遍历,判断已经存在的链表中的Entry实体中是否有与这个元素中的key的hash相同并且key相同,如果有则替换原来oldValue,如果没有,则把这个元素放在链表的链头,即头部插入法,最先加入的在链尾,最后加入的在链首;如果数组中的位置没有存放Entry实体,就直接新建一个Entry实体,将该元素放到此数组中的该位置上(即key.hashCode()得到hashCode------>hash(hashCode)得到hash值h------> h&(length-1)------>得到数组的最终索引)。下面按代码详细讲解:
代码一:put()方法:存元素
-
public V put(K key, V value) {
-
-
//如果数组为EMPTY_TABLE即数组是否为null,是则初始化table
-
if (table == EMPTY_TABLE) {
-
inflateTable(threshold);
-
}
-
//如果key为空值,执行putForNullKey()方法,将value放置在table的第一个位置
-
if (key == null)
-
return putForNullKey(value);
-
//根据key的hashCode,计算的hash值
-
int hash = hash(key);
-
//搜索指定hash值在对应table中的索引
-
int i = indexFor(hash, table.length);
-
//如果table[i]的Entry不为null,通过循环不断遍历e元素的下一个元素,判断是否有元素的hash相同和key相同的
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;//如果hash值相同,key相同,则新value替换老的value,返回老的value。
-
e.value = value;
-
e.recordAccess(this);
-
return oldValue;
-
}
-
}
-
//如果table[i]的Entry为空,说明table[i]没有Entry
-
modCount++;
-
//将hash,key,value填到table[i]处
-
addEntry(hash, key, value, i);
-
return null;
-
}
public V put(K key,V value)方法,存储元素;首先判断table是否为null,如果为null,则初始化数组,接下来判断元素的key是否为null,如果为null,则执行putForNullKey()方法;
-
private V putForNullKey(V value) {
-
//在table[0]处的链表进行循环遍历
-
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
-
//存在元素时,判断元素的key是否为null,如果是则用新值替换老值
-
if (e.key == null) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess(this);
-
return oldValue;
-
}
-
}
-
//如果table[0]不存在元素
-
modCount++;
-
//添加key为null的元素
-
addEntry(0, null, value, 0);
-
return null;
-
}
private V putForNullKey(V value)方法,存储key为null的元素;判断table[0]处的是否有元素,有则用新值代替老值,如果没有,则直接调用addEntry()方法,进行存储key为null的元素。
-
void addEntry(int hash, K key, V value, int bucketIndex) {
-
//判断table[i]位置存储的链表是否大于阈值并且判断table[i]不为null(扩容条件)
-
if ((size >= threshold) && (null != table[bucketIndex])) {
-
//进行扩容
-
resize(2 * table.length);
-
//重新计算hash值
-
hash = (null != key) ? hash(key) : 0;
-
//重新得到bucketIndex
-
bucketIndex = indexFor(hash, table.length);
-
}
-
//创建Entry实体添加此元素
-
createEntry(hash, key, value, bucketIndex);
-
}
void addEntry(int hash,K key,V value,int bucketIndex)方法,添加Entry实体存储元素;判断是否需要扩容,如果需要则进行扩容,长度变为2倍,调用resize()方法进行扩容,原来的元素进行重新散列存储。如果不需要则直接调用createEntry()方法,添加元素。
-
void createEntry(int hash, K key, V value, int bucketIndex) {
-
Entry<K,V> e = table[bucketIndex];
-
table[bucketIndex] = new Entry<>(hash, key, value, e);
-
size++;
-
}
至此,当key为null的value已经存储在table[0]处。下面来讨论key不为null时的元素存储。在key不等于null时,看上面的put()方法,我们知道首先需要计算这个元素的hash值,根据这个元素的key的hashCode。
-
final int hash(Object k) {
-
//首先选取一个hashSeed;
-
//hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this): 0;
-
int h = hashSeed;
-
//判断h是否为0并且判断k的是否为String类的实例
-
if (0 != h && k instanceof String) {
-
//是,则k返回hash值
-
return sun.misc.Hashing.stringHash32((String) k);
-
}
-
//不是,则进行hash值的计算
-
h ^= k.hashCode();
-
h ^= (h >>> 20) ^ (h >>> 12);
-
//返回hash值
-
return h ^ (h >>> 7) ^ (h >>> 4);
-
}
final int hash(Object k),调用这个方法返回对应的hash值。其中元素key的hashCode是JDK开发者定义的,不可见。有兴趣可以自行查阅资料。当返回对应的hash值时,再根据indexFor()方法,进行元素对应位置的寻取。
-
/**
-
* Returns index for hash code h.
-
*/
-
static int indexFor(int h, int length) {
-
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
-
return h & (length-1);
-
}
static int indexFor(int h,int length);根据hash值由indexFor()方法,进行计算得到数组的索引。这里声明的“assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2”,这里数组长度为什么必须是2的次幂呢?
举个例子,JDK默认的数组长度为:1<<4即为16;Length:2的次幂:2^4=16;我们就拿数组长度为16和15来进行计算解释说明为什么必须是2的次幂:
数组长度为16:length - 1 = 15 ------>二进制为: 1111;数组长度为15:length - 1 = 14 ------>二进制为:1110;假设来了两个元素,经过计算它们的key的hashCode得到hash值为:6(二进制为:0110)和7(二进制为:0111),下表是它们根据indexFor()计算后得到的数组索引:
由上表可以得知:当hash值为6和7时,它们和14的二进制进行“与”的时候,产生了相同的结果,产生了碰撞,即它们最后会在数组中的相同的位置即同一个bucket内形成链表,这样在查找这个bucket中的元素时,就会增加查询量,降低查询效率。并且数组中0001、0011、0101、0111、1001、1011、1101所对应的数组位置永远不能存放元素,浪费存储空间,增加了碰撞几率,也增加了每一个bucket中的Entry数量,这样在查询时会降低查询效率。即所有的决定因素都在低位,而数组长度为16可以很好的避免这些问题。使元素散列均匀,只有hash值相同的两个元素才能方法数组中的同一个位置上,形成链表。故数组长度为2的次幂的时候,不同的key.hashCode()在进行hash()后得到相同的位置几率会大大降低,使元素散列均匀,减少碰撞,这样元素在查找效率就会提高。总结:index的位置不会超过数组的下标,也不会引起数组越界;index的位置更加均匀。
接着上面的put(K key, V value)进行说明,在调用indexFor()得到数组的索引位置以后,接下来就是在数组所对应的bucket链表中进行遍历,判断是否存在hash值相同和key相同的元素,如果存在就用新value代替原来的老value,如果不存在就调用addEntry(hash, key, value, i)方法进行元素的存储。(这里就说一下上面存储key为null时与key不为null时有差别的方法,不再一步一步进行介绍)
-
void addEntry(int hash, K key, V value, int bucketIndex) {
-
//判断table[i]位置存储的链表是否大于阈值并且判断table[i]不为null(扩容条件)
-
if ((size >= threshold) && (null != table[bucketIndex])) {
-
//进行扩容
-
resize(2 * table.length);
-
//重新计算hash值
-
hash = (null != key) ? hash(key) : 0;
-
//重新得到bucketIndex
-
bucketIndex = indexFor(hash, table.length);
-
}
-
//创建Entry实体添加此元素
-
createEntry(hash, key, value, bucketIndex);
-
}
这个方法上面已经讲解过了,这里重点说一下resize()方法。
-
void resize(int newCapacity) {
-
//声明Entry[]数组oldTable,把table地址赋值给oldTable;
-
Entry[] oldTable = table;
-
//获取数组长度
-
int oldCapacity = oldTable.length;
-
//判断数组长度是否为MAXIMUM_CAPACITY容量
-
if (oldCapacity == MAXIMUM_CAPACITY) {
-
//把Integer.MAX_VALUE赋值给threshold
-
threshold = Integer.MAX_VALUE;
-
return;
-
}
-
//声明新的Entry[]数组实体
-
Entry[] newTable = new Entry[newCapacity];
-
//将扩容之前的转到扩容之后的HashMap中
-
transfer(newTable, initHashSeedAsNeeded(newCapacity));
-
//把地址赋值给原来声明的数组地址
-
table = newTable;
-
//重新定义新的阈值
-
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
-
}
上面是进行扩容的方法这里说一下扩容的条件,扩容:第一个有冲突且使用数组的位置已经大于数组的所有位置*扩容值(0.75);原来的长度*2 保证扩容之后的length-1 还是保证低位为1。
-
/**
-
* Transfers all entries from current table to newTable.
-
*/
-
void transfer(Entry[] newTable, boolean rehash) {
-
int newCapacity = newTable.length;
-
for (Entry<K,V> e : table) {
-
while(null != e) {
-
Entry<K,V> next = e.next;
-
if (rehash) {
-
e.hash = null == e.key ? 0 : hash(e.key);
-
}
-
int i = indexFor(e.hash, newCapacity);
-
e.next = newTable[i];
-
newTable[i] = e;
-
e = next;
-
}
-
}
-
}
transfer()方法,对原来的进行重新散列到扩容以后的新的HashMap中。
-
//根据key获取元素
-
public V get(Object key) {
-
//取key为null的Entry实体元素
-
if (key == null)
-
return getForNullKey();
-
//取key不为null的Entry实体元素
-
Entry<K,V> entry = getEntry(key);
-
return null == entry ? null : entry.getValue();
-
}
public V get(Object key);读取HashMap中的元素,分为两种情况,读取key为null的Entry实体元素;读取key不为null的Entry实体元素。getForNullKey()用来读取key为null的Entry实体元素。
-
private V getForNullKey() {
-
if (size == 0) {
-
return null;
-
}
-
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
-
if (e.key == null)
-
return e.value;
-
}
-
return null;
-
}
getEntry()读取key不为null的Entry实体
-
final Entry<K,V> getEntry(Object key) {
-
if (size == 0) {
-
return null;
-
}
-
-
int hash = (key == null) ? 0 : hash(key);
-
for (Entry<K,V> e = table[indexFor(hash, table.length)];
-
e != null;
-
e = e.next) {
-
Object k;
-
if (e.hash == hash &&
-
((k = e.key) == key || (key != null && key.equals(k))))
-
return e;
-
}
-
return null;
-
}
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + Shift + B
斜体:Ctrl/Command + Shift + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
| 项目 | Value |
|---|---|
| 电脑 | $1600 |
| 手机 | $12 |
| 导管 | $1 |
设定内容居中、居左、居右
使用:---------:居中
使用:----------居左
使用----------:居右
| 第一列 | 第二列 | 第三列 |
|---|---|---|
| 第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
| TYPE | ASCII | HTML |
|---|---|---|
| Single backticks | 'Isn't this fun?' |
‘Isn’t this fun?’ |
| Quotes | "Isn't this fun?" |
“Isn’t this fun?” |
| Dashes | -- is en-dash, --- is em-dash |
– is en-dash, — is em-dash |
创建一个自定义列表
- Markdown
- Text-to-HTML conversion tool
- Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 是通过欧拉积分
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图::
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件或者.html文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
-
注脚的解释 ↩︎