首先我们需要明白数据解构的物理存储解构只有两种(顺序存储解构)(链式存储解构);
(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中的)
那么我们设想一下 若是我们自己实现一个HashMap 的put()方法该如何设计?
是否我们会考虑当我们需要存入一个数据的时候我将这个数据存入List集合里面add()这个数据
当第二个数据存进来之前我们判断之间的List里面是否有这个数据据通过循环(便利)去比较来实现这个功能呢好像没问题?
这种方式就是O(N) 次算法有多少个数据我们就循环多少次,数据量与耗时成正比增加
算法 – o(1), o(n), o(logn), o(nlogn)
描述算法复杂度时,常用o(1), o(n), o(logn), o(nlogn)表示对应算法的时间复杂度,是算法的时空复杂度的表示。不仅仅用于表示时间复杂度,也用于表示空间复杂度。
O后面的括号中有一个函数,指明某个算法的耗时/耗空间与数据增长量之间的关系。其中的n代表输入数据的量。
比如时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。
再比如时间复杂度O(n2),就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n2)的算法,对n个数排序,需要扫描n×n次。
再比如O(logn),当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。
二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。
O(nlogn)同理,就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)的时间复杂度。
O(1)就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)
时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)
但是呢我们通过o(n)次算法去便利的话若是数据量庞大那么耗时将是无法预计的,此时该以何种方式来解决这个问题呢?
那我们不妨采用Hash算法既o(1)次算法,每一个对象引用数据类型我都去计算出他的HashCode 将得到它的Hash值然后找到数据对应的索引下标将值放到对应的索引的位置这样的话我们的数据量无论增加多少倍在耗时上是不会改变的只需要一次就可以找到我们需要的值;
若是我们采用这种方式在数据量庞大的情况下还行看不出来什么问题但是若是数据量比较少,那么对于空间的浪费该如何改进呢?
我们是否可以将一个对象的Hashcode值%以咱们的初始容量比方说12将得到一个余数值这个余数值对应咱们数组的索引是否就会大大节省了空间 然后一个索引上将存放多个值我们采用链表的形式存放追加即可此时我们再通过o(n)次算法便利比对通过equals()方法来确定值是否相同
此时我们将引入链表结构且设置几个初始值在HashMap的构造方法里面可以看到:
- initialCapacity:初始容量。指的是 HashMap集合初始化的时候自身的容量。可以在构造方法中指定;如果不指定的话,总容量默认值是 16 。需要注意的是初始容量必须是 2 的幂次方。
- loadFactor:加载因子。所谓的加载因子就是 HashMap (当前的容量/总容量) 到达一定值的时候,HashMap
会实施扩容。加载因子也可以通过构造方法中指定,默认的值是 0.75 。举个例子,假设有一个 HashMap 的初始容量为 16
,那么扩容的阀值就是 0.75 * 16 = 12 。也就是说,在你打算存入第 13 个值的时候,HashMap 会先执行扩容。 - threshold:扩容阀值。即 扩容阀值 = HashMap 总容量 * 加载因子。当前 HashMap 的容量大于或等于扩容阀值的时候就会去执行扩容。扩容的容量为当前 HashMap 总容量的两倍。比如,当前 HashMap 的总容量为 16 ,那么扩容之后为 32 。
- table:Entry 数组。我们都知道 HashMap 内部存储 key/value 是通过 Entry 这个介质来实现的。而 table 就是 Entry 数组。
简单的来说HashMap的底层就是数组加链表的结构实现的 数组就是HashMap的主体 - 若是对应索引的位置没有链表(当前entry的next指向null)那么对于查找添加等操作比较快只需要一次地址值即可;
- 若是对应的所应位置有链表则复杂程度为O(N)次,遍历出来存在则覆盖否则新增对于查找来说还是需要全部便利然后通过equals来比较 所以就性能来说当HashMap里面出现的链表越少性能才会越好
//这是一个神奇的函数,用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀 尽可能的减少Hash冲突 位运算代替模运算直接二进制运算不用转换成十进制
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);
}
在 Java 1.8 中,如果链表的长度超过了 8 ,那么链表将转化为红黑树; 就是用2分法查找
发生 hash 碰撞时,Java 1.7 会在链表头部插入,而 Java 1.8 会在链表尾部插入;
为什么常使用string 做为键 因为string是不可变的字符串 出现hash碰撞的几率比较小