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. CentOS 离线安装Gitlab-ce

    1. 上gtilab的官网,找了一下安装说明.. 首先安装 依赖的包 sudo yum install -y curl policycoreutils-python openssh-server cr ...

  2. Laravel 常见错误 1071 Specified key was too long

    Laravel 5.5 + Mysql 5.5 ,执行 migrate 时,提示索引长度超过指定的 1000 bytes 原因: Mysql 对索引有一定的长度限制,版本不同长度不同: MyIsAm ...

  3. Centos6.5下VNC Server配置

    整个流程如下~ 1.首先,检查下本机是否安装了vnc: 提示没有安装,那么开始装吧: 2.先关闭防火墙 查看防火墙状态: 关闭防火墙: 注:要切到root下 [carsonzhu@localhost ...

  4. python拉格朗日插值

    #拉格朗日插值代码 import pandas as pd #导入数据分析库Pandas from scipy.interpolate import lagrange #导入拉格朗日插值函数 inpu ...

  5. iptables防火墙配置

    iptables防火墙配置 一.防火墙简介 1.功能: 1)通过源端口,源IP地址,源MAC地址,包中特定标记和目标端口,IP,MAC来确定数据包是否可以通过防火墙 2)分割内网和外网[附带的路由器的 ...

  6. Java之枚举类范例

    代码如下: package catf.component.http.model; /** * @Auther:gongxingrui * @Date:2018-04-17 * @Description ...

  7. Lodop打印二维码内容长度不同如何大小相同

    利用Loodop打印控件打印二维码的时候,往往传入的数值是变量,有的只有一个数字,有的却一大堆数字和字母,根据内容长度不同,二维码大小也不同,这样如果批量打印二维码标签,传入的数据是不同的,会造成有的 ...

  8. UVALive5870-Smooth Visualization-模拟水题

    很水的模拟题,拿数组搞就好了. 注意边界的地方不要算重. #include <cstdio> #include <cstring> #include <algorithm ...

  9. 经典Java面试题收集

    1.面向对象的特征有哪些方面? 答:面向对象的特征主要有以下几个方面: 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面.抽象只关注对象有哪些属性和行为,并不关注这些 ...

  10. Spring事务说明与自实现

    要使用Springboot的事务其实非常简单,在启动类上添加@EnableTransactionManagement,在Service的类或者方法上使用@Transactional就可以了. 事务本身 ...