我的意思是,我们应该使用数组,而不是链表桶。
一切的利弊,取决于许多因素。
数组最大的两个问题:
更改容量涉及将所有内容复制到另一个内存区域
-
您必须选择:
a) Element*s 的数组,在表操作期间添加一个额外间接,并为每个非空存储桶分配一个额外内存 具有相关的堆管理开销
b) Elements 的数组,使得预先存在的 Elements 迭代器/指针/引用被其他节点上的某些操作无效 (例如insert)(链表方法 - 或上面的 2a - 不需要使这些无效)
...将忽略一些关于数组间接的较小设计选择...
从 1. 开始减少复制的实用方法包括保留多余的容量(即当前未使用的内存用于预期或已擦除的元素),以及 - 如果sizeof(Element) 太多 大于sizeof(Element*) - 你被推向arrays-of-Element*s(有“2a”问题)而不是Element[]s/2b。
还有其他几个答案声称在数组中擦除比链表更昂贵,但相反的通常是正确的:搜索连续的Elements 比扫描链表更快(更少的步骤在代码中,对缓存更友好),一旦找到,您可以将最后一个数组 Element 或 Element* 复制到正在擦除的数组上,然后减小大小。
如果关心的是数组的大小,那么这意味着我们有太多的冲突,所以我们已经遇到了哈希函数的问题,而不是我们解决冲突的方式。我是不是误会了什么?
要回答这个问题,让我们看看一个出色的哈希函数会发生什么。使用加密强度哈希将一百万个元素打包到一百万个桶中,我的程序的几次运行计算了 0、1、2 等元素哈希产生的桶的数量......
0=367790 1=367843 2=184192 3=61200 4=15370 5=3035 6=486 7=71 8=11 9=2
0=367664 1=367788 2=184377 3=61424 4=15231 5=2933 6=497 7=75 8=10 10=1
0=367717 1=368151 2=183837 3=61328 4=15300 5=3104 6=486 7=64 8=10 9=3
如果我们将其增加到 1 亿个元素 - 仍然使用负载因子 1.0:
0=36787653 1=36788486 2=18394273 3=6130573 4=1532728 5=306937 6=51005 7=7264 8=968 9=101 10=11 11=1
我们可以看到比率非常稳定。即使负载因子为 1.0(C++ 的 unordered_set 和 -map 的默认最大值),预计 36.8% 的存储桶是空的,另外 36.8% 处理一个 Element,18.4% 处理 2 个元素等等。对于任何给定的数组大小调整逻辑,您可以轻松了解它需要多久调整一次大小(并可能复制元素)。 您说得对,对于这种理想的加密哈希情况,它看起来不错,并且如果您进行大量查找或迭代,它可能比链表更好。强>
但是,高质量的哈希在 CPU 时间上相对昂贵,因此支持哈希函数的通用哈希表通常非常弱:例如std::hash<int> 的 C++ 标准库实现返回它们的参数是很常见的,而 MS Visual C++ 的 std::hash<std::string> 选择沿 string 间隔排列的 10 个字符合并到散列值中,无论 string 有多长.
显然,实现的经验是这种弱但快速的哈希函数和链表(或树)的组合以处理更大的冲突倾向,平均而言运行得更快 - 并且具有令人讨厌的糟糕性能的用户对抗表现更少 -用于日常关键和要求。