之前实现的 Sets(或Map) 集合的总结
存在的问题:
- 基于搜索树实现的 Sets 的缺陷
- 存储的元素必须是可排序的(comparable)
- 是否可以有更好的性能【比O(log N)更好】
解决方案
1. 创建 boolean 索引数组来存储数据
缺点: 及其浪费空间,只支持整型数据
2. 在方案 1 的基础上支持字符串类型
- 首先从支持小写英语单词开始
- 假设 a = 1, b = 2, c = 3, ... , z = 26
- 进制转换
- cat(27进制)=(3 * )+(1*)+(20*)= 2234(10进制)
- 实现 englishToInt(String word) 方法如下:
/** 代码实现 */
public class DataIndexedEnglishWordSet {
private boolean[] present;
public DataIndexedEnglishWordSet() {
present = new boolean[2000000000];
}
public void add(String s) {
present[englishToInt(s)] = true;
}
public boolean contains(int i) {
return present[englishToInt(s)];
}
/** Converts ith character of String to a letter number.
* e.g. 'a' -> 1, 'b' -> 2, 'z' -> 26 */
public static int letterNum(String s, int i) {
int ithChar = s.charAt(i);
if ((ithChar < 'a') || (ithChar > 'z'))
{ throw new IllegalArgumentException(); }
return ithChar - 'a' + 1;
}
public static int englishToInt(String s) {
int intRep = 0;
for (int i = 0; i < s.length(); i += 1) {
intRep = intRep * 27;
intRep = intRep + letterNum(s, i);
}
return intRep;
}
}
3. 在方案2的基础上支持 ASCII 码字符串
- ASCII 码字符取值范围 0 ~ 127,其中 33 - 126 是 printable 的,因此使用 126 进制存储数据
- 转换函数如下:
public static int asciiToInt(String s) {
int intRep = 0;
for (int i = 0; i < s.length(); i += 1) {
intRep = intRep * 126;
intRep = intRep + s.charAt(i);
}
return intRep;
}
4. 在方案3的基础上支持 Unicode 码
- Unicode 支持的最大中文字符对应的数值是 40959,所以使用 40959 进制 以上方案存在的最大问题是:整型溢出导致了哈希碰撞,上面的方法其实都是为了计算哈希码
哈希算法的两个挑战:
- 如何减少哈希碰撞
- 如何计算一个对象的哈希码
哈希表解决哈希碰撞
-
将存储 boolean 值的地方改为 bucket 桶,桶中存储发生冲突的数据
-
bucket 的实现可以是 LinkedList, ArrayList, ArraySet 等等数据结构
-
Separate Chaining 链接法处理哈希冲突 hash collision:
1. 如果哈希码 h 对应的 bucket 是空的,创建新的包含目标元素的 list 并存储在 h 下标的数组中 2. 如果哈希码 h 对应的 bucket 不是空的,添加目标元素到已有 list 中(如果没有相同元素存在的话) -
性能:O(Q) --> Q 是 bucket 中最长 list 的长度 --> 也就是 O(N)
-
性能分析:对于 M 个 buckets 的哈希表,有 N 个元素要存储,平均情况就是 O(N/M), 为了能达到 O(1):每当 N/M > 1.5 的时候,使 M 翻倍, N/M 称为 load factor,代表哈希表有多满,这个过程称为扩容 resize,扩容本身的时间复杂度是 O(N)
哈希表特点:
- 良好的性能
- 不需要元素是可排序的
- 实现简单
Java 中的哈希表实现:
-
计算哈希值(Object#hashCode())
-
通过类似 Math.floorMod(hashcode, bucketSize) 方法求取数组下标
-
使用建议:
1.Key最好为不可变对象(可变对象可能导致 hashcode 变化) 2.重写 equals 方法时也要重写 hashCode 方法
Java8 String 类的 hashCode 功能特点
- 使用了 31 进制
-
h(s) = s0 × 31n-1 + s1 × 31n-2 + … + sn-1
-
h(s) = s0 × 126n-1 + s1 × 126n-2 + … + sn-1
-
126进制的问题:数据溢出导致的哈希碰撞
如:126^32 = 126^33 = 126^34 = ... 0. Thus upper characters are all multiplied by zero. See CS61C for more
- 缓存哈希码用于提升性能
@Override
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
如何生成好的哈希码
-
原则 --> 基于小的质数:
-
为什么是质数:
never even --> 避免数据溢出 对 bucket number 友好,详情请见 CS61B hw3 -
为什么要小:
减少计算消耗 -
示例:
- 计算 Collection 的哈希码:
@Override
public int hashCode() {
int hashCode = 1;
for (Object o : this) {
hashCode = hashCode * 31;
hashCode = hashCode + o.hashCode();
}
return hashCode;
}
- 计算递归数据结构的哈希码(如二叉树)
@Override
public int hashCode() {
if (this.value == null) {
return 0;
}
return this.value.hashCode() +
31 * this.left.hashCode() +
31 * 31 * this.right.hashCode();
}
总结
补充:
-
用开地址法(open addressing)解决哈希冲突
线性探针:冲突了就放在下一个不冲突的 bucket 中 对数探针:冲突了就依次尝试下4个、9个、16个、、、 其它方案(CS170)
参考: