【Algorithms 4】算法(第4版)学习笔记 14 - 3.4 散列表(Hash Tables)

前言

说起来挺丢脸的哈哈哈,在打开这一章之前,或者说是打开这一章节视频(官网)之前,我不知道 散列表 居然就是 Hash Tables,也就是每一个会敲代码的人应该都用过的哈希表(属实是汗流浃背了朋友们)。所以我特意在本篇的标题后面打了个括号:(Hash Tables)

继续批评一下自己,说明基础太薄弱了,还得继续学习。不过说实话散列表说起来确实是不太习惯,所以 在本篇中还是尽量使用哈希来代替中文翻译的散列

Ok,回到正题,本篇的主要内容包括:哈希函数分离链接Separate Chaining,这个书里面中文翻译成 拉链法)、线性探测 以及一点点关于哈希的 背景context) 介绍。

参考目录

  • B站 普林斯顿大学《Algorithms》视频课
    (请自行搜索。主要以该视频课顺序来进行笔记整理,课程讲述的教授本人是该书原版作者之一 Robert Sedgewick。)
  • 微信读书《算法(第4版)》
    (本文主要内容来自《3.4 散列表》)
  • 官方网站
    (有书本配套的内容以及代码)

学习笔记

注1:下面引用内容如无注明出处,均是书中摘录。
注2:所有 demo 演示均为视频 PPT demo 截图。
注3:如果 PPT 截图中没有翻译,基本会在下面进行汉化翻译(除非很简单一眼懂就不翻译了),因为内容比较多,本文不再一一说明。

开篇的时候,Prof. Sedgewick 是这样介绍哈希表的:

Which is another approach to implementing symbol tables that can also be very effective in a practical applications.
这是实现符号表的另一种方法,在实际应用中也非常有效。

0:符号表(ST)实现小结

在这里插入图片描述

哈希表的简单引出:

在这里插入图片描述

将项目存储在 键索引表 中(索引是基于键的一个函数)。
哈希函数。用于从键计算数组索引的方法。

涉及的问题

  • 计算哈希函数(设计并实现一个有效的哈希函数以将键转化为数组索引。)
  • 等价性测试:一种方法用于检查两个键是否相等。
  • 冲突解决:当两个键通过哈希函数映射到同一个数组索引时,所采用的算法和数据结构来处理这种冲突。

经典的空间-时间权衡

  • 无空间限制情况:可以使用简单的哈希函数,直接将键作为数组索引。
  • 无时间限制情况:可以采用顺序搜索进行最直观的冲突解决,无需特别的数据结构优化。
  • 空间和时间受限情况:在实际应用中,需要兼顾时间和空间效率,这时就需要运用到真正的哈希技术。

1:哈希函数

1.1:计算哈希函数

在这里插入图片描述

理想目标:对键进行均匀随机打乱以生成表索引。

  • 计算效率高。
  • 对于每个键,每个表索引具有同等的概率被生成。(这是一个经过深入研究但仍存在实际应用问题的难题)

示例 1:电话号码。

  • 不好:前三位。
  • 更好:最后三位数字。

美国的电话前三位是区号,可以考虑日常生活中的电话号码,前三位一般是所属运营商,通常情况下会使用后四位数字进行简单区分(例如取快递报手机号后四位)。

示例 2:社会保险号。

  • 不好:前三位。(573 = 加利福尼亚州,574 = 阿拉斯加(按地理区域内的时间顺序分配)
  • 更好:最后三位数字。

书里面的相关描述:

在这里插入图片描述

实际挑战:每种密钥类型需要不同的方法。

1.2:Java中的哈希码规范

在这里插入图片描述

所有 Java 类都继承了一个方法 hashCode(),该方法返回一个 32 位整数。

要求:如果 x.equals(y),则 (x.hashCode() == y.hashCode())
高度期望:如果 !x.equals(y),则 (x.hashCode() != y.hashCode())

默认实现:返回对象x的内存地址。
合法(但较差)的实现:始终返回 17。
定制化实现示例:Integer、Double、String、File、URL、Date 等。
用户自定义类型:对于用户自定义的类型(需要自行设计和实现)。

1.2.1:哈希码实现:integers、booleans、doubles

在这里插入图片描述

找了一下 jdk 包的源码:
Oracle OpenJDK version 11.0.6

java.lang.Integer#hashCode

在这里插入图片描述

java.lang.Boolean#hashCode

在这里插入图片描述

java.lang.Double#hashCode

在这里插入图片描述

1.2.2:哈希码实现:strings

在这里插入图片描述

霍纳法则(Horner’s method)用于计算长度为L的字符串哈希值:需要进行L次乘法和加法操作。

在这里插入图片描述

同样地,找了一下 jdk 包的源码(不过版本差异因此实现有出入,这里看一下 jdk 11 以及 jdk 8 的实现):
Oracle OpenJDK version 11.0.6

java.lang.String#hashCode
在这里插入图片描述

jdk 11 区分了不同的字符集 StringUTF16 以及 StringLatin1

java.lang.StringUTF16#hashCode

在这里插入图片描述

java.lang.StringLatin1#hashCode

在这里插入图片描述

来看看 jdk 8 的实现:
java version 1.8.0_192

Java.lang.String#hashCode

在这里插入图片描述

1.2.3:哈希码实现:用户自定义类型

在这里插入图片描述

1.3:哈希码设计

在这里插入图片描述

用户自定义类型的“标准”设计方法

  • 采用 31x + y 规则组合每个有意义的字段。
  • 如果字段是基本类型,则使用包装器类型的 hashCode() 方法计算。
  • 如果字段为 null,则返回0。
  • 如果字段是引用类型,则使用 hashCode() 方法(递归应用,对引用对象进行同样规则的计算)。
  • 如果字段是数组,则对每个元素应用此规则。(或者可以使用 Arrays.deepHashCode() 方法)

实际应用中,这个方法在 Java 库中被广泛采用,并且表现相当不错。
理论上,键被视为比特串;存在“通用”的哈希函数。

基本原则:计算哈希码时需要使用整个键;
若要获取最先进的哈希码计算方法,请咨询专家。

1.4:模块化哈希

在这里插入图片描述

1.5:均匀哈希假设

在这里插入图片描述

均匀哈希假设:任何给定的键都具有相等的概率映射到从 0 到 M-1 之间的任何一个哈希槽(桶)内。

桶与球模型:假设我们将球均匀随机地投入 M 个桶中。

生日问题:当随机向 M 个桶中抛掷约 √(πM/2) 次时,预计会出现至少有两个球落入同一桶的情况。

优惠券收集者问题:在大约 MlnM 次投掷之后,可以期望每个桶里至少有一个球。

负载均衡:在进行M次投掷操作后,预期最繁忙的桶所容纳的球的数量大约为 Θ(log M / log log M)

2:分离链接 / 独立链表 Separate Chaining

对应章节:3.4.2 基于拉链法的散列表

2.1:哈希碰撞(哈希冲突)

在这里插入图片描述

哈希碰撞。两个不相同的键被散列至同一个索引位置。

  • 根据生日问题理论,除非拥有极其庞大的(平方级别的)内存空间,否则无法完全避免碰撞。
  • 结合优惠券收集者问题和负载均衡原则,可以确保碰撞在各个索引位置上均匀分布。

挑战:如何高效地应对这种碰撞情况。

2.2:分离链接 ST

在这里插入图片描述

采用一个包含M(M小于N)个链表的数组实现。[该方法由H. P. Luhn于1953年在IBM公司提出]

  • 哈希函数:将键转换为介于 0 和 M-1 之间的某个整数索引 i。
  • 插入操作:将新元素添加到第 i 个链表的头部(若该元素不在链表中)。
  • 查找操作:仅需在第 i 个链表中进行搜索。

2.3:Java 实现:查找、插入

edu.princeton.cs.algs4.SeparateChainingHashST

在这里插入图片描述

edu.princeton.cs.algs4.SeparateChainingHashST#get

在这里插入图片描述

edu.princeton.cs.algs4.SeparateChainingHashST#put

在这里插入图片描述

2.4:分离链接分析

在这里插入图片描述

这里对应的是书本里面的几个假设以及性质:

假设J(均匀散列假设)。我们使用的散列函数能够均匀并独立地将所有的键散布于0到M-1之间。


命题K。在一张含有M条链表和N个键的的散列表中,(在假设J成立的前提下)任意一条链表中的键的数量均在N/M的常数因子范围内的概率无限趋向于1。


性质L。在一张含有M条链表和N个键的的散列表中,未命中查找和插入操作所需的比较次数为~N/M。

感兴趣的朋友可以去书里面看一下证明的方法,这里不再赘述。

2.5:(补充)Java 实现:调整大小

注:教授在视频里面没有讲到这一小节,根据资料和书本进行补充。

在这里插入图片描述

目标:使得链表平均长度(即元素总数N除以桶数量M)维持恒定。

  • 若链表平均长度 N / M 大于等于8,则将数组M的容量加倍。
  • 若链表平均长度 N / M 小于等于2,则将数组M的容量减半。
  • 每当调整数组大小时,必须对所有键执行重新哈希操作。

edu.princeton.cs.algs4.SeparateChainingHashST#resize

在这里插入图片描述

2.6:(补充)Java 实现:删除

注:教授在视频里面没有讲到这一小节,根据资料和书本进行补充。

在这里插入图片描述

edu.princeton.cs.algs4.SeparateChainingHashST#delete

在这里插入图片描述

2.7:ST 实现小结

在这里插入图片描述

3:线性探测 Linear Probing

对应章节:3.4.3 基于线性探测法的散列表

3.1:碰撞解决方案:开放式寻址

在这里插入图片描述

书中关于开放寻址线性探测的描述:

在这里插入图片描述

3.2:demo 演示:插入

在这里插入图片描述

哈希:通过计算将给定键映射为一个介于 0 和 M-1 之间的整数索引 i。
插入操作:若哈希表中索引为i的位置未被占用,则将元素存入该位置;否则,依次检查 i+1、i+2 等后续位置,直到找到一个空闲的槽位来存放该元素。

线性探测哈希表:

在这里插入图片描述

插入 S,hash(S) = 6:

在这里插入图片描述

插入 E,hash(E) = 10;
插入 A,hash(A) = 4;
插入 R,hash(R) = 14;
插入 S,hash(R) = 5;

在这里插入图片描述

插入 H,hash(A) = 4:

在这里插入图片描述


在这里插入图片描述

插入 X,hash(X) = 15;
插入 M,hash(M) = 1;
插入 P,hash(P) = 14:

在这里插入图片描述


在这里插入图片描述

插入 L,hash(L) = 6,冲突,最终插入 8:

在这里插入图片描述

书本的轨迹图可以参考一下:

在这里插入图片描述

3.3:demo 演示:搜索

在这里插入图片描述

哈希:通过计算将给定键转换为一个位于 0 到 M-1 之间的整数索引 i。
搜索操作:首先查找哈希表中对应索引 i 的槽位,若该位置已存在元素但不匹配目标键,则依次检查 i+1、i+2 等后续槽位,直至找到匹配项或确定不存在匹配项为止。

搜索 E,hash(E) = 10:

在这里插入图片描述

搜索 L,hash(L) = 6:

在这里插入图片描述


在这里插入图片描述

搜索 K,hash(K) = 5:

在这里插入图片描述

当探测遇到空槽:

在这里插入图片描述

3.4:线性探测小结

在这里插入图片描述

哈希:通过计算将给定键映射为一个介于 0 和 M-1 之间的整数索引 i。
插入操作:若哈希表中索引为i的位置未被占用,则将元素存入该位置;否则,依次检查 i+1、i+2 等后续位置,直到找到一个空闲的槽位来存放该元素。
搜索操作:首先查找哈希表中对应索引 i 的槽位,若该位置已存在元素但不匹配目标键,则依次检查 i+1、i+2 等后续槽位,直至找到匹配项或确定不存在匹配项为止。

注意:数组大小 M 必须大于键值对数量 N。

3.5:Java 实现:查找、插入

edu.princeton.cs.algs4.LinearProbingHashST

在这里插入图片描述

edu.princeton.cs.algs4.LinearProbingHashST#get

在这里插入图片描述

edu.princeton.cs.algs4.LinearProbingHashST#put

在这里插入图片描述

3.6:键簇

在这里插入图片描述

键簇:连续的一组数据项。
观察发现:新添加的键在哈希过程中,有较大几率被分配到已存在的大型数据簇的中部。

3.7:停车问题

在这里插入图片描述

3.8:线性探测分析

在这里插入图片描述

这里对应的是书本里面的性质:

在这里插入图片描述

3.9:(补充)Java 实现:调整大小

注:教授在视频里面没有讲到这一小节,根据资料和书本进行补充。

在这里插入图片描述

目标:使得链表平均长度(元素总数N除以桶数量M)不超过1/2。

  • 若链表平均长度 N / M 大于等于1/2,则将数组M的容量加倍。
  • 若链表平均长度 N / M 小于等于1/8,则将数组M的容量减半。
  • 每当需要调整数组大小时,必须对所有键执行重新哈希操作。

edu.princeton.cs.algs4.LinearProbingHashST#resize

在这里插入图片描述

3.10:(补充)Java 实现:删除

注:教授在视频里面没有讲到这一小节,根据资料和书本进行补充。

在这里插入图片描述

edu.princeton.cs.algs4.LinearProbingHashST#delete

源码太长了不好截图,直接贴一下:

	/**
     * Removes the specified key and its associated value from this symbol table
     * (if the key is in this symbol table).
     *
     * @param  key the key
     * @throws IllegalArgumentException if {@code key} is {@code null}
     */
    public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");
        if (!contains(key)) return;

        // find position i of key
        int i = hash(key);
        while (!key.equals(keys[i])) {
            i = (i + 1) % m;
        }

        // delete key and associated value
        keys[i] = null;
        vals[i] = null;

        // rehash all keys in same cluster
        i = (i + 1) % m;
        while (keys[i] != null) {
            // delete keys[i] and vals[i] and reinsert
            Key   keyToRehash = keys[i];
            Value valToRehash = vals[i];
            keys[i] = null;
            vals[i] = null;
            n--;
            put(keyToRehash, valToRehash);
            i = (i + 1) % m;
        }

        n--;

        // halves size of array if it's 12.5% full or less
        if (n > 0 && n <= m/8) resize(m/2);

        assert check();
    }

3.11:ST 实现小结

在这里插入图片描述

4:哈希背景

本小节省略了一些内容介绍,如果对原内容感兴趣的朋友建议移步原视频学习。

4.1 哈希算法变体

在这里插入图片描述

两探针哈希(Two-probe hashing)[ 分离链接(separate-chaining)变种 ]:

  • 将键散列到两个位置,并将键插入较短的那个链表中。
  • 可以将最长链的预期长度降低至 log log N。

双重哈希(Double hashing)[ 线性探测法(linear-probing)变种 ]:

  • 使用线性探测方法,但每次不只跳过1个位置,而是根据一个变量值跳跃。
  • 实质上消除了聚集现象。
  • 允许哈希表接近满载状态。
  • 实现删除操作较为复杂。

布谷鸟哈希(Cuckoo hashing)[ 线性探测法(linear-probing)变种 ]:

  • 将键散列到两个位置,将键插入其中一个空闲位置;如果该位置已被占用,则将被替换的键重新插入其备选位置(并递归执行此过程)。
  • 在最坏情况下搜索的时间复杂度为常数。

4.2 哈希表 vs 平衡查找树

在这里插入图片描述

哈希表

  • 编码更简单。
  • 对于无序键,没有有效的替代方案。
  • 对于简单键(只需执行几次算术操作,而非 log N 次比较),查找速度更快。
  • 在 Java 中对字符串提供更好的系统支持(例如,缓存哈希码)。

平衡搜索树

  • 提供更强的性能保证。
  • 支持有序集合操作(如插入、删除和查找)。
  • 实现 compareTo() 方法相对于 equals() 和 hashCode() 方法来说更容易正确完成。

Java 系统同时包含两者

  • 红黑树(Red-black BSTs):java.util.TreeMap 和 java.util.TreeSet。
  • 哈希表(Hash tables):java.util.HashMap 以及根据对象标识进行散列的 java.util.IdentityHashMap。
  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

MichelleChung

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

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

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

打赏作者

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

抵扣说明:

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

余额充值