CS61B - Lec 24 - Hashing

本文深入探讨哈希表的原理,从数据索引数组到处理字符串碰撞,讲解整型溢出与哈希码计算。分析哈希表在Java中的实现细节,包括碰撞处理、性能优化与resize策略,以及最佳与最坏情况下的复杂度。


终于讲到了Hashing。早在leetcode第一题twoSum就见到了Hashing Table,但是一直不知道为什么它这么优秀,这次课终于学习了原理。

Data Index Arrays

我们上几次课实现的结构,最优只能实现O(log N)的查找复杂度。还能不能再快呢?

和之前的UnionFind类似,要用数组的Index代表存储的值,寻找某个值时,就只有O(1)的复杂度。(不过这样实现的是Set,无法存储顺序)
在这里插入图片描述
但是这样做还有很多优化的地方。首先,每次需要建一个特别庞大的数组。其次,只能存储数字。

English String Set

延续之前的思路,能否用Index代表字符串呢?

可以的!每个char都对应着一个ASCII码。再将每个char视作一位,以126进制就理论上可以表示任何字符串。
在这里插入图片描述

Integer Overflow and Hash Codes

现在需要考虑int类型溢出的问题。
在这里插入图片描述
比如上图中的例子,两个不同的字符串通过ASCII码126进制转化为整形,均超出了整形范围,就会发生溢出,变成同样的值,称之为Collision。
事实上,Collision是无法避免的,因为字符串的个数是无穷的,没有一种数据类型能穷尽所有,因此需要另外想解决办法。

对于每个字符串,java有一套自己的转化方法,称之为Hash Code。

Hash Tables: Handling Collisions

实际上,处理Collisions的方式是这样的:一个坑放很多值,也就是一个坑放一个list。
在这里插入图片描述
某x需要放到h位置,就是将x加入位于h的list中。比如上图例子,abomamora和abevilish的hash code都是111239444,那么该处就会有一个包含他们两个的list。

值得一提的是,查找或者插入操作,复杂度都为O(Q), Q为最长的list,并不是O(1)。这是因为必须遍历list,才能确定有无x或者需不需要插入x。
在这里插入图片描述
下面再优化一下另一个问题:开始新建的数组太大,占用内存空间。方法是取余,如下图,先新建一个大小为10的数组。一个字符串计算的Hash Code是2348762878,那么将它除以十取余,放到数组中,之后再插入字符串,按照上面的list方法操作。这就是Hash Table。
在这里插入图片描述
当然,这样会使list变得很长,明显需要resize。

Hash Table Performance

在这里插入图片描述
如图,如果初始的数组大小只有5,那么当加入N个数据时,Q最小是N/5, 最大是N,查找复杂度达到了O(N),这不是一个理想的复杂度。解决方法就是resize。

设定当前数组的长度是M,插入的数据数量为N,那么当N/M > 1.5时,将数组大小翻倍,变成2M。这样无论何时,N/M永远是一个常数,也就是达到了O(1)的最佳查找时间。注意是最佳状态,最坏情况仍然是O(N)。

值得注意的是,resize之后,所有的数据都要重新分配位置,因为每次是计算hash code - 除以M取余 - 放入指定位置这样的过程。M变化,数据的位置就会变,可见resize需要的操作不少。但是同ArrayList一样,即便加入resize,平均复杂度仍然是O(1),之后会有讨论。

Hash Tables in Java

上面提到,最佳的hash table的复杂度是O(1),但是最坏的情况下,即有一堆hash code相同的数据,复杂度仍然能达到O(N),因此hash code计算也是很重要的一环。下面看看java中实际是如何计算的。

我们之前学习过,Java中所有的类都继承了Object类,而Object类中就包含一个hashCode()方法。就是说,对所有的类,java都有一个相对应的hash code。
在这里插入图片描述
String类中Override了hashCode方法,很像之前提到的进制计算方法。
在这里插入图片描述
实际的hashCode方法中会计算出负数,为了增大范围,下面看一个问题。如果金苹果的hashCode是-1,那么把他放在0, 1, 2, 3哪个位置呢?
在这里插入图片描述
应该放在3,原因如上图。-1和0相邻,所以此时-1取余,会计算出-1,结果不对,此时应该取模,java中使用floorMod来实现的。
在这里插入图片描述
在这里插入图片描述
两点注意事项:

  1. 不要在HashSet和HashMap中存储可变的类。如果类中变量改变,则HashCode就变了,可能造成数据丢失
  2. 不要单独override equals方法,必须和hashCode方法一起override。

Java 8中String类的hashCode方法。有两点不同:

  1. 以31为模
  2. 用一个变量:cachedHashValue保存了计算所得的hashCode,作为缓存,再调用hashCode方法时,就不用重新计算一遍了。

在这里插入图片描述
为什么以31为模?答案是以126为模,虽然包括了全部的ASCII码,显得每个数据都是独立的,减少了数据的hash code重复的概率。然而,通过list以及resize方法,unique就不那么重要,以126为模造成的大量的数据溢出问题才是重点。溢出之后会产生大量相同的hash code,list长度很大,复杂度就升高了。因此,取一个小点的模效果会更好,更接近"randomness"。

并且,还要保证模是质数。(这个还要查查资料)
在这里插入图片描述

Summary

这节课内容还是挺多的。
在这里插入图片描述

### 常见哈希函数及其实现 #### 1. **除留取余法** 这是最简单的一种哈希函数,其基本形式为 `H(key) = key % m`,其中 `key` 是输入的关键字,`m` 是哈希表的大小。这种方法的优点在于计算简便,但在实际应用中容易受到关键字分布的影响。 ```python def hash_modulo(key, table_size): return key % table_size ``` 此方法适用于关键字均匀分布在整数范围内的场景[^1]。 --- #### 2. **乘法散列法** 该方法通过将关键字与某个常量相乘后再提取部分位的方式生成哈希值。具体公式如下: \[ H(\text{key}) = \lfloor A \cdot (\text{key} \mod W) \rfloor \% M \] 其中 \(A\) 是一个小于 1 的正实数,\(W\) 和 \(M\) 分别表示机器字长和哈希表长度。这种算法能够有效降低因模运算带来的偏移影响。 ```python import math def hash_multiplication(key, table_size, constant=0.618033): scaled_key = key * constant fractional_part = scaled_key - math.floor(scaled_key) return int(table_size * fractional_part) ``` 它广泛应用于需要高随机性的场合,比如密码学领域[^4]。 --- #### 3. **双重哈希(Double Hashing)** 当单个哈希函数无法满足需求时,可以采用双哈希技术解决冲突问题。其核心思想是在发生碰撞后利用另一个辅助哈希函数调整探查序列。例如, \[ H_i = (H_1(\text{key}) + i \times H_2(\text{key})) \% M \] 这里 \(i\) 表示尝试次数,而 \(H_1\) 和 \(H_2\) 则分别代表主次两套不同的哈希规则[^2]。 ```python def double_hashing(key, primary_func, secondary_func, table_size): h1 = primary_func(key, table_size) h2 = secondary_func(key, table_size) def probe(i): return (h1 + i * h2) % table_size return probe ``` 这种方式特别适合那些对性能敏感的应用程序,如数据库管理系统中的索引设计。 --- #### 4. **FNV 哈希算法** 快速非加密型哈希(Fast Non-cryptographic Hash Algorithm,FNV)因其速度优势成为许多现代软件系统的默认选项之一。它的更新过程遵循以下模式: \[ \begin{aligned} &\text{hash} := FNV\_offset \\ &\forall c \in \text{input}: \\ &\quad \text{hash} *= FNV\_prime\\ &\quad \text{hash} ^= c\\ \end{aligned} \] 最终返回经过多次迭代后的累积结果作为目标位置指示符[^3]。 ```python def fnv_hash(input_string, prime=16777619, offset_basis=2166136261): hash_value = offset_basis for byte in input_string.encode('utf-8'): hash_value = hash_value ^ byte hash_value = hash_value * prime return hash_value & 0xFFFFFFFF ``` 由于具备良好的抗聚类特性,因此非常适合文件路径名解析或者网络包分类等领域。 --- #### 5. **MD5/SHA 系列消息摘要算法** 尽管严格意义上讲这些属于安全散列家族成员而非传统意义上的定位工具,但由于它们产生的固定长度指纹同样可用于构建分布式缓存机制或其他一致性哈希方案之中,故也在此提及一下。 ```python import hashlib def md5_hash(input_data): hasher = hashlib.md5() hasher.update(str(input_data).encode('utf-8')) return int(hasher.hexdigest(), 16) % (2**32) ``` 这类强健型解决方案更多服务于身份验证协议或是防止篡改检测等方面的需求。 --- ### 应用场景分析 每种类型的哈希函数都有各自适用的最佳环境: - 对于小型项目或资源受限设备来说,简单的线性同余变换可能已经足够; - 如果追求更高的质量,则推荐选用基于数学理论精心构造出来的复杂模型; - 而涉及到隐私保护或者是大规模并发访问控制的时候,则务必考虑引入工业标准级的安全框架支持。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值