共计 1904 个字符,预计需要花费 5 分钟才能阅读完成。
为了响应陈皓老师的 ARTS 挑战,之后打算每周至少在 LetCode 上做一道算法题。那么就从最简单的开始做起吧。
LetCode 算法题 – 两数之和
给定一个整数数组 nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
LetCode 题解
解题思路有两种,一种是遍历两次去查找元素的暴力解法;另一种则要用到哈希表对时间复杂度进行优化:LetCode 官方题解
暴力解法在此就不过多叙述,这篇文章我想重点来讲讲 Hash Table 这种数据结构。
Hash Table – 哈希表
Hash Table 中文译名 散列表 ,又名 哈希表 或者 Hash 表。散列表利用了数组支持按照下标随机访问数据的特性,由数组扩展而来。
基本概念
首先在这里定义一下下文中有关 Hash Table 的基本概念:
- 键(
key
):用于操作数据的标示,例如 PHP 数组中的索引,或者字符串键等等 - 槽(
slot/bucket
):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器 - 哈希函数(
hash function
):将key
映射 (map) 到数据应该存放的slot
所在位置的函数 - 哈希冲突(hash collision):哈希函数将两个不同的
key
映射到同一个索引的情况
散列思想
想要弄清散列表,就一定要先了解数组的实现:1. 使用连续的内存空间储存 2. 储存一组相同类型的数据
数组能够具有按照下标随机访问数据的特性,也是通过上面两条实现。首先获取数据类型的长度 type_size
,然后根据数组下标 k
和数组的首地址 base_address
,利用寻址公式 a[k]_address = base_address + k * type_size
即可实现时间复杂度 O(1) 的情况下随机访问数据。
而散列表的思想,便是通过散列函数将键转换为数组下标,然后根据数组下标存取数据,即 hash(key) => index
。实际上,PHP 关联数组的设计便是利用了散列表这一思想。在 PHP 关联数组中,每个键关联一个值,即 key => value
,通过合理的散列函数的设计,便可以将 key
从字符串转换为数组的数字下标 index
,利用上文所述寻址公式到数组中存取 arr[index]
。
散列函数的设计
散列函数的基本要求有以下三点:
- 散列函数计算得到的散列值是一个非负整数
- 相同的
key
根据相同的散列函数得到的value
相同 - 不同的
key
根据相同的散列函数得到的value
不同
第一条很好理解,由于数组下标都是非负整数,散列函数计算得到的值也应该为非负整数;第二条讲的是散列函数具有幂等性,使用同样的参数通过散列函数,都应该得到相同的返回值;而第三点,实际情况下想要完全实现并不是一件简单的事情。即使是一些知名的(例如 MD5)哈希算法,也无法避免这种散列冲突,再加上数组的存储空间有限,也会加大散列冲突的概率。
目前解决散列冲突常见的方法有 开放寻址法 和 链表法,而 PHP 在处理散列冲突时则使用了链表法,这里简单介绍一下。
散列表中,每个槽
slot
对应一条链表,当出现散列冲突时,将值存放到对应槽中对链表中。
插入时,只需要通过散列函数 hash()
计算 key
对应的散列槽位 slot
,将其插入对应链表中即可。所以插入的时间复杂度为 O(1)。
查找或者删除时,时间复杂度与链表的长度 k 成正比,也就是 O(k)。对于散列比较均匀的散列函数,理论上 k = n/m。其中 n 表示散列中数据的个数,m 表示散列表中槽 slot
的个数。
值得注意的是,如果别有用心的人设计大量不同的 key
经过散列后都储存到同一个 slot
时,哈希表便会退化为一个链表,操作链表的时间复杂度为 O(n)。也就是说,如果原来散列表中有 10 万条数据,退化为链表之后,查询的效率就下降了 10 万倍。这样可能会导致大量 CPU 和线程资源用来处理查询操作,导致无法响应其他请求,使服务器遭受 DoS (服务拒绝攻击)。
因此,设计散列函数时,要保证以下两点:
- 生成的值要尽可能的随机且均匀分布
- 设计不能太复杂
其中设计太复杂是因为通过散列函数计算对应槽位时,也会有时间消耗。
小结
本篇文章主要是从 LetCode 的“两数之和”引发的思考,由于缺乏对数据结构的了解,看到这个题目时的思路只局限在暴力解法,很难想到利用哈希表查找时时间复杂度为 O(1) 的特性。希望之后能够对数据结构更深入了解,开拓自己的思路。
参考资料:
- 极客时间专栏 – 王争 –《数据结构与算法之美》
- 深入理解 PHP 内核 – 哈希表(HashTable)