HashMap的实现原理(上)

前言:要分析HashMap的实现,首先要知道为什么需要HashMap?它究竟解决了什么问题?它又是如何解决问题的?为了回答上面三个问题,我们先来看一下,数组与链表这两种数据结构的优劣。

一、数组与链表的优劣。

1、数组的优劣:
1、优势: 已知下标的情况下,查找某条数据,速度快。时间复杂度为O(1),即不管你数组有多长,我都很快。(看似是优势,但对男人来说,快却是劣势)

2、劣势:

  • 不知下标,并且数组没有排序的情况下,查找某条数据,只能遍历整个数组。在最倒霉的情况下,时间复杂度为O(n),即你有多长,你就有多久。(长长久久,男人们的希望)。
  • 不知下标,并且数组是有排序的情况下,使用折半查找,时间复杂度也要log(n),即你的时间,取决于你长度的一半的一半的一半的…
  • 当从长度为N的数组中,Add or Remove 一个 item,会导致整个数组的后半段,相应的移动一定数量的位置。比如,插入一个item时,数组会有【N-index】(插入的下标位置)条item向后移动。删除一个item时,数组又会有【N-index】条item向前移动。明眼人一看就知道,这做法费时费力。那有没有办法,解决数组增删时,数组移动的问题呢?有!那就是下面我们要说的链表。

2、链表的优劣:
链表分为单链表和双链表。
单链表像这样:A->B->C->D。A只知B,B只知C,C只知D。但反过来,B不知有A,C不知有B,D不知有C。
双链表像这样:A<->B<->C<->D。A与B互知,B与C互知,C与D互知。

1、劣势:查找麻烦,在最倒霉的情况下,时间复杂度为O(n)。
2、优势:增删item时,不用像数组一样,使【N-index】条item向前或向后移动。只要修改,被删除的item的
前一个item的指针就行。比如,当我们不要B时,就变成A->C->D。

二、使用hash来解决数组的寻址问题。

1、通过对数组的简述,我们知道,在已知下标的情况下,数组查找某条数据的速度是极快的。但问题是,我们如何快速计算出某条数据所在的下标呢?如果用遍历的方式来寻址,那就跟链表没啥区别。为了实现快速寻址的问题,Someone发明了hash的方式。 具体的实现步骤,分为以下几步:

  • 实现通过对要存储到数组的数据(假设这条数据的内容是"abc"),进行hash计算,得出hash值。
  • 再将hash值与数组的长度进行计算,来得出这条数据(“abc”)应该存储到数组的某个位置的下标(假设为x[ x >=0 && x < 数组的长度]),并将数据存储到对应位置,即下标为x的位置。
  • 当下次需要从数组中,获取内容为"abc"的数据时。只要对"abc"进行hash计算,得出hash值,再重复第二步,获取到下标,再通过下标获取即可。

2、那么这个hash值是如何得出来的呢?hash值的计算,依赖于hash算法。 不同的hash算法,会导致HashMap的存储、查询效率的不同。hash算法越好,发生hash冲突的机率就越低。

三、HashMap是如何通过hash值计算出数据应该存储的位置呢?

1、首先,让我们看看HashMap里面,如何是计算key的hash值的?

   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

里面的实现,我们不用管,我们唯一要关心的是,这里的hash算法,通过计算后,会返回一个int类型的数据,而不是abcdefag… 简而言之,在HashMap里面,key的hash值一定是个整型。

2、然后,让我们看看HashMap里面存放数据的table数组的长度有什么特点?来点代码,加加料。


    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

这是HashMap初始化也好,后面扩容也好,它的长度,总会是2的指数(a power of two)。这是重点,画起来,要考。

3、&运算符的运用。

简述下运算规则:两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0。

比如:129&128.

129转换成二进制就是10000001,128转换成二进制就是10000000。从高位开始比较得到,得到10000000,即128.

4、综合1、2、3,我们来看一个例子:

    // 数组的长度 - 1
	int arrayLength = 32 - 1;
	// 需要存储到这个数组的所有数据的hashCode。
	int dataCount = 3000
	
	// 这里重点解释下hashCode,
	// hashCode代表要存储的数据的hashCode,
	// 这里是为了简化逻辑,直接用个for循环。
	for(int hashCode = 0; hashCode < dataCount; ++hashCode) {
	    System.out.println("value="+(hashCode& length));
	}
		

输出的结果:有3000个,但所有的结果x,都是0<=x<=3000。也就是说,对于任意数值 x和y(数组的长度-1) , 当x <= Integer.MAX_VALUE时,对x与y进行&运算,所得出的结果z,一定满足以下条件,即:0<=z<=y。换而言之,即结果z的值必定不超过数组的长度。我们将这种定律,称为“Hash下标不越界定律”

有些朋友,可能不太理解上面的简化版例子。这里再举个完整的例子:
1、假设HashMap里面的array数组的长度为32
2、假设要存储的数据是“abc"。
3、通过某种hash算法 ,得出“abc"的hashCode是96354(这个值是java的"abc".hashCode()方法返回的)。
4、对数组的长度-1,即32-1=31。与"abc"的hashCode值96354,进行&运算。
5、通过&运算得出的结果是:2。这个2就是所求的下标。

疑问点:
问:那数组的长度可不可以是30?然后,30-1=29?
答:不行。请看三.2部分,数组的长度必须是2的指数。
问:如果hashCode是其它数值,那&运算后的值会不会比数组的值大或小,导致越界?
答:不会。参考“Hash下标不越界定律”

总结:到此为止,你已经学会了,HashMap是如何得出,所要存储的数据,应该存放在数组的哪个位置,即求数据所在的下标。后面,我们将继续讲解,Hash冲突及其解决方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值