自定义HashMap
- 数组和链表结构:HashMap 底层采用数组加链表的数据结构。数组用于存储键值对,而链表则用于解决哈希冲突。
- 哈希算法:当我们使用
put(key, value)
方法存储数据时,HashMap 首先会调用hashCode()
方法计算 key 的哈希值,然后通过哈希算法将其转换成数组的一个下标,对应存储位置。 - 数据碰撞:如果该位置已经有数据,就会发生数据碰撞。此时,HashMap 遍历该位置上的链表,通过
equals()
方法比对每个数据的 key。如果 key 相同,会覆盖链表上该位置的数据;如果 key 不同,会根据 J采用头插法将数据存储在链表中。 - 链表转红黑树:当链表上的节点个数大于等于 时,链表会自动转化成红黑树,以提高查找效率。
Node保存数据信息
/*
相当于HashMap 中的Entry<K,V>
*/
public class Node<K,V>{
int hash; //此Node计算出来的hash值,用这个hash值可以找到在数组中的位置
K k; //键
V v; //值
Node<K,V> next;//如果hash冲突,生成链表的话,则next就是指向下一个节点,没有下一个节点就是null
public Node(int hash, K k, V v, Node<K, V> next) {
this.hash = hash;
this.k = k;
this.v = v;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"hash=" + hash +
", k=" + k +
", v=" + v +
", next=" + next +
'}';
}
public void setHash(int hash) {
this.hash = hash;
}
public void setK(K k) {
this.k = k;
}
public void setV(V v) {
this.v = v;
}
public void setNext(Node<K, V> next) {
this.next = next;
}
public int getHash() {
return hash;
}
public K getK() {
return k;
}
public V getV() {
return v;
}
public Node<K, V> getNext() {
return next;
}
}
table数组:保存每个Node
public V get(K k)思路:
1.根据 k 得到 数组下标
2.根据下标得到table中取出Node
3.Node==null 说明数组中没有这个key 直接返回null
4.Node!=null 说明数组中存了这个key
(1)这个Node中的next为空,说明这不是链表,只是唯一的一个节点,则直接取这个节点v返回
(2)这个Node中的next不为空,说明这是个链表(这个链表中的node的k不同,但生成的hash值(即索引)相同),循环这个链表,比较每个node中的k与要找的k是不是equals
public void put(K k , V v )思路:
1.先根据k计算数组索引
2.取出这个索引位置的node
(1)node == null 说明没有冲突,直接放到数组的索引位置
(2)node != null 说明有冲突,
1)k与node中的k相同,则将v覆盖
2)k与node中的k不同,则表明不同key生成了相同hash,hash冲突,生成链表的节点 ,使用头插法
public class MyHashMap<K,V> {
// map的底层就是一个Node数组,每个元素都是一个Node,如果这个元素位置有hash冲突,则这个node变为链表
private Node<K,V>[] table;
/*
数组的初始大小
*/
private final int DEFAULT_INITIAL_CAPACITY=1<<4; //1<<4 = 1000 就是 16
private int size = 0; //表示table中实际存的元素个数
/*
负载因子:减少生成链表的机会 因为table一旦存满了,则必会增加生成链表的机会
所有在table还没存满前,就要扩容
*/
private static final float DEFAULT_FACTOR=0.75f;
/*
阈值:数组容量*负载因子
*/
private int threshold;
public MyHashMap(){
table=new Node[DEFAULT_INITIAL_CAPACITY ];
threshold = (int) (DEFAULT_FACTOR*table.length);
}
public int size(){
return this.size;
}
public V get(K k){
//1.根据 K 得到 数组下标
int index = index(table.length, k );
//2.根据下标得到table中取出Node
Node<K,V> node = table[index];
//3.Node==null,说明map中还没有存这个key,直接返回null
if (node==null){
return null;
}
//4.Node!=null ,说明数组中有这个key
// (1)这个Node中的next为空,说明这不是链表,只是唯一的一个节点,则直接取这个节点v返回
if (node.getNext()==null){
return node.getV();
}else {
// (2)这个Node中的next不为空,说明这是个链表(这个链表中的node的k不同,但生成的hash值(即索引)相同)
// 循环这个链表,比较每个node中的k与要找的k是不是equals
if (node.getK() == k ){
return node.getV();
}
Node<K,V> next = node.next;
while (next!=null){
if (next.getK() == k){
return next.getV();
}
next=next.next;
}
}
return null;
}
public void put(K k , V v ){
//1.先根据k计算数组索引
int index = index(table.length,k);
//2.取出这个索引 位置的node
Node<K,V> node =table[index];
// 判断node==null 说明没有冲突
// 直接放到数组的索引位置
if ( node==null ){
table[index] = new Node<>( hash(k) , k , v , null);
size++;
}
if (node!=null ){
//node!=null 说明冲突
if ( node.getK()==k ){
// k与node中的k相同,则将v覆盖
table[index] = new Node<>( hash(k), k, v , null);
}else {
//链表的头插法
// k与node中的k不同,则表明不同key生成了相同hash,hash冲突,生成链表的节点
table[index] = new Node<>( hash(k), k, v , node );
++size;
}
}
System.out.println( "元素个数:"+size+",阈值:"+threshold+",容量:"+table.length);
if (size>threshold){
System.out.println("扩容");
resize(); // 扩容 这个数组 ,要将数组中的原来的数据移动
}
}
//扩容
private void resize(){
// 容量为2的幂次
Node[] tableOld = table;
Node[] tableNew = new Node[tableOld.length<<1];
for (int i=0; i<tableOld.length;i++){
if ( tableOld[i]!=null){
//原来node的hash值对新数组计算一次索引下标
int newIndex = index( tableNew.length, tableOld[i].k );
tableNew[newIndex] = tableOld[i];
}
}
table = tableNew;
//更新阈值
threshold = (int) (DEFAULT_FACTOR*table.length);
}
/*
利用hash()来完成key生成hashcode的操作
object中原来就有hashCode(), key.hashCode()就已经生成了hash码?
a b c d e f 不够离散 再进行生成减少hash冲突
解决方案:1.先key.hashCode 2.hashCode是32位参数,将它的高低16位混洗,( ^ )
*/
static final int hash( Object key){
int h = key.hashCode();//获取原来的hashCode
/*
^ 异或运算(位运算,相异为1,相同为0)
>> 右移16位
*/
return (key==null)?0:( h^(h>>16) );//打乱原来的hashCode并返回出去,减少hash冲突
}
/*
计算在Node[]数组中的索引
n:表示Node[]数组的长度 通常是2的幂次 -1: 全为1
n=8 n-1=7 111
n=16 n-1=15 1111
*/
static int index(int n,Object key){
//hash(key)%n; //按位与操作比取模操作更高效
/*
等同于求余数(即取低位的数),其余数即为key在数组中的索引
如 n=8 时 即取 hash(key)二进制的后面四位 取低位
*/
return (n-1)&hash(key);// 按位与 取低位
}
}