HashMap的结构就是数组+链表。先上图吧,这张图可谓清晰明了。
当让我手写实现HashMap的时候,才发现对HashMap的理解是停留在表明,或者可以说压根就没看懂这张被我放在上上篇文章中的图。出现了一些误解和困惑。
百度中对哈希表的定义: 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
然后我理解为:元素值value 是在数组中Entry[ ]中的所有位置index有一个确定的关系,就是hash关系,K与V之间存在一种映射关系。所以我以为通过K可以之间找到V。但在我自己实现HashMap的过程中是Key通过hash函数(hash过程是求hashCode,其实我的理解是:就是如图中找它该在哪一条链表上)先找到头节点,再遍历头节点所在的那条单链表。(还没看懂源码,不知道底层是不是这样一步一步查找到的,还是怎样实现的。
对上图更简单点的理解。哈希表一开始是个定长的数组,然后存入某个键值对的时候,通过hash计算应该放在索引为1的数组中,但里面已经存在一个键值对了。所以产生冲突,用链地址发解决冲突,将这个键值对挂到链表上。然后就产生这样一个数组+链表的哈希表结构了。
为了防止冲突太多以至于挂的链表很长,引入了加载因子。(HashMap中加载因子是0.75)。数组中存储的个数size / 数组长度length>0.75时就进行rehash 扩容操作。扩容就是将数组长度变为原来的两倍,并且将现在的数组内的数据重新hash并均匀的散列分布到新的数组中,所以该操作消耗比较大。
PS:加载因子值是经过测试得出,考虑各种因素后的均衡值,值过大会照成扩容的概率下降,消耗少但是存取数据的效率也降低(反之。。。)
误解一:数组中存的是所有的节点,还是只存头节点?(这是我一眼只看一个存了数据的哈希表结构,没想到put进入这个过程。把数据如何放入哈希表中的这个过程梳理一下就很清晰了。)
根据加载因子不超过1和查找相关资料来分析,明白数组中是存储每个链表的头节点。其实就跟图中一样。
误解二:以为数组中存储的是索引,节点都存储在链表中,然而不是,可以理解为数组本身有下标作为索引。(引起这种误解的原因是我画数据结构中的链地址法解决冲突的方法是错的= = )
因为只要将每个链表的头节点保存到数组中,可以通过它的next遍历链表,找到任意节点。
重点还是要理解一下数据存入哈希表的过程。
对hashCode的实现还是有点云里雾里的感觉,希望有大佬能指出我的错误或交流。
源码:
package com.hash;
import java.util.HashMap;
public class MyHashMap {
public Node[] HeadNode = new Node[11];
public int size = 0;
public float loadFactor = 0.75f;// 加载因子
public class Node {
Node next;// 指向下一个节点
Object key;
Object value;
// 构造方法
public Node(Object key, Object value) {
this.key = key;
this.value = value;
}
public Object getKey() {
return key;
}
public Object getValue() {
return value;
}
}
// 向哈希表中添加键值对
public void put(Object key, Object value) {
// 判断是否超过加载因子(是都一定要先判断,一开始加入时就开始执行判断,也很耗时?或许可以到达一定程度后再开始判断以减少判断次数)
if (size * 1.0 / HeadNode.length > loadFactor)
rehash();
//
int index = hash(key, HeadNode.length);
Node node = new Node(key, value);
input(node, HeadNode, index);
}
// 添加函数
private void input(Node node, Node[] HeadNode, int index) {
// 如果HeadNode为空,则把当前节点赋给头节点
if (HeadNode[index] == null) {
HeadNode[index] = node;
} else {
// 否则,遍历链表
// 先用temp记下HeadNode[index],后面就用temp代替HeadNode[index],不如太长会看晕
Node temp = HeadNode[index];
if (node.key == temp.key) {
System.out.println("键值" + temp.key + "已存在");
} else {
while (temp.next != null) {
temp = temp.next;
}
}
temp.next = node; // 把要加入的节点加到链表最后
}
}
// hash过程是获取hashcode,其实我的理解是:就是找它该在哪一个链表中
public int hash(Object key, int length) {
int index = -1;
if (key != null) {
// key是Object类型的,要转成int类型的才能进行取余运算。
// 先转成字符数组
char[] c = key.toString().toCharArray();
int tokey = 0;
// 计算转换的key值
for (int i = 0; i < c.length; i++) {
tokey += c[i];
}
index = tokey % length;
}
return index;
}
public void rehash() {
// 每次扩展都把当前的哈希表增大一倍
Node[] newHeadNode = new Node[HeadNode.length * 2];
// 把原来的哈希表依次重新hash进去
for (int i = 0; i < HeadNode.length; i++) {
if (HeadNode[i] != null) {
int newindex = hash(HeadNode[i].key, HeadNode.length * 2);
// 重新new新的节点来保存原理hash表中的键值对
Node rehashheadnode = new Node(HeadNode[i].key, HeadNode[i].value);
input(rehashheadnode, newHeadNode, newindex);
Node temp = HeadNode[i];
while (temp.next != null) {
temp = temp.next;
Node rehashnextnode = new Node(temp.key, temp.value);
int nextindex = hash(temp.key, newHeadNode.length);
input(rehashnextnode, newHeadNode, nextindex);
}
}
}
// 重新设置节点数组的引用,就是把newHeadNode数组的地址赋给HeadNode(可以理解为让HeadNode指向newHeadNode)
HeadNode = newHeadNode;
}
// 获取键值对应的value
public Object getValue(Object key) {
int index = hash(key, HeadNode.length);
// 先找到在哪条链中
Node temp = HeadNode[index];
// 先判断头节点是否为空
if (temp == null)
return null;
else {
if (key == temp.key)
return temp.value;
else {// 遍历链表
while (temp.next != null) {
temp = temp.next;
if (key == temp.key)
return temp.value;
}
}
}
return null;
}
// public Node getNode(){
//
// }
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public static void main(String[] args) {
MyHashMap mhm = new MyHashMap();
HashMap<Object, Object> hm = new HashMap<>();
long s3 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
hm.put(i + " ", i);
}
for (int i = 0; i < 1000000; i++) {
hm.get(i);
}
long s4 = System.currentTimeMillis();
System.out.println("jdkHashMap usetime:" + (s4 - s3));
// 测试
long s1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
mhm.put(i + " ", i);
}
for (int i = 0; i < 1000000; i++) {
mhm.getValue(i);
}
long s2 = System.currentTimeMillis();
System.out.println("MyHashMap usetime:" + (s2 - s1));
}
}
测试结果:
100次 1000 1w 10w
100w 我去了个厕所我的还没run出来。。