这道题利用 Hash table 哈希表来做。如果不使用 hash table,进行暴力循环的话,时间复杂度较高 O ( N 2 ) O(N^2) O(N2)。
哈希表(Hash Table)可以被视为一种空间换时间的策略
哈希表利用哈希函数将键(Key)映射到值(Value),从而能够在常数时间内(O(1))完成查找和插入操作。
对于这道题目的求解思路:
首先创建一个空的 hash table,然后我们开始循环遍历向量,对当前元素,我们首先求出当前元素的补数(target-nums[i]
),然后将这个补数作为 key 去hash table中利用 find() 迭代器查找补数是否存在于 hash table中(hash table中的元素的 key 都是从向量中而来的),如果存在则返回下标pair,如果不存在,将当前元素加入到hash table中,数值作为key,下标作为value。如果经过循环遍历结束后程序没有返回,那么说明不存在这样的两个数,返回空向量。
不同的Hash函数是否会影响Hash Table的时间复杂度?
不同的哈希函数确实会影响哈希表的时间复杂度。虽然理想情况下,哈希表可以实现 O(1) 的时间复杂度,但这依赖于哈希函数的质量和冲突处理策略。如果哈希函数设计不当,可能会导致较多的哈希冲突,从而增加查找、插入和删除操作的平均时间复杂度。
哈希函数对哈希表性能的影响
-
理想情况:
在理想的情况下,一个好的哈希函数能够将键均匀地分布到哈希表中的各个位置,避免或最小化哈希冲突。在这种情况下,插入、查找和删除操作的时间复杂度为 O(1)。这是因为每个键都能迅速映射到唯一的存储位置,且冲突很少甚至没有。 -
非理想情况:
如果哈希函数设计不佳,可能会导致大量的键被映射到同一个索引位置,形成哈希冲突。当发生冲突时,查找和插入操作的复杂度将会随冲突数量增加而增加。- 对于链地址法:当发生冲突时,需要遍历链表中的所有元素,因此最坏情况下,时间复杂度会退化为 O(n),其中 n 是链表的长度。
- 对于开放地址法:如果冲突频繁发生,找到一个空位置可能需要多次探测,时间复杂度也会增加,甚至退化为 O(n)。
不同哈希函数的特点及对性能的影响
-
简单哈希函数:
简单哈希函数可能通过取模运算将键映射到哈希表的数组中。这种函数计算速度快,但容易产生冲突,特别是在输入数据有某种模式或相似性时。例子:
def simple_hash(key): return key % table_size
如果输入的键是连续的整数,比如 [1, 2, 3, 4],使用简单哈希函数会让这些键映射到连续的数组索引中,冲突的概率非常高,导致时间复杂度大幅增加。
-
复杂哈希函数:
复杂的哈希函数会考虑输入的特性并生成更均匀分布的哈希值。例如,MD5、SHA 等加密哈希函数虽然生成的哈希值均匀,但它们的计算成本较高,可能会导致时间开销增加,不适合高性能需求的场景。例子:
import hashlib def complex_hash(key): return int(hashlib.md5(key.encode()).hexdigest(), 16) % table_size
尽管这种哈希函数能提供较好的分布,但其计算时间较长,对于插入和查找大量数据的场景不太适合。
-
均匀分布的哈希函数:
一个设计良好的哈希函数应该能够均匀地将输入分布到哈希表的各个位置。这样可以有效减少冲突,确保哈希表的时间复杂度接近 O(1)。常见的良好哈希函数考虑了输入的多个因素,如字符串中的字符顺序、长度等。例子:
Python 的内置hash()
函数经过精心设计,能够为大多数输入生成良好的哈希值,减少冲突。
影响时间复杂度的其他因素
除了哈希函数的设计外,还有其他一些因素会影响哈希表的时间复杂度:
-
负载因子:
负载因子越高,哈希冲突的概率就越大。因此,在负载因子过高时,哈希表通常会进行扩容(rehashing),以确保时间复杂度不显著退化。 -
冲突解决策略:
冲突解决的策略(如链地址法或开放地址法)也会影响查找和插入的时间复杂度。链地址法在冲突较少时依然可以保持 O(1) 的性能,而开放地址法在冲突严重时可能导致更多的探测,从而增加时间复杂度。
总结
虽然哈希表理论上能够在常数时间内完成查找和插入操作(O(1)),但实际性能依赖于以下因素:
- 哈希函数的设计:不良的哈希函数会导致较多的冲突,从而使查找和插入的时间复杂度退化为 O(n)。
- 冲突的处理策略:冲突解决的方式(如链地址法或开放地址法)决定了在发生冲突时的额外开销。
- 负载因子:过高的负载因子会增加冲突的几率,因此需要动态扩展哈希表来保持性能。
一个均匀分布、计算高效的哈希函数能够显著减少冲突,保持哈希表在大多数情况下的 O(1) 性能。
需要用到的头文件 #include <unordered_map>
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> num; //初始化一个hash table存储元素值和下标
for(int i = 0; i < nums.size(); i++) {
int complement = target - nums[i]; //求当前元素的补数
if(num.find(complement) != num.end()) { //说明在目前的hash table中找到了补数
return {num[complement], i}; //返回这对补数的下标
}
//否则,将当前元素加入hash table中,有可能当前元素可能作为之后某个元素的补数满足题目要求
num[nums[i]] = i; //因为题目要求返回的是这两个数的下标,所以将下标作为value存储
}
//如果到这里执行完for循环并且还没有return,说明没有找到符合条件的两数,返回空向量
return {};
}
};
这是Leetcode中经典的“两数之和”问题,题目要求在数组中找到两个数,使它们的和等于目标值,并返回它们的索引。
使用哈希表来提高查找效率:
代码解释:
- 使用
unordered_map
来存储数组中的元素值和它们的索引,以便我们能快速找到目标数对应的补数。 - 遍历数组时,对于每一个数,我们先计算它的补数,然后检查补数是否已经存在于哈希表中。
- 如果补数存在于哈希表中,则返回补数的索引和当前数的索引。
- 如果遍历结束仍未找到符合条件的两个数,则返回空数组(在实际 Leetcode 题目中这个分支通常不会执行,因为题目保证有解)。
这种解法的时间复杂度是 O(n),其中 n 是数组的长度。