集合Set和Map之哈希表和链表结构浅谈

1 哈希表

数组中元素的值(键对象)和位置有确定(一一对应)的对应关系,这样的数组叫做哈希表(散列表,这种对应关系叫做映射(类似数学中的映射),实际是函数关系。

哈希表最大的优点:是就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间----查询速度快其次是编码比较容易

代价:仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。

形如:index=hash(value)----元素与索引的映射

哈希表的例子:


映射规则:index=value%10-1

也即:元素值直接映射到元素的位置

问题:上图只是一种理想的情况,并不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的位置中去例如:现在出现了54该放到哪呢

结论:在一些简单的例子中可以直接把哈希码作为元素的值,但在以下情况中,不能直接把哈希码作为元素的位置

1---哈希码很大,超过了数组的索引,显然不合理

2---多个元素具有相同的哈希码,这种情况称之为哈希冲突,为了保证每个元素有不同的位置,不能把哈希码直接作为元素的位置

因此产生了如下的映射机制:

元素值-----------→hash(value)--------------→哈希码(hashCode()方法得到的)---------------→某种映射-------------------→元素位置

明确一个概念:散列值-----索引-----位置,我们查询的时候是根据键值找到索引(键值对存储位置)---得到值对象

幸运的是我们有方法来解决哈希冲突,后续会提到(拉链法),散列值分布均匀,有效降低hash冲突

明确一点:每种类型的键都要有一个与之对应的散列函数

1.1 散列函数(hashCode()方法)计算

看各种类型的散列函数,键对象是对应类型;

正整数:除留余数法----选择大小为M的素数数组,对于任意正整数k,计算k除以M的余数;

问题:但是如果M不为素数,如:键是10进制数而M为10的k次幂,我们只能利用键的后k位,会发现大量的键散列为小于某个值的索引,即:分布不均匀,哈希冲突未减弱(参考Algorithms第四版的p294)

浮点数:如果键是[0,1)之间的实数,可以将它乘以M并四舍五入到一个[0,M-1]之间的索引值

缺陷:键的高位起的作用更大,最低位对散列的结果没有影响

java是如何弥补的:将键表示为二进制然后再用除留余数法(高低位都考虑了)

字符串:

其实还是除留余数法来计算字符串value的散列值

String类型hashCode的源码

public class DemoDemo {
	 public int hashCode() {
	        int h = hash;
	        //private int hash; -----String的成员变量:默认为0
	        if (h == 0 && value.length > 0) {
	            char val[] = value;
	            for (int i = 0; i < value.length; i++) {
	                h = 31 * h + val[i];
	             //说明:val[i]=val.charAt(i);R=31
	            }
	            hash = h;
	        }
	        return h;
	    }
}
特点:R=31可以保证字符串的所有位都能发挥作用

乘法、加法---------hashCode(哈希值)------取余来计算一个字符串的散列值

组合键

键的类型含有多个整形变量,将这多个整形变量整合起来

自定义的hashCode()

看API文档,会发现每个数据类型都需要相应的散列函数,于是Java令所有的数据类型都继承了一个能返回一个32位整数的hashCode方法,32位----返回值类型是int;由于我们需要的是数组的索引而不是一个32位的整数,所以在实现的过程中我们会将默认的hashCode()方法和除留余数法结合起来长生一个[0,M-1]的整数,方法如下:

hash=(x.hashCode()&0x7fffffff)%M;一般会将数组的大小M取为素数,以充分利用原散列值的所有位

自定义数据类型hashCode()产生方法:将对象中的每个变量(Integer,String等)的hashCode()返回值转化为32位整数并计算得到散列值

代码

1.2 软缓存

散列值计算很耗时,将每个键的散列值缓存起来,即:用一个变量(hash)保存它的hashCode()的返回值;第一次调用hashCode()方法会计算对象的散列值,之后对此对象hashCode()方法的调用会直接返回hash变量的值(不用再计算了),感兴趣的可以看看String的hashCode方法的源码

优秀的散列方法需要满足三个条件

一致性-------等价的键对象产生相等的散列值

高效性-------计算方便

均匀性-------均匀地散落所有的键-----等价说法-----键的散列值均匀分布

提一点:均匀性----保证键的每一位都在散列值的计算中起到相同的作用,实际中容易忽略键的高位,这也就是后来为什么

2 基于拉链法的散列表-----重点

2.1   碰撞处理

散列函数(hashCode()方法)作用:将键(对象)转化为数组的索引

散列算法的第二步是碰撞处理:处理两个或多个键对象的散列值相同的情况

基于拉链法的散列表:将大小为M的数组中的每一个元素指向同一个链表,链表中的每一个结点都存储了散列值为该元素的索引的键值对

明确两点:1  数组的索引----散列值;2  数组中的每一个元素储存的是一条链表的头指针(地址)

java中HashSet的底层就是采用了哈希桶(数组+链表+红黑树(jdk7以后特性)),就是为为M个元素分别构建符号表来保存散列到这里的键,符号表实现维护着一条链表的数组,用散列函数(的值)来为每个键选择一条链表。

2.2  散列表的大小

对于上述基于拉链法的散列表,我们的目标是选择合适的数组大小M,M的选择:既不会因为空链表而浪费内存(M较大),也不会因为链表太长在查找上浪费时间(M较小)java的jdk8提供了自动扩容,动态调整链表数组的大小,原理(随后会提到)

3 基于线性探测法的散列表

开放地址散列表(有时间了补充)

4 hash算法的应用

HashSet类代表能存储java对象的集合,它在实现中封装了一个哈希表,用链表结构去解决哈希冲突

补充说明:HashSet不能放重复的元素(java引用对象),底层判断元素是否重复:hashCode和equals方法

参考相关链接

1---点击打开链接---猪的故事

2---点击打开链接---哈希算法在HashMap类中的应用

3---点击打开链接---哈希算法提高篇---一致性哈希算法

4---点击打开链接---3 链地址法,同义词链

5---点击打开链接--HashMap底层在Jdk8的提升,哈希桶的理解

6---点击打开链接--最新的jdk8的特性

关于哈希运算:hashCode-----高位运算(高低位都参与到hash运算)-----取模运算(&比%的运算效率高)

7--点击打开链接--三个方面的讲解

8--点击打开链接--jdk1.7

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值