当前位置:   article > 正文

【JAVA-哈希表实现】_java实现哈希表

java实现哈希表

哈希表

1.哈希表也叫散列表,是一种数据结构。

2.哈希表本质上是数组。通过哈希函数将键映射到数组的特定位置,以便快速访问和查找键对应的值。

3.实现哈希表采用的两种方法:①数组+链表;②数组+二叉树。

4.哈希表提供了 O(1) 时间复杂度的查找操作。

1.哈希表用来快速判断一个元素是否出现集合里。
哈希算法:把元素直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道此元素是否在数组里面。
  • 存储时采用键值对存储方式 ,Key-Value,其中要求Key不重复
  • 先计算Key的哈希值(通常为int),将哈希值与数组(设置初始值是16length)的长度范围进行下标映射,将哈希值映射到大小为16(2的整数倍)的表中。
  • 获取一个下标: int index = hash & len;
  • 直接到数组 index位置取出数据(节点数据(K-V-Next)) 
2.解决哈希冲突的方法
(1)开放寻址法

假设当hash值为3冲突时(假设此时hash表长度为15)。

        线性探测法:顺着表查找,直到找到一个空单元或查遍全表。

        H1 = (3+1)%15 = 4,此时若4依旧冲突,则往下一个查找

        H2 = (3+2)%15 = ... 

        二次探查法:当哈希冲突时,在表的左右进行跳跃探测。1^2,-1^2,2^2,-2^2...

        H1 = (3+1^2)%15 = 4,此时若4依旧冲突,则再hash,即

        H2 = (3+(-1)^2)%15 = 2 …

        伪随机探测法:产生一些随机系列值,并给定随机数作为起点

        假设产生的随机系列为2,5,9 …,则

        H1 = (3+2)%15 = 5

        H2 = (3+5)%15 = 8...

(2)拉链法
拉链法在索引1的位置发生了冲突, 发生冲突的元素都被存储在链表中。
(3)再哈希法

对原始哈希函数重新计算哈希值,然后将冲突的元素插入到重新计算的哈希值对应的位置。

        再哈希法的函数表达式可以表示为:newValue = (hash_value + f(key)) % table_size

newHvalue 是重新计算后的哈希值,hash_value 是原始哈希值,f(key) 是一个用于计算新的偏移量的函数,table_size 是哈希表的大小。

(4)公共溢出区法

设立两个表:基础表溢出表。将所有关键字通过哈希函数计算出相应的地址。然后将未发生冲突的关键字放入相应的基础表中,将具有冲突的元素存储在溢出表中。(通常是一个链表或者其他数据结构)

在查找时,先用给定值通过哈希函数计算出相应的散列地址,与基本表的相应位置进行比较,如果不相等,再到溢出表中顺序查找。

3.开放定址法与拉链法的比较:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②拉链法中各链表上的结点空间是动态申请的,更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α(装填因子 = 元素数量 / 表的大小)较小,当结点规模较大时会浪费很多空间。

而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

4.存在问题:
1.哈希冲突(一般 很少)。
2. 不同的 hash 值与长度 length 进行映射时,映射到同一个 index ( 频繁 )。
5.数组+链表的哈希表实现方式:
  1. /**
  2. * 哈希表:
  3. * 节点类:Node类
  4. * 属性:
  5. * 存储结构-节点数组
  6. * 元素个数
  7. * 存储结构数组被占用的格子数量
  8. * 默认初始长度 16
  9. * 扩容阈值 0.75
  10. * 数组长度
  11. * 方法:
  12. * put: 存储
  13. * resize(); 扩容
  14. * get: 获取
  15. *
  16. * @param <K>
  17. * @param <V>
  18. */
  19. class Node<K, V> {
  20. K key;
  21. V value;
  22. Node<K, V> next;
  23. int hash;// K的hash值
  24. public Node(K key, V value, int hash) {
  25. this.key = key;
  26. this.value = value;
  27. this.hash = hash;
  28. }
  29. }
  30. public class MyHashMap<K,V> {
  31. private Node<K, V>[] table;
  32. private int modCount;// 数组被占用的格子数量
  33. private int elmSize;// 元素个数
  34. private int capacity;// 数组的容量
  35. private static final int DEFAULT_CAPACITY = 16;
  36. private static final double LOAD_FACTOR = 0.75;
  37. /**
  38. * 构造方法 根据传进来的容量进行初始化哈希表
  39. *
  40. * @param initCapacity
  41. */
  42. public MyHashMap(int initCapacity) {
  43. if (initCapacity < DEFAULT_CAPACITY) {
  44. initCapacity = DEFAULT_CAPACITY;
  45. }
  46. table = new Node[initCapacity];
  47. capacity = initCapacity;
  48. elmSize = 0;
  49. modCount = 0;
  50. }
  51. public V get(K key) {
  52. int h = key.hashCode();
  53. int index = h & (capacity - 1);
  54. Node<K, V> first = table[index];
  55. // 判断 目标位置的节点是否为null
  56. if (first != null) {
  57. // 判断 first 是否是咱们需要的节点
  58. // 1: 哈希值一致 才需要比较后面的
  59. // 2: 接着比较key的引用地址 如果地址一致 就不需要比较内容
  60. // 3: 地址不一致的情况会存在内容一致的情况 使用equals比较
  61. if (first.hash == h && first.key == key || first.key.equals(key)) {
  62. return first.value;
  63. }
  64. // 第一个节点比较不成功 从这个节点开始作为头节点遍历链表
  65. Node temp = first;// 此处与java底层实现有区别:判断 first 是链表节点(写循环进行遍历)还是红黑树节点(写方法去遍历红黑树)
  66. while (temp.next != null) {
  67. temp = temp.next;
  68. if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
  69. return (V) temp.value;
  70. }
  71. }
  72. }
  73. return null;
  74. }
  75. /**
  76. * 设置元素 用键值对进行存放
  77. *
  78. * @param key
  79. * @param value
  80. */
  81. public void put(K key, V value) {
  82. // System.out.println("Put");
  83. int h = key.hashCode();// 计算Key 的hash
  84. Node<K, V> elmNode = new Node<>(key, value, h);// 新节点
  85. int index = h & (capacity - 1);// 根据hash值 与 数组长度 得到下标
  86. // System.out.println(index);
  87. Node<K, V> first = table[index];
  88. if (first == null) {
  89. table[index] = elmNode;
  90. modCount++;
  91. elmSize++;
  92. } else {
  93. Node<K, V> oldNode = null;
  94. // 如果first 与 新节点的key一致 更新 first节点的v
  95. if (first.hash == h && first.key == key || first.key.equals(key)) {
  96. first.value = value;
  97. oldNode = new Node<>(key, first.value, h);
  98. } else {
  99. Node<K, V> temp = first;
  100. while (temp.next != null) {
  101. temp = temp.next;
  102. if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
  103. temp.value = value;
  104. oldNode = new Node<>(key, temp.value, h);
  105. break;
  106. }
  107. }
  108. if (oldNode == null) {// 新增节点
  109. temp.next = elmNode;
  110. elmSize++;
  111. }
  112. }
  113. }
  114. // 扩容: 数组的被占用格子数 比例大于数组容量的百分之75的时候扩容
  115. if (modCount >= capacity * LOAD_FACTOR) {
  116. resize();
  117. }
  118. }
  119. /**
  120. * 扩容:
  121. * 扩容 2倍扩容
  122. * 创建一个更大的数组 将原本的所有元素存储进去
  123. * (每个元素取出 重新映射位置 因为放置进去时是根据 int index = h & (capacity - 1); capacity放置的,扩容后capacity发生改变)
  124. * 每个元素存储的位置与当前数组的长度capacity相关
  125. * 写函数时,如果出现问题,自己重新写一遍
  126. */
  127. private void resize() {
  128. System.out.println("扩容进入:" + capacity);
  129. modCount = 0;
  130. elmSize = 0;
  131. int oldCapacity = capacity;
  132. int newCapacity = oldCapacity + oldCapacity; //加法比乘法快
  133. Node<K, V>[] oldTable = table;
  134. Node<K, V>[] newTable = new Node[newCapacity];
  135. //把表中的数据取出来一个一个放到新的表中去
  136. for (int i = 0; i < oldCapacity; i++) {
  137. // System.out.println("扩容循环");
  138. // 取出旧节点
  139. Node<K, V> node = oldTable[i];
  140. //此处与对比处的bug进行对比
  141. //取出旧表中的一个node,看这个node的头是否为空。如果头为空,设置该节点放入元素;如果头不为空,循环遍历到链表尾部,尾部下一个节点设置为该节点放入元素
  142. //如果oldTable中node为空,则不考虑
  143. while (node != null) {
  144. Node<K, V> newNode = new Node<>(node.key, node.value, node.hash);
  145. System.out.println("遍历旧数组中的链表");
  146. int h = node.hash;
  147. System.out.println("旧数组中元素重新进行映射");
  148. int index = h & (newCapacity - 1);
  149. Node<K, V> first = newTable[index];
  150. if (first == null) {
  151. newTable[index] = newNode;
  152. elmSize++;
  153. modCount++;
  154. } else {
  155. System.out.println("新数组目标位置是一条链表");
  156. Node<K, V> temp = first;
  157. while (temp.next != null) {
  158. // System.out.println("temp.next !=null");
  159. // System.out.println(temp.value);
  160. temp = temp.next;
  161. }
  162. temp.next = newNode;
  163. elmSize++;
  164. }
  165. node = node.next;
  166. }
  167. }
  168. //更新全局的 哈希表与容量
  169. table = newTable;
  170. capacity = newCapacity;
  171. System.out.println("扩容:" + capacity);
  172. }
  173. /**
  174. * 测试存储以及输出
  175. * @param args
  176. */
  177. public static void main(String[] args) {
  178. MyHashMap<String, Integer> map = new MyHashMap<>(16);
  179. for (int i = 0; i < 100; i++) {
  180. map.put("Hello" + i, i); //存进去的字符Hello是哈希值就比较大了
  181. }
  182. System.out.println("数组长度:" + map.capacity);
  183. System.out.println("数组被占用格子数量:" + map.modCount);
  184. System.out.println("元素个数:" + map.elmSize);
  185. }
  186. }

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/775639
推荐阅读
相关标签
  

闽ICP备14008679号