hashmap hash冲突怎么解决_HashMap原理及冲突之简谈

了解HashMap原理对于日后的缓存机制多少有些认识。在网络中也有很多方面的帖子,但是很多都是轻描淡写,很少有把握的比较准确的信息,在这里试着不妨说解一二。

对于HashMap主要以键值(key-value)的方式来体现,笼统的说就是采用key值的哈希算法来,外加取余最终获取索引,而这个索引可以认定是一种地址,既而把相应的value存储在地址指向内容中。这样说或许比较概念化,也可能复述不够清楚,来看列式更加清晰:

int   hash=key.hashCode();//------------------------1

int   index=hash%table.lenth;//table表示当前对象的长度-----------------------2

其实最终就是这两个式子决定了值得存储位置。但是以上两个表达式还有欠缺。为什么这么说?例如在key.hashCode()后可能存在是一个负整数,你会问:是啊,那这个时候怎么办呢?所以在这里就需要进一步加强改造式子2了,修改后的:

int   index=(hash&Ox7FFFFFFF)%table.lenth;

到这里又迷惑了,为什么上面是这样的呢?对于先前我们谈到在hash有可能产生负数的情况,这里我们使用当前的hash做一个“与”操作,在这里需要和int最大的值相“与”。这样的话就可以保证数据的统一性,把有符号的数值给“与”掉。而一般这里我们把二进制的数值转换成16进制的就变成了:Ox7FFFFFFF。(注:与操作的方式为,不同为0,相同为1)。而对于hashCode()的方法一般有:

public int hashCode(){

int hash=0,offset,len=count;

char[] var=value;

for(int i=0;i

h=31*hash+var[offset++];

}

return hash;

}

说道这里大家可能会说,到这里算完事了吧。但是你可曾想到如果数据都采用上面的方式,最终得到的可能index会相同怎么办呢?如果你想到的话,那恭喜你!又增进一步了,这里就是要说到一个名词:冲突率。是的就是前面说道的一旦index有相同怎么办?数据又该如何存放呢,而且这个在数据量非常庞大的时候这个基率更大。这里按照算法需要明确的一点:每个键(key)被散列分布到任何一个数组索引的可能性相同,而且不取决于其它键分布的位置。这句话怎么理解呢?从概率论的角度,也就是说如果key的个数达到一个极限,每个key分布的机率都是均等的。更进一步就是:即便key1不等于key2也还是可能key1.hashCode()=key2.hashCode()。

对于早期的解决冲突的方法有折叠法(folding),例如我们在做系统的时候有时候会采用部门编号附加到某个单据标号后,这里比如产生一个9~11位的编码。通过对半折叠做。

现在的策略有:

1.键式散列

2.开放地址法

在了解这两个策略前,我们先定义清楚几个名词解释:

threshold[阀值],对象大小的边界值;

loadFactor[加载因子]=n/m;其中n代表对象元素个数,m表示当前表的容积最大值

threshold=(int)table.length*loadFactor

清晰了这几个定义,我们再来看具体的解决方式

键式散列:

我们直接看一个实例,这样就更加清晰它的工作方式,从而避免文字定义。我们这里还是来举一个图书编号的例子,下面比如有这样一些编号:

78938-0000

45678-0001

72678-0002

24678-0001

16678-0001

98678-0003

85678-0002

45232-0004

步骤:

1.把编号作为key,即:int hash=key.hashCode();

2.int index=hash%表大小;

3.逐步按顺序插入对象中

现在问题出现了:对于编号通过散列算法后很可能产生相同的索引值,意味着存在冲突。

解释上面的操作:如果对于key.hashCode()产生了冲突(比如途中对于插入24678-0001对于通过哈希算法后可能产生的index或许也是501),既而把相应的前驱有相同的index的对象指向当前引用。这也就是大家认定的单链表方式。以此类推…

而这里存在冲突对象的元素放在Entry对象中,Entry具有以下一些属性:

int hash;

Object key;

Entry next;

对于Entry对象就可以直接追溯到链表数据结构体中查阅。

开放地址法:

1.线性地址探测法:

如何理解这个概念呢,一句话:就是通过算法规则在对象地址N+1中查阅找到为NULL的索引内容。

处理方式:如果index索引与当前的index有冲突,即把当前的索引index+1。如果在index+1已经存在占位现象(index+1的内容不为NULL)试图接着index+2执行。。。直到找到索引为内容为NULL的为止。这种处理方式也叫:线性地址探测法(offset-of-1)

如果采用线性地址探测法会带来一个效率的不良影响。现在我们来分析这种方式会带来哪些不良因素。大家试想下如果一个非常庞大的数据存储在Map中,假如在某些记录集中有一些数据非常相似(他们产生的索引在内存的某个块中非常的密集),也就是说他们产生的索引地址是一个连续数值,而造成数据成块现象。另一个致命的问题就是在数据删除后,如果再次查询可能无法定到下一个连续数字,这个又是一个什么概念呢?例如以下图片就很好的说明开发地址散列如何把数据按照算法插入到对象中:

对于上图的注释步骤说明:

从数据“78938-0000”开始通过哈希算法按顺序依次插入到对象中,例如78938-0000通过换

算得到索引为0,当前所指内容为NULL所以直接插入;45678-0001同样通过换算得到索引为地址501所指内容,当前内容为NULL所以也可以插入;72678-0002得到索引502所指内容,当前内容为NULL也可以插入;请注意当24678-0001得到索引也为501,当前地址所指内容为45678-0001。即表示当前数据存在冲突,则直接对地址501+1=502所指向内容为72678-0002不为NULL也不允许插入,再次对索引502+1=503所指内容为NULL允许插入。。。依次类推只要对于索引存在冲突现象,则逐次下移位知道索引地址所指为NULL;如果索引不冲突则还是按照算法放入内容。对于这样的对象是一种插入方式,接下来就是我们的删除(remove)方法了。按照常理对于删除,方式基本区别不大。但是现在问题又出现了,如果删除的某个数据是一个存在冲突索引的内容,带来后续的问题又会接踵而来。那是什么问题呢?我们还是同样来看看图示的描述,对于图-2中如果删除(remove)数据24678-0001的方法如下图所示:

对于我们会想当然的觉得只要把指向数据置为NULL就可以,这样的做法对于删除来说当然是没有问题的。如果再次定位检索数据16678-0001不会成功,因为这个时候以前的链路已经堵上了,但是需要检索的数据事实上又存在。那我们如何来解决这个问题呢?对于JDK中的Entry类中的方法存在一个:boolean markedForRemoval;它就是一个典型的删除标志位,对于对象中如果需要删除时,我们只是对于它做一个“软删除”即置一个标志位为true就可以。而插入时,默认状态为false就可以。这样的话就变成以下图所示:

通过以上方式更好的解决冲突地址删除数据无法检索其他链路数据问题了。

2.双散列(余商法)

在了解开放地址散列的时候我们一直在说解决方法,但是大家都知道一个数据结构的完善更多的是需要高效的算法。这当中我们却没有涉及到。接下来我们就来看看在开放地址散列中它存在的一些不足以及如何改善这样的方法,既而达到无论是在方法的解决上还是在算法的复杂度上更加达到高效的方案。

在图2-1中类似这样一些数据插入进对象,存在冲突采用不断移位加一的方式,直到找到不为NULL内容的索引地址。也正是由于这样一种可能加大了时间上的变慢。大家是否注意到像图这样一些数据目前呈现出一种连续索引的插入,而且是一种成块是的数据。如果数据量非常的庞大,或许这种可能性更大。尽管它解决了冲突,但是对于数据检索的时间度来说,我们是不敢想象的。所有分布到同一个索引index上的key保持相同的路径:index,index+1,index+2…依此类推。更加糟糕的是索引键值的检索需要从索引开始查找。正是这样的原因,对于线性探索法我们需要更进一步的改进。而刚才所描述这种成块出现的数据也就定义成:簇。而这样一种现象称之为:主簇现象。

(主簇:就是冲突处理允许簇加速增长时出现的现象)而开放式地址冲突也是允许主簇现象产生的。那我们如何来避免这种主簇现象呢?这个方式就是我们要来说明的:双散列解决冲突法了。主要的方式为:

uint hash=key.hasCode();

uint index=(hash&Ox7FFFFFFF)%table.length;

u按照以上方式得到索引存在冲突,则开始对当前索引移位,而移位方式为:

ffset=(hash&Ox7FFFFFFF)/table.length;

u如果第一次移位还存在同样的冲突,则继续:当前冲突索引位置(索引号+余数)%表.length

u如果存在的余数恰好是表的倍数,则作偏移位置为一下移,依此类推

这样双散列冲突处理就避免了主簇现象。至于HashSet的原理基本和它是一致的,这里不再复述。在这里其实还是主要说了一些简单的解决方式,而且都是在一些具体参数满足条件下的说明,像一旦数据超过初始值该需要rehash,加载因子一旦大于1.0是何种情况等等。还有很多问题都可以值得我们更加进一步讨论的,比如:在java.util.HashMap中的加载因子为什么会是0.75,而它默认的初始大小为什么又是16等等这些问题都还值得说明。要说明这些问题可能又需要更加详尽的说明清楚。

源码分析:HashMap

HashMap是Java新Collection Framework中用来代替HashTable的一个实现,HashMap和HashTable的区别是: HashMap是未经同步的,而且允许null值。HashTable继承Dictionary,而且使用了Enumeration,所以被建议不要使用。

HashMap的声明如下:

public class HashMap extends AbstractMap implements Map, Cloneable,Serializable

有关AbstractMap:http://blog.csdn.net/treeroot/archive/2004/09/20/110343.aspx

有关Map:http://blog.csdn.net/treeroot/archive/2004/09/20/110331.aspx

有关Cloneable:http://blog.csdn.net/treeroot/archive/2004/09/07/96936.aspx

这个类比较复杂,这里只是重点分析了几个方法,特别是后面涉及到很多内部类都没有解释

不过都比较简单。

static final int DEFAULT_INITIAL_CAPACITY = 16; 默认初始化大小

static final int MAXIMUM_CAPACITY = 1 << 30; 最大初始化大小

static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认加载因子

transient Entry[] table; 一个Entry类型的数组,数组的长度为2的指数。

transient int size; 映射的个数

int threshold; 下一次扩容时的值

final float loadFactor; 加载因子

transient volatile int modCount; 修改次数

public HashMap(int initialCapacity, float loadFactor) {

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if (loadFactor <= 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " +loadFactor);

int capacity = 1;

while (capacity < initialCapacity)

capacity <<= 1;

this.loadFactor = loadFactor;

threshold = (int)(capacity * loadFactor);

table = new Entry[capacity];

init();

}

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

public HashMap() {

this.loadFactor = DEFAULT_LOAD_FACTOR;

threshold = (int)(DEFAULT_INITIAL_CAPACITY);

注意:这里应该是一个失误! 应该是:threshold =(int)(DEFAULT_INITIAL_CAPACITY * loadFactor);

table = new Entry[DEFAULT_INITIAL_CAPACITY];

init();

}

public HashMap(Map m) {

this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY),                                   DEFAULT_LOAD_FACTOR);

putAllForCreate(m);

}

void init() {}

static final Object NULL_KEY = new Object();

static Object maskNull(Object key){

return (key == null ? NULL_KEY : key);

}

static Object unmaskNull(Object key) {

return (key == NULL_KEY ? null : key);

}

static int hash(Object x) {

int h = x.hashCode();

h += ~(h << 9);

h ^= (h >>> 14);

h += (h << 4);

h ^= (h >>> 10);

return h;

}

在HashTable中没有这个方法,也就是说HashTable中是直接用对象的hashCode值,但是HashMap做了改进 用这个算法来获得哈希值。

static boolean eq(Object x, Object y) {

return x == y || x.equals(y);

}

static int indexFor(int h, int length) {

return h & (length-1);

}

根据哈希值和数组的长度来返回该hash值在数组中的位置,只是简单的与关系。

public int size() {

return size;

}

public boolean isEmpty() {

return size == 0;

}

public Object get(Object key) {

Object k = maskNull(key);

int hash = hash(k);

int i = indexFor(hash, table.length);

Entry e = table[i];

while (true) {

if (e == null) return e;

if (e.hash == hash && eq(k, e.key)) return e.value;

e = e.next;

}

}

这个方法是获取数据的方法,首先获得哈希值,这里把null值掩饰了,并且hash值经过函数hash()修正。 然后计算该哈希值在数组中的索引值。如果该索引处的引用为null,表示HashMap中不存在这个映射。 否则的话遍历整个链表,这里找到了就返回,如果没有找到就遍历到链表末尾,返回null。这里的比较是这样的:e.hash==hash && eq(k,e.key) 也就是说如果hash不同就肯定认为不相等,eq就被短路了,只有在 hash相同的情况下才调用equals方法。现在我们该明白Object中说的如果两个对象equals返回true,他们的 hashCode应该相同的道理了吧。假如两个对象调用equals返回true,但是hashCode不一样,那么在HashMap 里就认为他们不相等。

public boolean containsKey(Object key) {

Object k = maskNull(key);

int hash = hash(k);

int i = indexFor(hash, table.length);

Entry e = table[i];

while (e != null) {

if (e.hash == hash && eq(k, e.key)) return true;

e = e.next;

}

return false;

}

这个方法比上面的简单,先找到哈希位置,再遍历整个链表,如果找到就返回true。

Entry getEntry(Object key) {

Object k = maskNull(key);

int hash = hash(k);

int i = indexFor(hash, table.length);

Entry e = table[i];

while (e != null && !(e.hash == hash && eq(k, e.key)))

e = e.next;

return e;

}

这个方法根据key值返回Entry节点,也是先获得索引位置,再遍历链表,如果没有找到返回的是null。

public Object put(Object key, Object value) {

Object k = maskNull(key);

int hash = hash(k);

int i = indexFor(hash, table.length);

for (Entry e = table[i]; e != null; e = e.next) {

if (e.hash == hash && eq(k, e.key)) {

Object oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

modCount++;

addEntry(hash, k, value, i);

return null;

}

首先获得hash索引位置,如果该位置的引用为null,那么直接插入一个映射,返回null。如果此处的引用不是null,必须遍历链表,如果找到一个相同的key,那么就更新该value,同时返回原来的value值。如果遍历完了没有找到,说明该key值不存在,还是插入一个映射。如果hash值足够离散的话,也就是说该索引没有被使用的话,那么不不用遍历链表了。相反,如果hash值不离散,极端的说如果是常数的话,所有的映射都会在这一个链表上,效率会极其低下。这里举一个最简单的例子,写两

个不同的类作为key插入到HashMap中,效率会远远不同。

class Good{

int i;

public Good(int i){

this.i=i;

}

public boolean equals(Object o){

return (o instanceof Good) && (this.i==((Good)o).i)

}

public int hashCode(){

return i;

}

}

class Bad{

int i;

public Good(int i){

this.i=i;

}

public boolean equals(Object o){

return (o instanceof Good) && (this.i==((Good)o).i)

posted on 2009-05-14 13:32 郭鹏 阅读(791) 评论(0)  编辑  收藏 所属分类: JAVA

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/560924.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

html5把六张图片做成立方体,HTML5绘制在立方体上的几何曲线图形

CSS语言&#xff1a;CSSSCSS确定body {background: #222;width: 100vw;height: 100vh;overflow: hidden;-webkit-filter: blur(1px);filter: blur(1px);box-sizing: border-box;-webkit-perspective: 3000px;perspective: 3000px;}.cube {position: absolute;width: 250px;heig…

c语言中闰年 日期 天数 统计出在某个特定的年份中,出现了多少次既是13号又是星期五的情形

常识&#xff1a; 1、3、5、7、8、10、12月份&#xff0c;每个月31天。2月闰年有29天&#xff0c;非闰年28天其他月份&#xff0c;每月30天 闰年&#xff1a;一年有365天&#xff0c;闰年有366天&#xff0c;所谓闰年&#xff0c;即能被4整除且不能被100整除的年份&#xff0c…

java有啥区别 jsp_Java与JSP有什么区别

Java与JSP的区别有&#xff1a;1、Java是面向对象编程语言&#xff0c;而JSP是一个建立在Java基础上用于开发动态内容的web页面技术&#xff1b;2、Java负责逻辑业务处理&#xff0c;而JSP负责页面展现等等。【推荐课程&#xff1a;Java教程】JAVA是一种编程语言&#xff0c;可…

c++ stl 容器 迭代器 stl用法示例

1.基本概念 1.1容器概述 顺序容器 vector, deque,list关联容器 set, multiset, map, multimap容器适配器 stack, queue, priority_queue 1.1.1迭代器 用于指向顺序容器和关联容器中的元素迭代器用法和指针类似 有const 和非 const两种通过迭代器可以读取它指向的元素通过非…

python julian date_Python 的内嵌time模板翻译及说明

一、简介time模块提供各种操作时间的函数 说明&#xff1a;一般有两种表示时间的方式: 第一种是时间戳的方式(相对于1970.1.1 00:00:00以秒计算的偏移量),时间戳是惟一的 第二种以数组的形式表示即(struct_time),共有九个元素&#xff0c;分别表示&#xff0c;同一个时间戳的st…

html玫瑰花效果代码,html5渲染3D玫瑰花情人节礼物js特效代码

情人节马上就要到来了&#xff0c;这里给程序员前端设计师们献上一个&#xff0c;html5渲染而成的3D玫瑰花js效果&#xff0c;可以作为虚拟的情人节礼物送给自己的爱人。支持html5的浏览器查看。查看演示下载资源&#xff1a;16次 下载资源下载积分&#xff1a;20积分情人节玫瑰…

跳跃游戏(数组下标跳跃)

给定一个非负整数数组&#xff0c;假定你的初始位置为数组第一个下标。 数组中的每个元素代表你在那个位置能够跳跃的最大长度。 请确认你是否能够跳跃到数组的最后一个下标。 例如&#xff1a;A [2,3,1,1,4]A[2,3,1,1,4] 能够跳跃到最后一个下标&#xff0c;输出true&…

qc中的流程图怎么画_QC流程图参考

华北工控股份有限公司QC流程图样件试产量产文件编号:WI-MED-000FIC-G009版本:A0修改日期:客户:常规品机种:pcba适用工序&#xff1a;IQC/SMT/DIP/OQC/TEST/PACK审核:批准&#xff1a;检验方法检验频率记录担当负责人在制品完成品11确认型号、数量依送货单核对型号/数量仓库检查…

html 页面怎么加载富文本,UILabel加载html富文本

本文主要解决html标签之外文本属性设置当APP里面有搜索的需求的时候&#xff0c;产品可能会要求关键字显示特殊颜色或者字体。其中一种可能性是服务器返回的数据是带有html标签的字符串&#xff0c;那么该怎么解决&#xff1f;当标签之外的其他字体也需要设置不同格式&#xff…

python 打印皮卡丘_用python打印你的宠物小精灵吧

我们来通过一个有趣的例子开始编写我们的第一个python代码。本文涉及的python基础语法为&#xff1a;print输出函数&#xff0c;赋值&#xff0c;字符串print()print()是python的一个内置函数&#xff0c;用于打印输出&#xff0c;是最常见的一个函数之一。有些朋友可能对于打印…

最大子阵列

在一个数组中找出和最大的连续几个数。&#xff08;至少包含一个数&#xff09; 例如&#xff1a; 数组A[] [−2, 1, −3, 4, −1, 2, 1, −5, 4]&#xff0c;则连续的子序列[4,−1,2,1]有最大的和6. 输入格式 第一行输入一个不超过1000的整数n。 第二行输入n个整数A[i]。…

html嵌入原始数据,如何用html和javascript显示原始图像数据?

我有一个ajax应用程序,PHP端将来自摄像头的未编码的原始图像数据发送到客户端javascript端.我想使用img或canvas标签通过html和javascript显示图像.图像数据是32 * 32无符号字符.我想做,无论达到我的目标(编码或其他所有),但我想在客户端做,因为我无法处理服务器端的任何其他操…

三值排序

排序是一种很频繁的计算任务。一个实际的例子是&#xff0c;当我们给某项竞赛的优胜者按金银铜牌排序的时候。在这个任务中可能的值只有三种1&#xff0c;2和3。我们用交换的方法把他排成升序的。 写一个程序计算出&#xff0c;计算出的一个包括1、2、3三种值的数字序列&#…

imgaug批量椒盐噪声 python_python图像扩增-imgaug

# encodingutf-8import osimport imageioimport randomimport numpy as npimport imgaug as iaimport concurrent.futuresfrom imgaug import augmenters as iaaimport matplotlib.pyplot as pltfrom argparse import ArgumentParseria.seed(4)# %matplotlib inline# 参考 http…

express html文件接收路由参数,express 获取post 请求参数

在 Express 中没有内置获取表单 POST 请求体的 API , 我们需要添加第三方插件库安装:npm install --save body-parser配置:var bodyParser require(body-parser)//配置 body-parser 中间件 (插件, 专门用来解析表单 POST 请求)// parse application/x-www-form-urlencodedapp.…

深度优先搜索重要模板

深度优先搜索(Depth-First-Search) 从起点出发&#xff0c;走过的点要做标记&#xff0c;发现有没走过的点&#xff0c;就随意挑一个往前走&#xff0c;走不 了就回退&#xff0c;此种路径搜索策略就称为“深度优先搜索”&#xff0c;简称“深搜”。 其实称为“远度优先搜索”…

360浏览器查看html文件在哪里,360浏览器8菜单栏怎么弄出来?如何查看网站源代码...

今天想查看一个页面的源代码&#xff0c;没想到这个页面竟然禁止了鼠标右键功能&#xff0c;所以无法通过鼠标右键选择“查看源代码”来查看。然后就想通过 360 浏览器的“查看源代码”按钮进行查看&#xff0c;没想到找了半天都找不到这个按钮&#xff0c;最后只能通过查看源代…

tcp欢动窗口机制_TCP协议中的窗口机制------滑动窗口详解

一、窗口机制的分类在TCP协议当中窗口机制分为两种&#xff1a;1.固定的窗口大小2.滑动窗口二、固定窗口存在的问题如下图所示&#xff1a;我们假设这个固定窗口的大小为1&#xff0c;也就是每次只能发送一个数据&#xff0c;只有接收方对这个数据进行了确认后才能发送第二个数…

1-1.HTML、CSS、JavaScript语言在web前端开发中的作用

HTML是网页内容的载体。内容就是网页制作者放在页面上想要让用户浏览的信息&#xff0c;可以包含文字、图片、视频等。 CSS样式是表现。 就像网页的外衣。比如&#xff0c;标题字体、颜色变化&#xff0c;或为标题加入背景图片、边框等。所有这些用来改变内容外观的东西称之为…

判断html()中有长度,VBS 字符串长度判断的问题

对于给你一个字符串你应该怎样判断他的字符串长度&#xff0c;试举一些典型的例子C语言——字符串长度的计算方法提示&#xff1a;(1)计算字符串长度时关键是要注意辨认转义字符&#xff1b;(2)一个转义字符总是以反斜杠开始&#xff0c;再加一个其他字符组成。所以&#xff0c…