哈希表

我们通过将我们要查找的某种数据类型转化为一个索引index,然后通过索引去数组中查找,这时它的复杂度就是O(1)级别的。而将某个数据类型转化为索引的函数我们就称为是哈希函数,比如说将26个小写字母转化为索引,我们可以这么写

  1. index = ch - 'a';

这样就建立起了一一对应的关系,但是并不是所有的对应关系都是一一对应的,因为数组的容量是有限的,而输入的范围可能是无穷的,所以很有可能不同的键对应着同一个索引,比如说键是字符串,因为字符串的组合方式是非常的多,可以看做是无穷的,我们不可能去开辟一个无穷的空间去与这些字符串一一对应,所以不同的字符串生成的索引很有可能会有冲突,我们称这种情况为哈希冲突。由于上面讲到的哈希冲突,所以我们要设计好哈希函数(hashCode())使得发生哈希冲突的可能性小,即使哈希函数产生的哈希值均匀的分布在数组中。

哈希函数的设计

哈希函数应该满足上面提到的:哈希函数产生的哈希值均匀的分布在数组中。数据的类型五花八门,对于特殊的领域有特殊领域的哈希函数的设计方式,甚至还有专门的论文,说这么多就是想说哈希函数的设计十分的复杂,在这里我们只提最简单的一种,哈希函数的设计应该满足

  • 一致性
    • 如果a == b,那么hashCode(a) == hashCode(b)
  • 高效性
    • 计算迅速
  • 均匀性
    • 输出尽可能均匀

哈希表 - 图1
由于Java中基本数据类型和字符串类型有默认的hashCode()计算,所以我们就用Java自带的hashCode计算基本数据类型和字符串的哈希值,而对于引用类型Java是根据地址计算的哈希值,所以可能会出现问题,需要我们自己自定义规则,比如对于一个Student类,我们规定学号以及姓名相同(不区分大小写)就是同一个学生,所以根据一致性原则,它们应该产生相同的哈希值,但是由于Java默认是根据地址产生哈希值,由于二者的地址是不同的,所以产生的哈希值有极大的概率是不同的,所以我们需要自己创建哈希函数。

链地址法

现在我们来演示往哈希表中添加元素的步骤
哈希表 - 图2

  1. import java.util.TreeMap;
  2. public class HashTable<K, V> {
  3. //数组中存储的是TreeMap这种查找表
  4. private TreeMap<K, V>[] hashTable;
  5. private int M;
  6. private int size;
  7. public HashTable(int M) {
  8. this.M = M;
  9. size = 0;
  10. hashTable = new TreeMap[M];
  11. for (int i = 0; i < hashTable.length; i++) {
  12. hashTable[i] = new TreeMap<>();
  13. }
  14. }
  15. public HashTable() {
  16. this(97);
  17. }
  18. public int getSize() {
  19. return size;
  20. }
  21. //得到在数组中的索引
  22. private int hash(K key) {
  23. //与0x7fffffff是为了消除负数
  24. return (key.hashCode() & 0x7fffffff) % M;
  25. }
  26. public void add(K key, V value) {
  27. TreeMap<K, V> map = hashTable[hash(key)];
  28. //先查看已经是否有这个键了
  29. if (map.containsKey(key)) {
  30. //有则更新
  31. map.put(key, value);
  32. } else {
  33. //没有则进行添加,并维护size
  34. map.put(key, value);
  35. size++;
  36. }
  37. }
  38. public V remove(K key, V value) {
  39. V ret = null;
  40. TreeMap<K, V> map = hashTable[hash(key)];
  41. //如果包含键则删除,没有返回null
  42. if (map.containsKey(key)) {
  43. ret = map.remove(key);
  44. size--;
  45. }
  46. return ret;
  47. }
  48. public void set(K key, V value) {
  49. TreeMap<K, V> map = hashTable[hash(key)];
  50. //没有该键抛出异常
  51. if (!map.containsKey(key)) {
  52. throw new IllegalArgumentException("键不存在");
  53. }
  54. map.put(key,value);
  55. }
  56. //直接得到相应的TreeMap,然后去查,TreeMap有检查步骤
  57. public V get(K key) {
  58. return hashTable[hash(key)].get(key);
  59. }
  60. }

哈希表 - 图3

  1. import java.util.TreeMap;
  2. public class HashTable<K, V> {
  3. private static final int upperTol = 10;
  4. private static final int lowerTol = 2;
  5. private static final int initCapacity = 7;
  6. //数组中存储的是TreeMap这种查找表
  7. private TreeMap<K, V>[] hashTable;
  8. private int M;
  9. private int size;
  10. public HashTable(int M) {
  11. //只显示改变的内容
  12. //...
  13. hashTable = new TreeMap[initCapacity];
  14. }
  15. public void add(K key, V value) {
  16. //...
  17. if (size >= upperTol * M) {
  18. resize(2 * M);
  19. }
  20. }
  21. public V remove(K key, V value) {
  22. //...
  23. if (size < M * lowerTol && M / 2 >= initCapacity) {
  24. resize(M / 2);
  25. }
  26. return ret;
  27. }
  28. private void resize(int newM) {
  29. TreeMap<K,V>[] newHashTable = new TreeMap[newM];
  30. //后面要更新M,但是还需要旧M遍历数组
  31. int oldM = M;
  32. //由于后面要重新计算下标,所以这里要更新M
  33. M = newM;
  34. for (int i = 0; i < oldM; i++) {
  35. TreeMap<K, V> map = hashTable[i];
  36. for (K key: map.keySet()) {
  37. //重新计算下标并赋值
  38. newHashTable[hash(key)].put(key, map.get(key));
  39. }
  40. }
  41. hashTable = newHashTable;
  42. }
  43. }

但是我们发现每次我们都扩容为2 * M,这时M就不是一个素数了,为了解决这一个问题,我们准备一个素数表,让M取素数表中的值,每次扩容M在素数表中的索引+1,缩容-1

  1. import java.util.TreeMap;
  2. public class HashTable<K, V> {
  3. //素数表
  4. private static final int[] capacity = {};
  5. private static final int upperTol = 10;
  6. private static final int lowerTol = 2;
  7. private int capacityIndex = 0;
  8. //数组中存储的是TreeMap这种查找表
  9. private TreeMap<K, V>[] hashTable;
  10. private int M;
  11. private int size;
  12. public HashTable() {
  13. this.M = capacity[capacityIndex];
  14. size = 0;
  15. hashTable = new TreeMap[M];
  16. for (int i = 0; i < hashTable.length; i++) {
  17. hashTable[i] = new TreeMap<>();
  18. }
  19. }
  20. public void add(K key, V value) {
  21. //...
  22. if (size >= upperTol * M && capacityIndex + 1 < size) {
  23. capacityIndex++;
  24. resize(capacity[capacityIndex]);
  25. }
  26. }
  27. public V remove(K key, V value) {
  28. //...
  29. if (size < M * lowerTol && capacityIndex - 1 >= 0) {
  30. capacityIndex--;
  31. resize(capacity[capacityIndex]);
  32. }
  33. return ret;
  34. }
  35. }

完整代码

  1. import java.util.TreeMap;
  2. public class HashTable<K, V> {
  3. private static final int[] capacity = {};
  4. private static final int upperTol = 10;
  5. private static final int lowerTol = 2;
  6. private int capacityIndex = 0;
  7. //数组中存储的是TreeMap这种查找表
  8. private TreeMap<K, V>[] hashTable;
  9. private int M;
  10. private int size;
  11. public HashTable() {
  12. this.M = capacity[capacityIndex];
  13. size = 0;
  14. hashTable = new TreeMap[M];
  15. for (int i = 0; i < hashTable.length; i++) {
  16. hashTable[i] = new TreeMap<>();
  17. }
  18. }
  19. public int getSize() {
  20. return size;
  21. }
  22. //得到在数组中的索引
  23. private int hash(K key) {
  24. //与0x7fffffff是为了消除负数
  25. return (key.hashCode() & 0x7fffffff) % M;
  26. }
  27. public void add(K key, V value) {
  28. TreeMap<K, V> map = hashTable[hash(key)];
  29. //先查看已经是否有这个键了
  30. if (map.containsKey(key)) {
  31. //有则更新
  32. map.put(key, value);
  33. } else {
  34. //没有则进行添加,并维护size
  35. map.put(key, value);
  36. size++;
  37. }
  38. if (size >= upperTol * M && capacityIndex + 1 < capacity.length) {
  39. capacityIndex++;
  40. resize(capacity[capacityIndex]);
  41. }
  42. }
  43. public V remove(K key, V value) {
  44. V ret = null;
  45. TreeMap<K, V> map = hashTable[hash(key)];
  46. //如果包含键则删除,没有返回null
  47. if (map.containsKey(key)) {
  48. ret = map.remove(key);
  49. size--;
  50. }
  51. if (size < M * lowerTol && capacityIndex - 1 >= 0) {
  52. capacityIndex--;
  53. resize(capacity[capacityIndex]);
  54. }
  55. return ret;
  56. }
  57. public void set(K key, V value) {
  58. TreeMap<K, V> map = hashTable[hash(key)];
  59. //没有该键抛出异常
  60. if (!map.containsKey(key)) {
  61. throw new IllegalArgumentException("键不存在");
  62. }
  63. map.put(key,value);
  64. }
  65. //直接得到相应的TreeMap,然后去查,TreeMap有检查步骤
  66. public V get(K key) {
  67. return hashTable[hash(key)].get(key);
  68. }
  69. private void resize(int newM) {
  70. TreeMap<K,V>[] newHashTable = new TreeMap[newM];
  71. //后面要更新M,但是还需要旧M遍历数组
  72. int oldM = M;
  73. //由于后面要重新计算下标,所以这里要更新M
  74. M = newM;
  75. for (int i = 0; i < oldM; i++) {
  76. TreeMap<K, V> map = hashTable[i];
  77. for (K key: map.keySet()) {
  78. //重新计算下标并赋值
  79. newHashTable[hash(key)].put(key, map.get(key));
  80. }
  81. }
  82. hashTable = newHashTable;
  83. }
  84. }