【CS61B学习笔记】Lecture 20 - Hashing

546 阅读3分钟

之前实现的 Sets(或Map) 集合的总结

Sets.PNG

存在的问题:

  • 基于搜索树实现的 Sets 的缺陷
  1. 存储的元素必须是可排序的(comparable)
  2. 是否可以有更好的性能【比O(log N)更好】

解决方案

1. 创建 boolean 索引数组来存储数据

DataIndexedArray.PNG

缺点: 及其浪费空间,只支持整型数据

2. 在方案 1 的基础上支持字符串类型

  • 首先从支持小写英语单词开始
  • 假设 a = 1, b = 2, c = 3, ... , z = 26
  • 进制转换
  • cat(27进制)=(3 * 27227^{2})+(1*27127^{1})+(20*27027^{0})= 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 进制 以上方案存在的最大问题是:整型溢出导致了哈希碰撞,上面的方法其实都是为了计算哈希码

哈希算法的两个挑战:

  1. 如何减少哈希碰撞
  2. 如何计算一个对象的哈希码

哈希表解决哈希碰撞

  • 将存储 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)

哈希表特点:

  1. 良好的性能
  2. 不需要元素是可排序的
  3. 实现简单

Java 中的哈希表实现:

  • 计算哈希值(Object#hashCode())

  • 通过类似 Math.floorMod(hashcode, bucketSize) 方法求取数组下标

  • 使用建议:

      1.Key最好为不可变对象(可变对象可能导致 hashcode 变化) 
      2.重写 equals 方法时也要重写 hashCode 方法
      
    

Java8 String 类的 hashCode 功能特点

  1. 使用了 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
    
  1. 缓存哈希码用于提升性能
@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
    
  • 为什么要小:

      减少计算消耗
    
  • 示例:

  1. 计算 Collection 的哈希码:
@Override
public int hashCode() {
   int hashCode = 1;
   for (Object o : this) {
       hashCode = hashCode * 31;
       hashCode = hashCode + o.hashCode();
   	}
return hashCode;
}
  1. 计算递归数据结构的哈希码(如二叉树)
@Override
public int hashCode() {
   if (this.value == null) {
       return 0;
   }
   return  this.value.hashCode() +
   	31 * this.left.hashCode() +
   	31 * 31 * this.right.hashCode();
}

总结

Hash Tables in Java.PNG

补充:

  • 用开地址法(open addressing)解决哈希冲突

      线性探针:冲突了就放在下一个不冲突的 bucket 中
      对数探针:冲突了就依次尝试下4个、9个、16个、、、
      其它方案(CS170)
    

参考:

CS61B Lecture20 PPT

hashing study guide

CS61B 在线教材

CS61B 主页