1,Hashing过程

像二分查找、AVL树查找,这些查找算法的时间复杂度为O(logn),而对于哈希表而言,我们一般说它的查找时间复杂度为O(1)。那它是怎么实现的呢?这就是一个Hashing过程。

在JAVA中,每个对象都有一个散列码,它是由Object类的hashCode()方法计算得到的(当然也可以覆盖Object的hashCode())。而我们可以在散列码的基础上,定义一个哈希函数,再对哈希函数计算出的结果求余,最终得到该对象在哈希表的位置。

 final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
} h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

如上,哈希函数hash(Object k) 中用到了hashCode()。然后再经过进一步的特殊处理,得到一个最终的哈希值。哈希函数的定义是需要技艺的,因为它要保证尽量地将所有的Key均匀地分布,因此最好借助前人已实践的经验。

当得到哈希值之后,根据该哈希值Mod N(求余)计算出其在哈希表的位置。

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);
}

indexFor(int h, int length)实际上完成的就是求余操作。只不过求余操作涉及到除法,而这里可以通过移位操作来代替除法。即二者完成的功能都是一样的,移位的效率更高。

哈希过程为什么需要先根据hashCode得到一个值(又称散列码),然后再对该值求余呢?

在JAVA中,Object类的hashCode()方法返回的是由调用对象的内存地址导出的一个值,也即,当没有覆盖Object类中的equals() 和 hashCode()时,只有当两个对象的内存地址一样时,才认为两个对象是相等的。这显然不符合实际情况,比如Person类有 String id、String name.....显然在现实中是根据id(身份证)不同来判断两个人不同。因此,需要进一步根据hashCode()值来封装(如上面的 hash(Object k)方法),返回一个合理的散列码。

那为什么又需要对得到的散列码求余呢?---上面的 indexFor(int h, int length)完成的功能

在底层是用数组来存储<key, value>的,而我们得到的散列码可能很大(事实上散列码的范围非常广)

而内存是有限的,不能分配为数组分配一块很大很大的空间,因此,存储<key, value>的数组空间相对较小。

从而需要把 所有的散列码都 “约束” 到这个有效的数组空间中。----这也是导致冲突的根源

为什么使用HashMap查找是O(1)呢?

T value = hashmap.get(key)

①get(key)时,一步计算出该key所对应的底层数组array的 index  (相当于上面 hash(Object k ) 和 indexFor(int h, int length) 这两个函数完成的功能)

②value = array[index]

因此,就认为查找的复杂度为O(1)

2,冲突处理

冲突处理主要分两种,一种是开放定址法,另一种是链地址法。HashMap的实现中采用的是链地址法。

开放定址法有两种处理方式,一种是线性探测另一种是平方探测。

线性探测:依次探测冲突位置的下一个位置。如,在哈希表的位置2处发生了冲突,则探测位置3处是否被使用了,若被使用了,则探测位置4……直至下一个被探测的位置为空(意味着还有位置可以插入元素---插入成功)或者探测了N-1(N为哈希表的长度)个元素又回到了原始的冲突位置处(意味着已经没有位置可供新元素插入了---插入失败)

因此,插入一个元素时,最坏情况下的时间复杂度为O(N),因为它有可能探测了N-1个元素!

平方探测:以平方大小来递增下一次待探测的位置。如,在哈希表位置2处发生了冲突,则探测 (1^2=1)位置3(2+1),若位置3被使用了,则探测(2^2=4) 位置6(2+4),若位置6被使用了,则探测(3^2=9)位置11(2+9=11)……平方探测法有一个特点:对于任何一个给定的素数N(假设哈希表的长度设置为素数),当计算( h(k) + i ^2 ) MOD N 时,随着 i 的增长,得到的结果是循环的。

因此,当平方探测重复探测了某一个位置时,说明探测失败即已经没有位置可供新元素插入了,尽管此时哈希表并没有满。

平方探测是跳着探测的,它忽略了一些位置,而这些位置可能是空的。即在哈希表仍未满的情况下,已经不能再插入新元素了

最坏情况下,平方探测需要检测 N/2个位置,因此插入一个元素的最坏时间复杂度为O(N)。

链地址法

在HashMap的实现中,采用的链地址法来解决冲突,它有一个桶的概念:对于Entry数组而言,数组的每个元素处存储的是链表,而不是直接的Value。在链表中的每个元素才是真正的<Key, Value>。而一个链表,就是一个桶!因此HashMap最多可以有Entry.length 个桶。

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
static final Entry<?,?>[] EMPTY_TABLE = {};
.....
.....

HashMap中有一个Entry数组,而Entry类是HashMap的内部类。由Entry类来封装实际的<Key, Value>

 static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;

HashMap中还有两个变量: int threshold 和 float loadFactor。loadFactor 默认是0.75,threshold作用如下:当HashMap中的元素个数超过threshold时,就会重新调整哈希的大小。

void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);

而loadFactor作用是:指定threshold,一般情况下,哈希表的大小乘以0.75等于threshold。

 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

在HashMap中,addEntry()方法添加新元素时,总是将新元素添加在链表的表头。而不是链表的其它位置。

完。

HashMap分析及散列的冲突处理的更多相关文章

  1. 【数据结构】之散列链表(Java语言描述)

    散列链表,在JDK中的API实现是 HashMap 类. 为什么HashMap被称为“散列链表”?这与HashMap的内部存储结构有关.下面将根据源码进行分析. 首先要说的是,HashMap中维护着的 ...

  2. 哈希表---线性探测再散列(hash)

    //哈希表---线性探测再散列 #include <iostream> #include <string> #include <stdio.h> #include ...

  3. JDK8;HashMap:再散列解决hash冲突 ,源码分析和分析思路

    JDK8中的HashMap相对JDK7中的HashMap做了些优化. 接下来先通过官方的英文注释探究新HashMap的散列怎么实现 先不给源码,因为直接看源码肯定会晕,那么我们先从简单的概念先讲起   ...

  4. 【Java集合学习】HashMap源码之“拉链法”散列冲突的解决

    1.HashMap的概念 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io ...

  5. java 散列运算浅分析 hash()

            文章部分代码图片和总结来自参考资料 哈希和常用的方法 散列,从中文字面意思就很好理解了,分散排列,我们知道数组地址空间连续,查找快,增删慢,而链表,查找慢,增删快,两者结合起来形成散列 ...

  6. 散列数据结构以及在HashMap中的应用

    1. 为什么需要散列表? 对于线性表和链表而言,访问表中的元素,时间复杂度均为O(n).即便是通过树结构存储数据,时间复杂度也为O(logn).那么有没有一种方式可以将这个时间复杂度降为O(1)呢?当 ...

  7. HashMap的实现原理--链表散列

    1.    HashMap概述 HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. ...

  8. java 解决Hash(散列)冲突的四种方法--开放定址法(线性探测,二次探测,伪随机探测)、链地址法、再哈希、建立公共溢出区

    java 解决Hash(散列)冲突的四种方法--开放定址法(线性探测,二次探测,伪随机探测).链地址法.再哈希.建立公共溢出区 标签: hashmaphashmap冲突解决冲突的方法冲突 2016-0 ...

  9. Python:说说字典和散列表,散列冲突的解决原理

    散列表 Python 用散列表来实现 dict.散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组).在一般书中,散列表里的单元通常叫做表元(bucket).在 dict 的散列表当中,每个键 ...

随机推荐

  1. ASP.NET MVC自定义异常处理

    1.自定义异常处理过滤器类文件 新建MyExceptionAttribute.cs异常处理类文件

  2. oracle11.2.0.1 deferred_segment_creation 造成exp imp 空表无法导出的问题

     oracle11g 新增加了 deferred_segment_creation 的属性在创建的数据库表中,如果表中没有数据,并且这个参数是true的话,并不是直接就在数据文件中的增加相应的segm ...

  3. SQLSERVER case when 的学习

    sqlserver 查询时的CASE WHEN学习记录 ) as '任务数', RPATask_State as id, case RPATask_State when then '已接收' when ...

  4. Angular $location获取端口号

    <!DOCTYPE html><html ng-app="myApp"><head lang="en"> <meta ...

  5. CIO知识储备

    1.IT安全和法规知识是CIO的首要 2.IT项目管理专业知识是CIO的必备 3.合作伙伴管理和供应商管理对成功也很关键 4.企业数据管理技能对CIO越来越重要 5.企业财务技能是CIO的一种必备 6 ...

  6. spring 文件加载 通过listener的类获取配置文件 并加载到spring容器中

  7. poj3320(尺取法)

    题目大意:给你一串数字,找出最小的能够覆盖所有出现过的数字的区间长度: 解题思路:依旧是尺取法,但要用map标记下出现过的书: 代码:别用cin输入: #include<iostream> ...

  8. poj2586 【贪心】

    Accounting for Computer Machinists (ACM) has sufferred from the Y2K bug and lost some vital data for ...

  9. IDEA常见设置

    对于eclipse实在忍无可忍,各种功能各种bug..换回IDEA IDEA常见问题(其实不是问题,代码规范而已) 1.解决无限 This file is indented with tabs ins ...

  10. debian 系统安装配置apache

    安装sshapt-get install ssh-server  (安装失败请插入镜像)service ssh start Apache 服务安装apt-get install apache2 apa ...