哈希表Hash
目标:
- 基础
- 哈希函数的设计
- 哈希冲突处理
- 链地址方法Separate Chaining
- 实现自己的哈希表
- 哈希表的动态空间处理与复杂度
1.1 认识哈希表
- 每一个字符都和一个索引相对应(哈希函数),O(1)的查找操作
- 很难保证每一个“键”通过哈希函数的转换,对应不同的“索引”
- 哈希冲突
- 哈希函数的设计师很重要的
- “键”通过哈希函数得到的“索引”分布越均匀越好
- 哈希表充分体现了算法设计领域的经典思想:空间换时间。哈希表是时间和空间之间的平衡
1.2 哈希函数的设计
Java中的每一个Object类,默认有hashCode()的实现。
-
整型
-
小范围正整数直接使用
-
小范围负整数进行偏移 -100
100--> 0200 -
大整数
-
取模:模一个素数(涉及数学理论,不过多讨论)参考:planetmath.org/goodhashtab…
测试代码如下:
int a = 42; System.out.println(((Integer)a).hashCode()); int b = -42; System.out.println(((Integer)b).hashCode());
-
-
-
浮点型
在计算机中都是32位或者64位的二进制表示,只不过计算机解析成了浮点数
转成整型处理。
double c = 3.1415926; System.out.println(((Double)c).hashCode());
-
字符串、复合类型
- 转成整型处理
String d = "java"; System.out.println(d.hashCode());
-
原则
- 一致性:如果a == b,则hash(a) == hash(b)
- 高效性:计算高效简便
- 均匀性:哈希值均匀分布
1.3 哈希冲突的处理
-
链地址法
-
对于整个哈希表,开m个空间,对于每一个空间,由于有哈希冲突的存在,所以本质上都是存储一个链表
对于每一个位置,我们说存一个链表,其实也就是查找表,经过之前的学习,我们也可以使用平衡树结构(TreeMap、TreeSet)来实现查找表。
Java8之前,每一个位置对应一个链表,Java8开始,当哈希冲突达到一定程度,每一个位置从链表转成红黑树。
-
-
TreeMap实现
代码如下:
// 添加 public void add(K key, V value) { // 复用TreeMap TreeMap<K, V> map = hashtable[hash(key)]; if (map.containsKey(key)) { map.put(key, value); } else { map.put(key, value); size++; } } // 删除 public V remove(K key){ TreeMap<K, V> map = hashtable[hash(key)]; V ret = null; if (map.containsKey(key)){ ret = map.remove(key); size--; } return ret; } // 改 public void set(K key, V value){ TreeMap<K,V> map = hashtable[hash(key)]; if (!map.containsKey(key)){ throw new IllegalArgumentException(key + "doesn't exist"); } map.put(key, value); } public boolean contains(K key){ return hashtable[hash(key)].containsKey(key); } public V get(K key){ return hashtable[hash(key)].get(key); }
-
复杂度分析
-
假设总共有M个地址,如果放入哈希表得到元素为N,当用链表实现时,每个地址的时间复杂度是O(N/M),当用平衡树实现,那么每个地址的时间复杂度是O(logN/M)。
-
如何能达到O(1) 呢?动态空间分配。
- 平均每个地址承载的元素多过一定程度,即扩容。N/M >= upperTol
- 平均每个地址承载额元素少过一定程度,即缩容。N/M < lowerTol
具体实现代码如下:
private void resize(int newM){ TreeMap<K, V>[] newHashTable = new TreeMap[newM]; for (int i = 0;i<newM;i++){ newHashTable[i] = new TreeMap<>(); } int oldM = M; this.M = newM; for (int i = 0;i<oldM;i++){ TreeMap<K, V> map = hashtable[i]; for (K key:map.keySet()){ newHashTable[hash(key)].put(key, map.get(key)); } } this.hashtable = newHashTable; } // 修改添加元素 public void add(K key, V value) { // 复用TreeMap TreeMap<K, V> map = hashtable[hash(key)]; if (map.containsKey(key)) { map.put(key, value); } else { map.put(key, value); size++; if (size >= upperTol * M) { resize(2 * M); } } } // 删除 public V remove(K key) { TreeMap<K, V> map = hashtable[hash(key)]; V ret = null; if (map.containsKey(key)) { ret = map.remove(key); size--; if (size < lowerTol * M && M/2>=intitCapacity) { resize(M / 2); } } return ret; }
-
对于哈希表来说,元素数从N增加到upperTol*N,地址空间增倍,平均复杂度O(1),每个操作在O(lowerTol)~O(upperTol)-->O(1),缩容同理。
-
扩容2*M,不是素数。解决方案:根据 planetmath.org/goodhashtab…
// 素数表 private final int[] capacity = {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741}; // 添加 public void add(K key, V value) { // 复用TreeMap TreeMap<K, V> map = hashtable[hash(key)]; if (map.containsKey(key)) { map.put(key, value); } else { map.put(key, value); size++; if (size >= upperTol * M && capacityIndex + 1 < capacity.length) { capacityIndex++; resize(capacity[capacityIndex]); } } } // 删除 public V remove(K key) { TreeMap<K, V> map = hashtable[hash(key)]; V ret = null; if (map.containsKey(key)) { ret = map.remove(key); size--; if (size < lowerTol * M && capacityIndex - 1 >= 0) { capacityIndex--; resize(capacity[capacityIndex]); } } return ret; }
-
总结:
- 哈希表均摊复杂度为O(1),牺牲了顺序性。集合和映射的实现,可以是链表,可以是树,也可以是哈希表。
- 有序集合,有序映射:平衡树
- 无序集合,无需映射:哈希表
-