通常说在哈希表中插入和查找字符串是 O(1)。但是字符串的哈希键是如何产生的呢?为什么不是 O(L),字符串的长度?我很清楚为什么整数是 O(1),而不是字符串。
通常引用的 O(1) 表示时间不会随着容器中元素的数量而增长。正如您所说,从字符串生成哈希值的时间本身可能不是 O(1) 字符串长度 - 尽管对于某些实现它是:例如 Microsoft 的 C++ std::hash<std::string>有:
size_t _Val = 2166136261U;
size_t _First = 0;
size_t _Last = _Keyval.size();
size_t _Stride = 1 + _Last / 10;
if (_Stride < _Last)
_Last -= _Stride;
for(; _First < _Last; _First += _Stride)
_Val = 16777619U * _Val ^ (size_t)_Keyval[_First];
return (_Val);
_Stride 是字符串长度的十分之一,因此 固定 个相距很远的字符将包含在哈希值中。这样的哈希函数在字符串长度上是O(1)。
GCC 的 C++ 标准库采用了不同的方法:至少在 v4.7.2 中,它通过 _Hash_impl 支持类调用 static 非成员函数 _Hash_bytes,该函数执行包含每个字节的 Murmur 哈希.因此,GCC 的 hash<std::string> 在字符串长度上是 O(N) 。
- GCC 对冲突最小化的更高优先级也体现在它对
std::unordered_set 和std::unordered_map 使用质数的桶中,MS 的实现没有这样做 - 至少在 VS2013/VC12 之前;总而言之,MS 的方法对于不易发生冲突且负载系数较低的键来说会更轻/更快,但会更早且更大幅度地降级。
Java 中的 hashTable 和 C++ 中的 unordered_map 生成字符串哈希键的方式有什么区别?
C++ 标准没有指定字符串的散列方式 - 由各个编译器实现决定。因此,不同的编译器会做出不同的妥协——甚至是同一编译器的不同版本。
文档 David Pérez Cabrera 的答案链接解释了 Java 中的 hashCode 函数:
返回此字符串的哈希码。 String 对象的哈希码计算为
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用int算术,其中s[i]是字符串的ith字符,n是字符串的长度,^表示求幂。 (空字符串的哈希值为零。)
字符串的长度显然是 O(N)。
快速返回...
通常说在哈希表中插入和查找字符串是 O(1)。
...一个“关键”;-P 洞察力是,在许多问题领域中,已知字符串的实际长度不会有显着变化,或者最坏情况长度的散列仍然足够快。考虑一个人或公司的名称、街道地址、来自某些源代码的标识符、编程语言关键字、产品/书籍/CD 等名称:您可以预期十亿个键需要大约一百万倍的内存来存储第一千。使用哈希表,对整个数据集的大多数操作预计会花费一百万倍的时间。这将在 100 年后和今天一样真实。重要的是,如果某些请求与单个密钥相关,则执行时间不会比过去使用一千个密钥的时间长(假设有足够的 RAM,并忽略 CPU 缓存影响)——当然,如果它是一个长密钥它可能需要比短键更长的时间,如果您有超低延迟或硬实时要求,您可能会关心。但是,尽管拥有一百万倍的数据,使用随机键的请求的平均吞吐量将保持不变。
仅当您的问题域的密钥大小差异很大,并且考虑到您的性能需求,密钥散列时间很重要,或者您预计平均密钥大小会随着时间的推移而增加(例如,如果密钥是视频流,并且每隔几年人们就会提高分辨率和帧速率,从而导致密钥大小呈指数增长),您是否需要密切关注散列(和密钥比较)成本。