当前位置:   article > 正文

Java数据结构——哈希表_java中哈希表

java中哈希表

一.概念

哈希散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

二. HashMap类

1. 概述

HashMap是基于哈希表的Map接口实现,属于双列集合的一种,其存储数据的特点是无序,不重复,无索引

2. 特有方法

HashMap的方法与Map基本一致\n\ngetOrDefault( key,默认值)\n\n判断哈希表中是否存在key,若存在则返回key的value值,不存在则返回默认值\n\n这个方法可厉害了,在力扣的部分解题中很有帮助\n\n如:如何让哈希表中的一个键key对应多个值\n\n首先我们肯定会想到值用链表结构类型,在每次找键插值时,我们要先获取key的链表,再将value插入链表中,最后再用put覆盖将链表再次插入到key中

 

三. HashMap代码实现

⑴底层基本原理

7a51fd01990c4c29b4115cd92f745318.png

在JDK8以前,哈希表的底层结构是数组+链表,JDK8以后,为了优化性能,又加入了红黑树,当数组长度超过64且链表长度大于等于8,链表会优化为红黑树

下面我们主要来了解最初的底层结构原理

1.首先数组中存储的是键值对对象Entry

2.在创建哈希表时,会默认创建一个长度为16,加载因子为0.75的数组table

3.添加元素时,底层会根据键的哈希值与数组长度的关系计算出应插入的数组位置,若哈希值冲突,会将新元素以链表的形式添加到冲突位置的后面

⑵代码实现

知道了哈希表的基本底层原理,下面我们就来实现HashMap中最基本的操作

1.键值对对象Entry

首先键值对对象一定要有键key和值value以及用来计算存入位置的哈希码hash

又因为当哈希冲突时我们要将新元素以链表的形式添加,所以我们还需要一个用来记录键值对插入位置的指针next

  1. static class Entry<K, V> {
  2. /**
  3. * 键值对
  4. */
  5. final int hash;//键的哈希值
  6. final K key;//键唯一
  7. V value;//值
  8. Entry<K, V> next;//哈希值冲突的键值对用链表连接
  9. public Entry(int hash, K key, V value) {
  10. this.hash = hash;
  11. this.key = key;
  12. this.value = value;
  13. }
  14. @Override
  15. public boolean equals(Object o) {
  16. if (this == o) return true;//地址值相同
  17. if (o == null || getClass() != o.getClass()) return false;//地址值不同
  18. Entry<?, ?> entry = (Entry<?, ?>) o;
  19. return Objects.equals(key, entry.key);
  20. }
  21. @Override
  22. public final int hashCode() {
  23. return Objects.hashCode(key) ^ Objects.hashCode(value);
  24. }
  25. @Override
  26. public String toString() {
  27. return key+"="+value;
  28. }
  29. }

2.MyHashMap

我们主要实现哈希表的添加put,删除remove,查找get等操作

⑴初始成员变量

首先当我们创建哈希表的对象时,底层会自动创建一个默认长度为16且加载因子为0.75的数组table,当数组中已经存在的元素个数size大于数组的可存入最大数组长度(数组长度*加载因子)时,数组就要进行扩容

  1. Entry[] table = new Entry[16];//初始数组长度
  2. int size = 0;//已存数组长度
  3. final double DEFAULT_LOAD_FACTOR = 0.75d;//加载因子
  4. int threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);//可存入的最大数组长度
  5. public MyHashMap() {}

⑵添加put

e73849a0ae8842d6a6f0adde3e0c5a9b.png

我们根据哈希码与数组长度的关系(hash&(数组长度-1))来确定键值对的位置

若确定的数组位置中没有元素null,则将元素直接插入

若确定的数组位置中有元素,则遍历此位置的链表,若键存在,则将原来的值覆盖并返回,若键不存在,则插入链表尾部

  1. public void put(K key, V value) {
  2. int hash = key.hashCode();
  3. int index = hash & (table.length - 1);
  4. if (table[index] == null) {
  5. table[index] = new Entry(hash, key, value);
  6. } else {
  7. Entry<K, V> p = table[index];
  8. while (true) {
  9. if (p.key.equals(key)) {//键已经存在,将原来键的值覆盖
  10. p.value = value;
  11. return;
  12. }
  13. if (p.next == null) {//键不存在,插入
  14. break;
  15. }
  16. p = p.next;
  17. }
  18. p.next = new Entry<>(hash, key, value);
  19. }
  20. size++;
  21. if (size >= threshold) {//检查数组长度是否超出最大数组长度,超出则扩容
  22. resize();
  23. }
  24. }

细节:为什么根据hash&(数组长度-1)来确定键值对的位置?

其实hash&(数组长度-1)等价于hash%数组长度,在数组长度为2^n的前提下

我们来看个例子:

十进制下:15%2=1

转化为二进制为:0001111%0000010=0000001

十进制下:15%4=3

转化为二进制为:0001111%0000100=0000011

十进制下:15%8=7

转化为二进制为:0001111%0001000=0000111

我们发现余数就是被除数保留的位数

因此为了提高运算的性能,我们可以对被除数进行&操作

0001111%0000010↔0001111&0000001=0000001

其中0000001为2^1-1

0001111%0000100↔0001111&0000011=0000011

0000011为2^2-1

那么综上:在数组长度为2^n的前提下,hash&(数组长度-1)等价于hash%数组长度

⑶扩容resize

bd8c70caa464449589a45a80f3875cb7.png

扩容的新数组容量为原来数组的2倍,扩容后我们要将数组每一位的键值对链表重装为两组

我们利用尾插法,若hash&数组长度==0则为一组,若hash&数组长度!=0则为另一组,最后我们将两组链表分装为2组

  1. private void resize() {
  2. Entry[] newtable = new Entry[table.length * 2];//扩容容量为原来数组的两倍
  3. for (int i = 0; i < table.length; i++) {
  4. Entry<K, V> p = table[i];//获取每一个位置的链表拆分为两部分进行重装
  5. if (p != null) {
  6. //尾查法
  7. Entry<K, V> a = null;//链表尾指针
  8. Entry<K, V> b = null;
  9. Entry<K, V> heada = null;//链表头指针,用来记录操作的链表
  10. Entry<K, V> headb = null;
  11. while (p != null) {
  12. //第一组:hash&数组长度==0 第二组:hash&数组长度!=0
  13. if ((p.hash & table.length) == 0) {
  14. if (a == null) {//刚开始添加,记录头指针
  15. heada = p;
  16. } else {
  17. a.next = p;
  18. }
  19. a = p;//尾指针后移
  20. } else {
  21. if (b == null) {//刚开始添加
  22. headb = p;
  23. } else {
  24. b.next = p;
  25. }
  26. b = p;//尾指针后移
  27. }
  28. p = p.next;
  29. }
  30. if (a != null) {//将尾指针指向null
  31. a.next = null;
  32. newtable[i] = heada;//将重装的链表分装到数组中
  33. }
  34. if (b != null) {
  35. b.next = null;
  36. newtable[i + table.length] = headb;
  37. }
  38. }
  39. }
  40. table = newtable;//覆盖原来的数组
  41. //更新最大数组长度
  42. threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);
  43. }

细节:为什么要根据hash&数组长度进行分组?

我们来看一个例子:在数组长度为2^n前提下

十进制下:2%4=2

6%4=2

二进制下:0000010%0000100=0000010

0000110%0000100=0000010

我们进行&操作,检查倒数第三位,按位与后若结果为0则为一组,否则为另一组

⑷删除remove

删除操作与链表的删除操作相似

  1. public V remove(K key) {
  2. int hash = key.hashCode();
  3. int index = hash & (table.length - 1);//位置
  4. if (table[index] == null) {
  5. return null;
  6. } else {
  7. Entry<K, V> p = table[index];
  8. Entry<K, V> pre = null;
  9. //遍历链表寻找要删除的键值对
  10. while (p != null) {
  11. if (p.key.equals(key)) {
  12. if (pre == null) {//链表头部元素
  13. table[index] = p.next;
  14. } else {
  15. pre.next = p.next;
  16. }
  17. size--;
  18. return p.value;
  19. }
  20. pre = p;
  21. p = p.next;
  22. }
  23. }
  24. return null;
  25. }

⑸获取get

  1. public V get(K key) {/**找对应键的值*/
  2. int hash = key.hashCode();
  3. //根据哈希值计算在数组中的位置
  4. int index = hash & (table.length - 1);//hash%table.length;
  5. if (table[index] == null) {
  6. return null;
  7. }
  8. Entry<K, V> p = table[index];
  9. //遍历链表,找到键对应的值
  10. while (p != null) {
  11. if (p.key.equals(key)) {
  12. return p.value;
  13. }
  14. p = p.next;
  15. }
  16. return null;

下面是我们实现的哈希表的完整的代码

  1. package myHashMap;
  2. import java.util.Objects;
  3. import java.util.StringJoiner;
  4. public class MyHashMap<K, V> {
  5. static class Entry<K, V> {
  6. /**
  7. * 键值对
  8. */
  9. final int hash;//键的哈希值
  10. final K key;//键唯一
  11. V value;//值
  12. Entry<K, V> next;//哈希值冲突的键值对用链表连接
  13. public Entry(int hash, K key, V value) {
  14. this.hash = hash;
  15. this.key = key;
  16. this.value = value;
  17. }
  18. @Override
  19. public boolean equals(Object o) {
  20. if (this == o) return true;//地址值相同
  21. if (o == null || getClass() != o.getClass()) return false;//地址值不同
  22. Entry<?, ?> entry = (Entry<?, ?>) o;
  23. return Objects.equals(key, entry.key);
  24. }
  25. @Override
  26. public final int hashCode() {
  27. return Objects.hashCode(key) ^ Objects.hashCode(value);
  28. }
  29. @Override
  30. public String toString() {
  31. return key+"="+value;
  32. }
  33. }
  34. Entry[] table = new Entry[16];//初始数组长度
  35. int size = 0;//已存数组长度
  36. final double DEFAULT_LOAD_FACTOR = 0.75d;//加载因子
  37. int threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);//可存入的最大数组长度
  38. public MyHashMap() {}
  39. public V get(K key) {/**找对应键的值*/
  40. int hash = key.hashCode();
  41. //根据哈希值计算在数组中的位置
  42. int index = hash & (table.length - 1);//hash%table.length;
  43. if (table[index] == null) {
  44. return null;
  45. }
  46. Entry<K, V> p = table[index];
  47. //遍历链表,找到键对应的值
  48. while (p != null) {
  49. if (p.key.equals(key)) {
  50. return p.value;
  51. }
  52. p = p.next;
  53. }
  54. return null;
  55. }
  56. public void put(K key, V value) {
  57. int hash = key.hashCode();
  58. int index = hash & (table.length - 1);
  59. if (table[index] == null) {
  60. table[index] = new Entry(hash, key, value);
  61. } else {
  62. Entry<K, V> p = table[index];
  63. while (true) {
  64. if (p.key.equals(key)) {//键已经存在,将原来键的值覆盖
  65. p.value = value;
  66. return;
  67. }
  68. if (p.next == null) {//键不存在,插入
  69. break;
  70. }
  71. p = p.next;
  72. }
  73. p.next = new Entry<>(hash, key, value);
  74. }
  75. size++;
  76. if (size >= threshold) {//检查数组长度是否超出最大数组长度,超出则扩容
  77. resize();
  78. }
  79. }
  80. private void resize() {
  81. Entry[] newtable = new Entry[table.length * 2];//扩容容量为原来数组的两倍
  82. for (int i = 0; i < table.length; i++) {
  83. Entry<K, V> p = table[i];//获取每一个位置的链表拆分为两部分进行重装
  84. if (p != null) {
  85. //尾查法
  86. Entry<K, V> a = null;//链表尾指针
  87. Entry<K, V> b = null;
  88. Entry<K, V> heada = null;//链表头指针,用来记录操作的链表
  89. Entry<K, V> headb = null;
  90. while (p != null) {
  91. //第一组:hash&数组长度==0 第二组:hash&数组长度!=0
  92. if ((p.hash & table.length) == 0) {
  93. if (a == null) {//刚开始添加,记录头指针
  94. heada = p;
  95. } else {
  96. a.next = p;
  97. }
  98. a = p;//尾指针后移
  99. } else {
  100. if (b == null) {//刚开始添加
  101. headb = p;
  102. } else {
  103. b.next = p;
  104. }
  105. b = p;//尾指针后移
  106. }
  107. p = p.next;
  108. }
  109. if (a != null) {//将尾指针指向null
  110. a.next = null;
  111. newtable[i] = heada;//将重装的链表分装到数组中
  112. }
  113. if (b != null) {
  114. b.next = null;
  115. newtable[i + table.length] = headb;
  116. }
  117. }
  118. }
  119. table = newtable;//覆盖原来的数组
  120. //更新最大数组长度
  121. threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);
  122. }
  123. @Override
  124. public String toString() {
  125. StringJoiner sj = new StringJoiner(",", "[", "]");
  126. for (int i = 0; i < table.length; i++) {
  127. Entry<K, V> p = table[i];
  128. while (p != null) {
  129. sj.add(p.toString());
  130. p = p.next;
  131. }
  132. }
  133. return sj.toString();
  134. }
  135. public V remove(K key) {
  136. int hash = key.hashCode();
  137. int index = hash & (table.length - 1);//位置
  138. if (table[index] == null) {
  139. return null;
  140. } else {
  141. Entry<K, V> p = table[index];
  142. Entry<K, V> pre = null;
  143. //遍历链表寻找要删除的键值对
  144. while (p != null) {
  145. if (p.key.equals(key)) {
  146. if (pre == null) {//链表头部元素
  147. table[index] = p.next;
  148. } else {
  149. pre.next = p.next;
  150. }
  151. size--;
  152. return p.value;
  153. }
  154. pre = p;
  155. p = p.next;
  156. }
  157. }
  158. return null;
  159. }
  160. }

实现完成后我们来运行代码来爽一下吧

首先我们来看一下put方法的添加以及覆盖能否实现

  1. MyHashMap<String,Integer> ha=new MyHashMap<>();
  2. ha.put("aa",1);
  3. ha.put("aa",5);
  4. System.out.println(ha);//[aa=5]

然后是数组的扩容操作

1bc3f959385944c494bf5f71b7f526c7.png

79655650525942618b03d5b6f2086c8f.png

最后是remove以及get方法

6111776e5dde44c394995ae322558ec7.png

e569176220c64603ab6da2d820288c3c.png

四. 哈希表常见算法题

  在算法练习中,熟练的运用哈希表可以使许多题目简单很多,下面我整理了一些力扣中的有关哈希表的经典算法题,希望会对各位有所帮助

常见的利用哈希表主要有三种题型

①数组哈希

②set哈希

③map哈希

2ca042f8e5aa461a9c2df3d4c9e12a74.jpg

   若有不足,错误之处,望指出更正(*´I`*)

 

 

 

 

 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号