十五、这就是哈希表

208 阅读4分钟

哈希表Hash

目标:

  • 基础
  • 哈希函数的设计
  • 哈希冲突处理
    • 链地址方法Separate Chaining
  • 实现自己的哈希表
  • 哈希表的动态空间处理与复杂度

1.1 认识哈希表

  • 每一个字符都和一个索引相对应(哈希函数),O(1)的查找操作
  • 很难保证每一个“键”通过哈希函数的转换,对应不同的“索引”
  • 哈希冲突
    • 哈希函数的设计师很重要的
    • “键”通过哈希函数得到的“索引”分布越均匀越好
  • 哈希表充分体现了算法设计领域的经典思想:空间换时间。哈希表是时间和空间之间的平衡

1.2 哈希函数的设计

​ Java中的每一个Object类,默认有hashCode()的实现。

  • 整型

    • 小范围正整数直接使用

    • 小范围负整数进行偏移 -100100--> 0200

    • 大整数

      测试代码如下:

      int a = 42;
      System.out.println(((Integer)a).hashCode());
      
      int b = -42;
      System.out.println(((Integer)b).hashCode());
      
  • 浮点型

    在计算机中都是32位或者64位的二进制表示,只不过计算机解析成了浮点数

    image-20200916164235894

    转成整型处理。

    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),牺牲了顺序性。集合和映射的实现,可以是链表,可以是树,也可以是哈希表。
      • 有序集合,有序映射:平衡树
      • 无序集合,无需映射:哈希表