Hash算法及HashMap底层实现原理

1.哈希表结构的优势?

哈希表作为一种优秀数据结构
本质上存储结构是一个数组,辅以链表和红黑树
数组结构在查询和插入删除复杂度方面分别为O(1)和O(n)
链表结构在查询和插入删除复杂度方面分别为O(n)和O(1)
二叉树做了平衡 两者都为O(lgn)
而哈希表两者都为O(1)

2.哈希表简介

哈希表本质是一种(key,value)结构
由此我们可以联想到,能不能把哈希表的key映射成数组的索引index呢?
如果这样做的话那么查询相当于直接查询索引,查询时间复杂度为O(1)
其实这也正是当key为int型时的做法 将key通过某种做法映射成index,从而转换成数组结构

3.数据结构实现步骤

1.使用hash算法计算key值对应的hash值h(默认用key对应的hashcode进行计算(hashcode默认为key在内存中的地址)),得到hash值
2.计算该(k,v)对应的索引值index 
  索引值的计算公式为 index = (h % length) length为数组长度
3.储存对应的(k,v)到数组中去,从而形成a[index] = node<k,v>,如果a[index]已经有了结点
即可能发生碰撞,那么需要通过开放寻址法或拉链法(Java默认实现)解决冲突

当然这只是一个简单的步骤,只实现了数组 实际实现会更复杂
hash表 数组类似下图

索引 0 1 2 3 4 5 6 7
--- null null <10,node1> <27,node2> null null null null
---

两个重要概念

哈希算法
h 通过hash算法计算得到的的一个整型数值 
h可以近似看做一个由key的hashcode生成的随机数,区别在于相同的hashcode生成的h必然相同
而不同的hashcode也可能生成相同h,这种情况叫做hash碰撞,好的hash算法应尽量避免hash碰撞
(ps:hash碰撞只能尽量避免,而无法杜绝,由于h是一个固定长度整型数据,原则上只要有足够多的输入,就一定会产生碰撞)
关于hash算法有很多种,这里不展开赘述,只需要记住h是一个由hashcode产生的伪随机数即可
同时需要满足key.hashcode -> h 分布尽量均匀(下文会解释为何需要分布均匀)
可以参考https://blog.csdn.net/tanggao1314/article/details/51457585
解决碰撞冲突

由上我们可以知道,不同的hashcode可能导致相应的h即发生碰撞
那么我们需要把相应的<k,v>放到hashmap的其他存储地址

解决方法1:开放寻址法
通过在数组以某种方式寻找数组中空余的结点放置
基本思想是:当关键字key的哈希地址p=H(key)出现冲突时
以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,
解决方法1:链地址法
通过引入链表 数组中每一个实体存储为链表结构,如果发生碰撞,则把旧结点指针指向新链表结点,此时查询碰撞结点只需要遍历该链表即可
在这种方法下,数据结构如下所示
int类型数据 hashcode 为自身值
链地址法示例图

在JAVA中几个细节点

1.为什么需要扩容?扩容因子大还是小好?

由于数组是定长的,当数组储存过多的结点时,发生碰撞的概率大大增加,此时hash表退化成链表

过大的扩容因子会导致碰撞概率大大提升,过小扩容因子会造成存储浪费,在Java中默认为0.75

2.当从哈希表中查询数据时,如果key对应一条链表,遍历时如何判断是否应该覆盖?

当遍历链表时,如果两个key.hashcode的h一致会调用equals()方法判断是否为同一对象,equal的默认实现是比较两者的内存地址

因此为什么Java强调当重写equals()时需要同时重写hashcode()方法,假设两个不同对象,在内存中的地址不同分别为a和b,那么重写equals()以后a.equals(b) =true 开发者希望把a,b这两个key视作完全相等
然而由于内存地址的不同导致hashcode不同,会导致在hashmap中储存2个本应相同的key值

这里提供一个范例
public class Student {
    //学号
    public int student_no;
    //姓名
    public String name;
    
    @Override
    public boolean equals(Object o) {
        Student student = (Student) o;
        return student_no == student.student_no;
    }
    
}

通常情况下我们像上图一样期望通过判断两个Student的学号是否是否为同一学生
然而在使用map或set集合时产生出乎意料的结果


image.png

当我们重写hashcode()时

@Override
    public int hashCode() {
        return Objects.hash(student_no);
    }

可以看到现在可以正常使用集合框架中的一些特性


image.png

3.为什么在HashMap中数组的长度length = 2^n(初始值为16) ?

当计算索引值index = h % length 由于计算机的取余操作速度很慢,而计算机的按位取余 & 的操作非常快,又因为 h%length = h & (length-1) (需要满足length = 2^n) 因此规定了length = 2^n 加快index的计算速度 上式的证明参考http://yananay.iteye.com/blog/910460

4.HashMap的红黑树在哪里体现呢?

红黑树是JDK8中对hashmap作的一个变更,在JDK7之前,HashMap采用数组+链表的形式存储数据,我们知道优秀的hash算法应避免碰撞的发生,但假如开发者使用了不合适的hash算法,O(1)级别的数组查询会退化到O(n)级链表查询,因此在JDK8中引入红黑树的,当一个结点的链表长度大于8时,链表会转换成红黑树,提高查询效率,而链表长度小于6时又会退化成链表

5.扩容是如何触发的?

当hashmap中的size > loadFactory * capacity即会发生扩容,size 也是数组结点和链表结点的总和,要明确扩容是一个非常耗费性能的操作,因为数组的长度发生改变,需要对所有结点的索引值重新进行计算,而在JDK8中对这部分进行了优化,详细可以参考https://blog.csdn.net/aichuanwendang/article/details/53317351,在扩容完后减轻了碰撞产生的影响

在正常的Hash算法下,红黑树结构基本不可能被构造出来,根据概率论,理想状态下哈希表的每个箱子中,元素的数量遵守泊松分布:

P(X=k) = (λ^k/k!)e^-λ,k=0,1,...

当负载因子为 0.75 时,上述公式中 λ 约等于 0.5,因此箱子中元素个数和概率的关系如下:(参考https://blog.csdn.net/Philm_iOS/article/details/81200601),下述分布来自源码文档

> * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

JDK的设计者为了开发者真是煞费苦心= =

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容