数据结构--哈希表

第一版

未考虑 hash 码的生成,假定该 hash 码由我们提供

hashMap--数组+链表实现,链表过长之后会转化为红黑树

 public class HashTable {
 
     // 节点类
     static class Entry {
         int hash; // 哈希码
         Object key; // 键
         Object value; // 值
         Entry next;
 
         public Entry(int hash, Object key, Object value) {
             this.hash = hash;
             this.key = key;
             this.value = value;
         }
     }
 
     Entry[] table = new Entry[16];
     int size = 0; // 元素个数
     float loadFactor = 0.75f; // 12 阈值
     int threshold = (int) (loadFactor * table.length);
 
     /* 求模运算替换为位运算
         - 前提:数组长度是 2 的 n 次方
         - hash % 数组长度 等价于 hash & (数组长度-1)
      */
 
     // 根据 hash 码获取 value
     Object get(int hash, Object key) {
         int idx = hash & (table.length - 1);
         if (table[idx] == null) {
             return null;
         }
         Entry p = table[idx];
         while (p != null) {
             if (p.key.equals(key)) {
                 return p.value;
             }
             p = p.next;
         }
         return null;
     }
 
     // 向 hash 表存入新 key value,如果 key 重复,则更新 value
     void put(int hash, Object key, Object value) {
         int idx = hash & (table.length - 1);
         if (table[idx] == null) {
             // 1. idx 处有空位, 直接新增
             table[idx] = new Entry(hash, key, value);
         } else {
             // 2. idx 处无空位, 沿链表查找 有重复key更新,否则新增
             Entry p = table[idx];
             while (true) {
                 if (p.key.equals(key)) {
                     p.value = value; // 更新
                     return;
                 }
                 if (p.next == null) {
                     break;
                 }
                 p = p.next;
             }
             p.next = new Entry(hash, key, value); // 新增
         }
         size++;
         if (size > threshold) {
             resize();
         }
     }
 
     private void resize() {
         Entry[] newTable = new Entry[table.length << 1];
         for (int i = 0; i < table.length; i++) {
             Entry p = table[i]; // 拿到每个链表头
             if (p != null) {
             /*
                 拆分链表,移动到新数组,拆分规律
                 * 一个链表最多拆成两个
                 * hash & table.length == 0 的一组
                 * hash & table.length != 0 的一组
                                           p
                 0->8->16->24->32->40->48->null
                             a
                 0->16->32->48->null
                         b
                 8->24->40->null
              */
                 Entry a = null;
                 Entry b = null;
                 Entry aHead = null;
                 Entry bHead = null;
                 while (p != null) {
                     if ((p.hash & table.length) == 0) {
                         if (a != null) {
                             a.next = p;
                         } else {
                             aHead = p;
                         }
                         a = p; // 分配到a
                     } else {
                         if (b != null) {
                             b.next = p;
                         } else {
                             bHead = p;
                         }
                         b = p; // 分配到b
                     }
                     p = p.next;
                 }
                 // 规律: a 链表保持索引位置不变,b 链表索引位置+table.length
                 if (a != null) {
                     a.next = null;
                     newTable[i] = aHead;
                 }
                 if (b != null) {
                     b.next = null;
                     newTable[i + table.length] = bHead;
                 }
             }
         }
         table = newTable;
         threshold = (int) (loadFactor * table.length);
     }
 
     // 根据 hash 码删除,返回删除的 value
     Object remove(int hash, Object key) {
         int idx = hash & (table.length - 1);
         if (table[idx] == null) {
             return null;
         }
         Entry p = table[idx];
         Entry prev = null;
         while (p != null) {
             if (p.key.equals(key)) {
                 // 找到了, 删除
                 if (prev == null) { // 链表头
                     table[idx] = p.next;
                 } else { // 非链表头
                     prev.next = p.next;
                 }
                 size--;
                 return p.value;
             }
             prev = p;
             p = p.next;
         }
         return null;
     }
 }

生成 hashCode

hash 算法是将任意对象,分配一个编号的过程,其中编号是一个有限范围内的数字(如 int 范围内)

Object.hashCode

  • Object 的 hashCode 方法默认是生成随机数作为 hash 值(会缓存在对象头当中)

  • 缺点是包含相同的不同对象,他们的 hashCode 不一样,不能够用 hash 值来反映对象的特征,因此诸多子类都会重写 hashCode 方法

String.hashCode

public static void main(String[] args) {
    String s1 = "bac";                     
    String s2 = new String("abc");         

    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());

    // 原则:值相同的字符串生成相同的 hash 码, 尽量让值不同的字符串生成不同的 hash 码
    /*
    对于 abc  a * 100 + b * 10 + c
    对于 bac  b * 100 + a * 10 + c
     */
    int hash = 0;
    for (int i = 0; i < s1.length(); i++) {
        char c = s1.charAt(i);
        System.out.println((int) c);
        // (a*10 + b)*10 + c  ==>  a*100 + b*10 + c  2^5
        hash = (hash << 5) - hash + c;     
    }
    System.out.println(hash);
}

  • 经验表明如果每次乘的是较大质数,可以有更好地降低 hash 冲突,因此改【乘 10】为【乘 31】

  • 【乘 31】可以等价为【乘 32 - hash】,进一步可以转为更高效地【左移5位 - hash】

 检查 hash 表的分散性

public void print() {
    int[] sum = new int[table.length];
    for (int i = 0; i < table.length; i++) {
        Entry p = table[i];
        while (p != null) {
            sum[i]++;
            p = p.next;
        }
    }
    System.out.println(Arrays.toString(sum));

    Map<Integer, Long> result = Arrays.stream(sum).boxed()
        .collect(Collectors.groupingBy(s -> s, Collectors.counting()));
    System.out.println(result);
}

测试

public static void main(String[] args) throws IOException {
    // 测试 Object.hashCode
    HashTable table = new HashTable();
    for (int i = 0; i < 200000; i++) {
        Object obj = new Object();
        table.put(obj, obj);
    }
    table.print();
    
    // 测试 String.hashCode
    table = new HashTable();
    List<String> strings = Files.readAllLines(Path.of("words"));
    for (String string : strings) {
        table.put(string, string);
    }
    table.print();
}

MurmurHash

思考

  1. 我们的代码里使用了尾插法,如果改成头插法呢?

  2. JDK 的 HashMap 中采用了将对象 hashCode 高低位相互异或的方式减少冲突,怎么理解

  3. 我们的 HashTable 中表格容量是 2 的 n 次方,很多优化都是基于这个前提,能否不用 2 的 n 次方作为表格容量?

  4. JDK 的 HashMap 在链表长度过长会转换成红黑树,对此你怎么看

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ray-国

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值