• 手撕HashMap(1.7)


    思路

    本质其实就是数组+链表,我们的数组主要查询速度很快,我们每次放入元素,会验证它的位置是否冲突->如果该位置没人,我们就会进入addEntry方法,创建一个当前空节点(比如当前i位置),是一个空节点,然后我们再将赋值的新节点平替当前i位置节点,然后next为空节点;

    插入:

    如果当前位置有节点,也就是不为空,就会得到它的next,比如上面的i位置,它现在是有值的,但是它的next就是之前的空节点所以当前位置节点下一位置为null,然后我们验证hash值比较,发现key、hash值都不等(毕竟是空节点)->然后我们就会再次进入addEntry方法,一样的思路,空节点会后置,此时插入成功

     那么,哈希冲突是怎么解决的呢?

    这里设置的很巧妙,其实我上述已经说了,当两个相同key插入时,后插入的发现已经有值了,就会对它的next进行操作,对比它的hash以及key,next为空所以插入,从而有效达到哈希冲突的解决;

    如果key不相同,说明它的位置肯定就不一样,位置的判断——>用key的hash值然后位运算与数组长度与运算获得

    后续操作与上述一样

    1. public V put(K key, V value) {
    2. if (key == null)
    3. return putForNullKey(value);
    4. int hash = myHash(key);
    5. int i = indexForTable(hash, CAPACITY);
    6. for (MyEntry<K, V> e = table[i]; e != null; e = e.next) {
    7. if (e.hash == hash && (e.key == key || e.key.equals(key))) {
    8. V old = e.value;
    9. e.value = value;
    10. return old;
    11. }
    12. }
    13. addEntry(hash, key, value, i);
    14. return null;
    15. }

     得到位置:

    根据key得到hash值,然后进行高位运算

    为什么要进行高位运算?

    这里体现出了HashMap的容量为什么是2的n次幂

    因为hash值时整数->高位位运算得到一个很大的值,这样就保证了我们后面hash&数组-1的低位是有效的,保证了位置都在数组范围内;

    模运算不是也可以吗?

    如果用(hash % capicity)当然也能得到相同的结果,但是因为位运算的效率更高,所以当然要选择与运算,当然这是不一定的

    (30条消息) HashMap中的hash()函数以及如何通过哈希值确定下标_KKKLxxx的博客-CSDN博客_hashmap如何计算下标

    (30条消息) Java 位运算和普通运算,效率比较_Magicflowersbloom的博客-CSDN博客_位运算为什么效率高

    1. /**
    2. * 6.计算机key的hash值
    3. */
    4. private int MyHash(K key) {
    5. if (key == null) {
    6. return 0;
    7. }
    8. int h = key.hashCode();
    9. h = h ^ (h >>> 16);
    10. return h;
    11. }
    1. /**
    2. * 2.1插入key!=null的value到哈希表中
    3. * 计算插入位置:hash&
    4. */
    5. private int indexForTable(int hash, int CAPACITY) {
    6. //hash值与数组长度进行位运算得到下标
    7. return hash & (CAPACITY - 1);
    8. }

    扩容机制:

    我们来说说源码的扩容,当hashmap中的元素个数超过数组大小 * loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75这是一个折中的取值,也就是说,默认情况下数组大小为16,那么当hashmap中的元素个数超过16*0.75 = 12 (阈值或者边界值的时候)就把数组的大小扩展为2 * 16 = 32,然后重新计算出每个元素在数组中的位置,而这是一个非常耗性能的操作,所以我们最好能够提前预知并设置元素的个数。

    流程:

    hashMap首先会创建一个两倍大的新数组,然后我们会将原数组中的值放在我们的新数组中——>这时候需要重新hash——>(因为我们的位置是:hash&(数组.length-1)),数组长度发生变化所以,肯定是要重新哈希的;——>这里一个很重要的点:

    节点迁移1.7使用的头插法,不过多线程下会发送并发死链的现象,1.8采用的尾插法

    以下代码的do-while很细节:

    先得到原哈希表(数组)里的每个元素e,然后得到它的下一个元素next,不管怎么样是e,e.next还是怎么着,他们的hashcode肯定是一样的,那么重新hash肯定也是一样的

    那么我们对e重新hash确定位置,位置为i,此时新哈希表newtable当前位置肯定是null,我们将这个值赋予e.next,e.next=null,这样的目的和插入addEntry方法一样,就是为了让e的下一个节点为null,然后新哈希表当前位置i为e(newtable[i]=e),然后将Entry链上下一个值给到e(e=next)——>此时e就是第二个节点了,一直循环至e!=null

    1. private void resize() {
    2. CAPACITY= CAPACITY* 2;
    3. MyEntry[] newtable = new MyEntry[CAPACITY];
    4. for (Entry<K, V> entry : table) {
    5. MyEntry<K, V> e = entry; // 取得旧Entry数组的每个元素
    6. if (e != null) {
    7. entry = null;// 释放旧Entry数组的对象引用(循环后,旧的Entry数组不再引用任何对象)
    8. do {
    9. MyEntry<K, V> next = e.next;
    10. int i = indexForTable(e.hash, capacity); // 重新计算每个元素在数组中的位置
    11. e.next = newtable[i];
    12. newtable[i] = e; // 将元素放在数组上
    13. e = next; // 访问下一个Entry链上的元素
    14. } while (e != null);
    15. }
    16. }
    17. table = newtable;
    18. }

    代码

    1. package com.wyh;
    2. import java.util.Map;
    3. /**
    4. * @author diao 2022/6/29
    5. */
    6. public class MyHashMap<K, V> {
    7. private int CAPACITY = 16;
    8. private int size = 0;
    9. private MyEntry[] table;
    10. /**
    11. * 1.构造HashMap
    12. * 传阈值CAPACITY情况
    13. * 初始化哈希表,设置初始一个阈值
    14. *
    15. * @param CAPACITY
    16. */
    17. public MyHashMap(int CAPACITY) {
    18. this.CAPACITY = CAPACITY;
    19. this.table = new MyEntry[CAPACITY];
    20. }
    21. /**
    22. * 默认情况
    23. */
    24. public MyHashMap() {
    25. this.table = new MyEntry[CAPACITY];
    26. }
    27. /**
    28. * 2.传入值
    29. * 传入值
    30. */
    31. public V put(K key, V value) {
    32. if (key == null) {
    33. return putForNullKey(value);
    34. }
    35. //1.当key不为null,计算hash值->得到下标
    36. int hash = MyHash(key);
    37. int index = indexForTable(hash, CAPACITY);
    38. //2.当表当前位置不为空说明有人了,进行插入替换该值(这里没有解决hash冲突)
    39. for (MyEntry<K, V> e = table[index]; e != null; e = e.next) {
    40. //3.如果哈希表上该位置hash与你key的hahs相等就赋值返回
    41. if (e.hash == hash && (e.key == key || e.key.equals(key))) {
    42. e.value = value;
    43. V old = value;
    44. return old;
    45. }
    46. }
    47. //4.当前表index位置没有元素进行插入
    48. addEntry(hash, key, value, index);
    49. return null;
    50. }
    51. /**
    52. * 2.1插入key!=null的value到哈希表中
    53. * 计算插入位置:hash&
    54. */
    55. private int indexForTable(int hash, int CAPACITY) {
    56. //hash值与数组长度进行位运算得到下标
    57. return hash & (CAPACITY - 1);
    58. }
    59. /**
    60. * 3.插入key=null的value
    61. * key为null传入值情况
    62. */
    63. private V putForNullKey(V value) {
    64. //从哈希表的一个元素开始遍历->直至找到为null的key
    65. for (MyEntry<K, V> e = table[0]; e != null; e = e.next) {
    66. //给当前key为空对应的value赋值
    67. if (e.key == null) {
    68. e.value = value;
    69. V old = value;
    70. return old;
    71. }
    72. }
    73. //添加新的节点
    74. addEntry(0, null, value, 0);
    75. return null;
    76. }
    77. /**
    78. * 4.给数组添加新节点
    79. * 因为遍历HashEntry可能key都不为空,我们可以拓展一个HashEntry
    80. * 比如之前传空key而哈希表又满了,hash传入0,key=null,value,i=0
    81. */
    82. private void addEntry(int hash, K key, V value, int i) {
    83. //1、这里插入的策略是直接前面第一个插入一个,并且赋值
    84. MyEntry<K, V> e = table[i];
    85. table[i] = new MyEntry<K, V>(hash, key, value, e);
    86. //2、插入后,判断HashEntry是否达到阈值
    87. if (size == CAPACITY) {
    88. //达到阈值扩容
    89. resize();
    90. }
    91. size++;
    92. }
    93. /**
    94. * 5.扩容机制
    95. * 重置HashEntry数组,当size达到阈值时
    96. */
    97. private void resize() {
    98. CAPACITY=CAPACITY*2;
    99. //1.重新创建扩容后的一个Hash表
    100. MyEntry[] newtable = new MyEntry[CAPACITY];
    101. //2.然后将原来的hash表赋值上去
    102. for (MyEntry<K,V> entry : table) {
    103. MyEntry<K, V> e = entry;
    104. if (e != null) {
    105. entry = null;//释放旧的哈希表的元素
    106. do {
    107. MyEntry<K, V> next = e.next;
    108. int i = indexForTable(e.hash, CAPACITY); // 重新计算每个元素在数组中的位置
    109. e.next = newtable[i];
    110. newtable[i] = e; // 将元素放在数组上
    111. e = next; // 访问下一个Entry链上的元素
    112. } while (e != null);
    113. }
    114. }
    115. }
    116. /**
    117. * 6.计算机key的hash值
    118. */
    119. private int MyHash(K key) {
    120. if (key == null) {
    121. return 0;
    122. }
    123. int h = key.hashCode();
    124. h = h ^ (h >>> 16);
    125. return h;
    126. }
    127. /**
    128. * 7.取值
    129. */
    130. public V get(K key){
    131. //1.当键为空值
    132. if(key==null){
    133. return getForNullKey();
    134. }
    135. //2.当不为空key
    136. int hash = MyHash(key);
    137. int index = indexForTable(hash, CAPACITY);
    138. //2.2得到表当前位置,然后遍历,get值
    139. for (MyEntry<K,V>e=table[index];e!=null;e=e.next){
    140. if(e.hash==hash&&(e.key==key||e.key.equals(key))){
    141. return e.value;
    142. }
    143. }
    144. return null;
    145. }
    146. /**
    147. * 8.当key为null值取值
    148. * key为null,从表中的头元素寻找,直至空key为止
    149. */
    150. private V getForNullKey(){
    151. for(MyEntry<K,V>e=table[0];e!=null;e=e.next){
    152. if(e.key==null) {
    153. return e.value;
    154. }
    155. }
    156. return null;
    157. }
    158. }
    159. /**
    160. * hash表:本质是一个数组
    161. *
    162. * @param <K>:键
    163. * @param <V>:值
    164. */
    165. class MyEntry<K, V> {
    166. public int hash;
    167. public K key;
    168. public V value;
    169. //下一个节点
    170. public MyEntry<K, V> next;
    171. public MyEntry(int hash, K key, V value, MyEntry<K, V> next) {
    172. this.hash = hash;
    173. this.key = key;
    174. this.value = value;
    175. this.next = next;
    176. }
    177. }

     

  • 相关阅读:
    react获取Datepicker组件日期
    d为何用模板参数
    Git入门到精通(大全)
    你真的懂ArrayList吗?
    y97.第六章 微服务、服务网格及Envoy实战 -- xDS API与动态配置(八)
    ZKP6.1 Discrete-log-based Polynomial Commitments (Preliminary)
    给STM32装点中国风——华为LiteOS移植
    P14 JDBC 快速入门
    pycharm please specify a different SDK name
    密码学入门——环游密码世界
  • 原文地址:https://blog.csdn.net/weixin_57128596/article/details/125527887