TL; DR:O(1)
如果从通用哈希函数系列中随机选择哈希函数,哈希表可保证预期的最坏情况时间。预期的最坏情况与平均情况不同。
免责声明:我没有正式证O(1)
明哈希表是,因为请看Coursera的这段视频[ 1 ]。我也不讨论摊销哈希表方面。这与关于散列和冲突的讨论正交。
在其他答案和评论中,我对此主题感到非常令人困惑,并且将在此较长的答案中尝试纠正其中的一些问题。
关于最坏情况的推理
有不同类型的最坏情况分析。到目前为止,大多数答案在这里所做的分析不是最坏的情况,而是平均情况 [ 2 ]。平均案例分析往往更实用。也许您的算法有一个糟糕的最坏情况输入,但实际上对所有其他可能的输入都适用。底线是您的运行时取决于数据集您正在运行。
考虑以下get
哈希表方法的伪代码。在这里,我假设我们通过链接来处理冲突,因此表的每个条目都是(key,value)
成对的链接列表。我们还假设存储桶的数量m
是固定的,但是是O(n)
,其中n
输入中的元素数量是。
function get(a: Table with m buckets, k: Key being looked up)
bucket <- compute hash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
正如其他答案所指出的那样,这是在平均O(1)
和最坏的情况下进行的O(n)
。我们可以在此处通过挑战略述证明。挑战如下:
(1)您将哈希表算法交给对手。
(2)对手可以根据需要进行学习和准备。
(3)最后,对手会为您提供一个大小输入,n
供您插入表格中。
问题是:您的哈希表在对手输入上的速度有多快?
从步骤(1),对手知道您的哈希函数;在步骤(2)中,对手可以通过例如随机计算一堆元素的哈希来制作n
具有相同元素的列表hash modulo m
。然后在(3)中他们可以给您该列表。但是请注意,由于所有n
元素都散列到同一存储桶中,因此您的算法将需要O(n)
时间来遍历该存储桶中的链表。无论我们重试挑战多少次,对手总是会获胜,这就是最坏情况下您的算法有多糟糕O(n)
。
O(1)为何是哈希?
在上一个挑战中使我们脱颖而出的是,对手非常了解我们的哈希函数,并可以利用该知识来编写最差的输入。如果实际上有一组哈希函数而不是总是使用一个固定的哈希函数,H
该算法可以在运行时从中随机选择,该怎么办?如果您好奇的话,它H
被称为哈希函数的通用家族 [ 3 ]。好吧,让我们尝试为此添加一些随机性。
首先,假设我们的哈希表还包含一个seed r
,并且r
在构造时被分配给一个随机数。我们分配一次,然后针对该哈希表实例进行固定。现在,让我们重新访问我们的伪代码。
function get(a: Table with m buckets and seed r, k: Key being looked up)
rHash <- H[r]
bucket <- compute rHash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
如果我们再尝试一次挑战:从步骤(1)开始,对手可以知道我们拥有的所有哈希函数H
,但是现在我们使用的特定哈希函数取决于r
。的值r
对我们的结构是私有的,对手无法在运行时对其进行检查,也无法提前进行预测,因此他无法编制一份对我们始终不利的清单。让我们假设,在步骤(2)对手选择一个功能,hash
在H
随机的,然后他工艺品的列表n
下的碰撞hash modulo m
,并发送步骤(3),穿越手指在运行时H[r]
将是相同的hash
,他们选择。
这对对手来说是一个很大的赌注,他精心制作的列表会与该列表发生冲突hash
,但是在中的任何其他哈希函数下,它只是一个随机输入H
。如果他赢了这个赌注,我们的运行时间将是最糟糕的情况O(n)
,但是如果他输了,那么我们将获得随机输入,这需要平均O(1)
时间。实际上,在大多数情况下,对手会失败,他在每次|H|
挑战中只赢一次,所以我们可以做到|H|
很大。
将该结果与先前的算法进行对比,在先前的算法中,对手总是赢得挑战。有点费力,但是由于大多数情况下对手会失败,并且对手可能尝试的所有可能策略都是如此,因此可以得出结论,尽管最坏的情况是O(n)
,但实际上预期的最坏的情况是O(1)
。
同样,这不是正式证明。从这种预期的最坏情况分析中得到的保证是,我们的运行时间现在独立于任何特定的输入。这是一个真正的随机保证,与平均案例分析相反,在平均案例分析中,我们发现有动机的对手很容易做出错误的输入。