Hashmap和Hash码

Hashmap和Hash码

最近在写山脉生成算法时,碰到一些问题,所以就研究了一下浅层的Hashcode和Hashmap。就按照自己的理解来讲一下:

一、时间复杂度

在将之前先了解一下时间复杂度
这里有一篇讲的比较好的文章,我就不展开聊了

参考文章:
一套图搞懂“时间复杂度”

可见追求最优算法的路上肯定绕不开时间复杂度的坎,那么在读取,删减数据的时候,就希望时间复杂度越低越好,于是就为哈希表的引进提供铺垫。

二、为什么要用哈希表

先来衡量一下,如果不用哈希表,用数组和链表的利弊
数组:
查找时十分方便,仅需要一个下标就可以对数据进行读取,时间复杂度为O(1)
而在删减时,由于数组的地址是初始化时已经分配好的,删减时非常麻烦
链表:
删减时非常方便,让直接改变节点指向即可
而查找时非常麻烦,要从头节点遍历到尾才能完成寻找,时间复杂度为O(n)

那有没有一个数据结构能吸取两个的优点呢?他来了——Hashmap
(至于Hashmap和Hashtable之间区别和关系我还没有研究透彻,感兴趣的也可以自己研究一下 )

三、什么是哈希表

先从使用的角度来讲:
哈希表是Key-Value的集合,即键值对。
可以把哈希表比作一个房子,里面放了很多的保险箱,每把钥匙(钥匙叫做Key)只对应一个保险箱,但每个保险箱可以对应多把钥匙(即每个Key只对应一个Value,但同一个Value可以由不同的Key得到)。这个房子十分高级,我们先往保险箱里面放东西(put方法,放进去的叫Value,也就是Key所对应的的值),但这个房子里每个保险箱放的东西必须是同一类型的(String,Integer等基本类型,对象等等);要拿出来的时候只要直接调用get方法,房子就直接把保险箱里的东西复制一份吐出来(或者说这个保险箱和里面的物品不会因为拿出来了而消失,后面仍然可以再使用get方法拿取)
通俗来说就是装有映射的一个表,可以通过Key来找对应的Value

再从其内部结构来讲:
hashmap内部结构

可以看到哈希表的主体是一个数组,并且是个不太长的数组(创建的时候默认长度是16),在数组的元素上套链表,这样当我们寻找时就吸收了数组的快速查找能力(但当元素上有链表时依然要遍历链表),和链表的删减能力(但当所要获取的内容在数组上时仍然要改变数组)。所以可以理解成数组和链表相互妥协的结果。但为了应对各个结构自身的劣势,我们可以人为进行控制,例如控制链表的长度,让链表的长度控制在一定长度使得搜索时间不会太久(Java8默认当链表长度为8时,将会转化为另一种时间复杂度更低的结构——红黑树)

初始化和常用方法:
HashMap<K,V> map= new HashMap<K,V>();
(例:HashMap<String,Integer> map= new HashMap<String,Integer>(); )
map.put(Key, Value);//放入键值对
map.get(Key);//通过Key来获取value
map.containsKey(Key);//判断Key是否有重复(Key是不能重复的,下面的第4小点有解释)

接下来可能的问题是:什么时候会出现链表?我们的数据放置在哪个位置呢?是数组里还是列表里?明明数组中有些位置是空的,但还是要用链表?下标怎么计算?添加的机制是什么?如果放置的数据非常多,是不是搜索依然需要非常长的时间?

那么一个一个的说一下:
1、什么时候会出现链表?我们的数据放置在哪个位置呢?是数组里还是列表里?
我们知道链表的开头是在数组元素中的,当对一个数据进行处理的时候,会通过这个数据的hash值(下面会介绍,先理解成一个每个对象独有的id)来计算出在数组中对应的下标,因为数组不太长,就有可能会出现不同对象相同下标的情况(也叫哈希冲突),这时就会用链表来存储有同一下标的对象。

2、明明数组中有些位置是空的,但还是要用链表?
虽然我们知道数组的搜索速度更快,但计算出下标的方法是系统默认的(当然可以Override),所以不免的会出现数组中比较空,但某处的链表比较长的情况,所以为了减少这种情况的出现,就要尽量让数据都分配在数组上,可以通过修改hash值的定义方法,或是修改计算数组下标的方法来实现(不推荐,因为系统的方法比较复杂且比较好用)。

3、数组下标怎么计算
下标计算

附:Java8中index的计算方法(包括hash()和indexFor())

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
static int indexFor(int h, int length) {
	return h & (length-1);
}

4、添加的机制是什么?
假设有一对数据:<key0,value0>,想放进哈希表中。
首先拿到key0的hash值,通过前面提及的方法计算出对应下标,如果数组中该位置没有数据,则直接添加。
如果对应位置有数据,但没有链表,那先要对这个数据进行判断,判断这两数据的Key是不是一样的(为什么要先判断而不是直接添加链表呢?上面说过,每一个Key只对应一个Value值,如果已经有一个一样的key0了,那么后来添加的key0也必须和前面的key0指向同一个Value值,也就是说,同一个表中不能有相同的Key)如果是一样的,系统就会把原先存储在里面的Value换成后来放进来的value0,如果不一样,再生成链表。
如果对应位置有链表了,那就要对链表的每一个节点进行比较,判断是否有相同的key值,有则替换,没有就放入链表中。
这里抛下几个问题,为后面hashcode的引进做个铺垫:怎么判断Key是否相等("=="还是有其他方法)?链表的遍历耗时很久,又需要判断是否相等更耗时,有没有高效的办法?

5、如果放置的数据非常多,是不是搜索依然需要非常长的时间?
对应这个问题,哈希表有个自己的方法来提高搜索效率:
对于数组,当数组中一定比例数量的位置被占用了(默认是75%),哈希表会扩容(默认是扩大到原来的两倍)(扩容的具体机制我还没完全弄清,小伙伴们可以在最下方的参考文章中去寻找理解)
对于链表,当超过8个节点时就会生成红黑树来提高搜索和删除效率,若数组扩容,每个链表中的节点可能会有变动

看到这就应该知道了,对于哈希表中的每个元素(Entry<K,V>,这个我还不太清楚,还在研究中 )都包含4个属性:
1、Key 2、Value 3、指向下一个节点next(当是末节点时指向null) 4、hash值
那么这个hash值是什么?怎么定义的?往下看

四、Hashcode和hash值

hash值也叫哈希码(hashcode),是int类型,hash值是程序运行的时候分配的,类似于id,在一个程序运行了且未终止的期间内,一个对象的hash是固定不变的,但每次运行程序的同一对象可以有不同的hash值,换句话说,每次运行后,同一对象的hash可以不同,但在本次运行中,hash值是不变的。
hash值的定义方法(hashcode())时可以Override的,我们可以人为定义hash值计算规则来帮助我们的后续操作。

先解决一下上面留下的问题来引入正题:
1、怎么判断Key是否相等("=="还是有其他方法)?
这里用equals方法,我们通过重写Object的equals方法来判断相等,在判断String的内容是否相等就用的是String方法,先看看String中是怎么重写的

public boolean equals(Object anObject) {
	if (this == anObject) {//如果是同一个对象则直接返回true以减少判断
		return true;
	}
	if (anObject instanceof String) {//anObject 是String或其子类
		String anotherString = (String)anObject;
		int n = value.length;//拿到调用该方法的对象的长度
		if (n == anotherString.value.length) {//二者长度相等,则对应字符一个一个比较
			char v1[] = value;
			char v2[] = anotherString.value;
			int i = 0;
			while (n-- != 0) {
				if (v1[i] != v2[i])
					return false;//有对应位置不同的字符返回false
				i++;
			}
			return true;//全等返回true
		}
	}
	return false;
}

我们知道对于String类,只要内部的内容相等就可以认为两个String是相等的,则重写equals方法的目的是,实现通过判断内容来确定两个String是否相等,即使是两个不同的对象,不同的地址。
那再我们生活中有没有相关的实例依照这个equals来判断是否相同呢?比如一本书,只要书名和作者相同,基本就可以断定这是同一本书。比如一个人,判断人名,性别,户籍基本就可以确定是同一个人了。

2、链表的遍历耗时很久,又需要判断是否相等更耗时,有没有高效的办法?
在判断equals的时候,对于有些物品可能需要判断很多内容才能确定是同样东西,比如电脑,牌子,型号,拥有者(的姓名,性别等等),一旦参数较多就会非常耗时。于是这就是hash值出现的必要,在使用equals之前,直接对各个数据的Key的hash值进行判断是否相等(如果hash值相同才会调用equals方法,不等就直接认为二者不是不等的),就会大大提高判断效率。当然因为hash值可以直接判断,能用hash就能直接判断完成最好,所以人为定义hash值时要避免冲突,并且尽可能的把变量以数字的形式表达出来,并加入到hash值的生成方法中,来减少equals方法的判断量。
哈希表的大部分方法(put(),get()等)都是先判断hash值再判断equals
所以重写完equals方法后必须重写定义hash值方法(hashcode()如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
对于hash值和equals方法有下面的要求:
1、equals()和hashcode()要同时重写,只重写equals方法,相当于什么都没做,因为会先判断hash值。只重写hashcode方法,当出现hash相等的情况时就无法进一步判断了。
2、equals相等则hash值必须相等,equals不相等则hash值可以不等。
3、hash值相等则equals可以不等,hash值不等则equals必定不等。

先看看String中的hash值生成方法:

public int hashCode() {
	int h = hash;
	if (h == 0 && value.length > 0) {
		char val[] = value;
		for (int i = 0; i < value.length; i++) {
			h = 31 * h + val[i];
		}
		hash = h;
	}
	return h;
}

数学上就是val[0] * 31^(n-1) + val[1] * 31^(n-2) + … + val[n-1]

五、重写equals和hashcode时思路:

1、先根据自己的思路,确定什么情况下认为是相等的,把需要判断的变量总结出来,写进equals方法。
2、把要判断的变量中,可以转成int的对象拿出来。什么是可以转化成int?像double,float,boolean,可以转化成用int表示。但如果double,float类型是看小数为主,可以通过一定的放大再取int,而放大倍数与下面讲的权重是等效的。
3、对各个变量赋予权重。比如上面String的hashcode方法,前面的千位百位(高位)的权重就比十位个位(地位)的权重要大,自然放大倍数就多,所以这里要思考一下怎么分配权重来避免:不同的数据有相同的hash值。但这里要注意的是,尽可能的让各个对象的hash值连在一起但又不相同,以避免这个类的hash值与其他类的hash值冲突。

就从我自己实际遇到的问题来进一步讲equals和hashcode
首先我写了一个类:点。点的话就包含两个变量,x坐标和y坐标,但我的点是用极坐标写的,所以包括另外两个变量,角度aa和长度r来表示x,y坐标。

先重写了equals方法
equals
但由于只重写了equals方法,出现了下面的问题
hashcode和equals
由于是先判断hash值,而我没有重写hashcode方法,系统直接判定两个点不同,所以返回null。于是我重写了hashcode方法,但极坐标的长度r和角度aa都是double型。r可长可短,但aa的范围有限(0 ~ 2*3.14),为避免重复就要放大。
hashcode
对我自己程序来说角度放大100倍足够了,然后再判断长度要给多大的权重,我的想法是当r=0.1时,角度仍能影响hash值(而0.1在我的程序中已经时非常小的数了)。至于为什么是倍数31开头,因为31是质数,不容易重复,一般用的是31或17

参考文章:
为什么重写equals就必须重写hashCode
HashMap底层实现原理
震惊!!!原来HashMap的底层实现原理竟然是。。。?
java中hashmap的实现原理与底层数据结构

到这里我理解的内容就差不多了,还有很多没弄懂的地方,另外文章可能有错误,欢迎大佬指正。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值