哈希表真的可以是O(1)吗?

时间:2010-05-05 07:45:33

标签: algorithm performance language-agnostic big-o hashtable

哈希表可以达到O(1)似乎是常识,但这对我来说从来没有意义。有人可以解释一下吗?以下是两种情况:

一个。 该值是一个小于哈希表大小的int。因此,该值是它自己的哈希值,因此没有哈希表。但如果有的话,那将是O(1)并且仍然效率低下。

B中。 您必须计算值的哈希值。在这种情况下,查找数据大小的顺序为O(n)。在你做O(n)工作之后,查找可能是O(1),但在我看来仍然是O(n)。

除非你有一个完美的哈希表或一个大的哈希表,否则每个桶可能有几个项目。因此,无论如何,它会在某个时刻转变为一个小的线性搜索。

我认为哈希表很棒,但我没有得到O(1)的名称,除非它只是理论上的。

维基百科的article for hash tables始终引用常量查找时间,完全忽略了哈希函数的代价。这真的是一个公平的措施吗?


编辑:总结一下我学到的东西:

  • 技术上是正确的,因为哈希函数不需要使用密钥中的所有信息,因此可以是恒定时间,并且因为足够大的表可以将冲突降低到接近恒定的时间。

  • 在实践中确实如此,因为只要选择散列函数和表大小来最小化冲突,它就会发挥作用,即使这通常意味着不使用常量时间散列函数。

8 个答案:

答案 0 :(得分:54)

这里有两个变量,m和n,其中m是输入的长度,n是散列中的项目数。

O(1)查找性能声明至少做出两个假设:

  • 您的对象在O(1)时间内可以相等。
  • 哈希冲突很少。

如果您的对象是可变大小并且相等检查需要查看所有位,则性能将变为O(m)。然而,散列函数不必是O(m) - 它可以是O(1)。与加密散列不同,在字典中使用的散列函数不必查看输入中的每个位以计算散列。实现可以自由查看固定数量的位。

对于足够多的项目,项目数量将大于可能的哈希值,然后您将获得冲突,导致性能上升到O(1)以上,例如O(n)用于简单的链接列表遍历(或如果两个假设都是假的,则为O(n * m)。

在实践中,尽管O(1)声称在技术上是错误的,但对于许多现实世界的情况,近似是真的,特别是那些上述假设所持有的情况。

答案 1 :(得分:19)

  

您必须计算哈希值,因此查找数据大小的顺序为O(n)。在你做O(n)工作之后,查找可能是O(1),但在我看来仍然是O(n)。

什么?散列单个元素需要恒定的时间。为什么会是其他什么?如果你要插入n元素,那么是的,你必须计算n哈希值,这需要线性时间......看一个元素,你计算一个哈希值寻找,然后找到适当的桶。您不会重新计算散列表中已有的所有内容的哈希值。

  

除非你有一个完美的哈希表或一个大的哈希表,否则每个桶可能有几个项目,所以无论如何它都会转换成一个小的线性搜索。

不一定。存储桶不一定必须是列表或数组,它们可以是任何容器类型,例如平衡的BST。这意味着O(log n)最坏的情况。但这就是为什么选择一个良好的散列函数以避免将太多元素放入一个桶中的重要性。正如KennyTM指出的那样,平均而言,即使偶尔你需要挖掘一个桶,你仍然可以获得O(1)时间。

哈希表的权衡当然是空间复杂性。你是时间交易空间,这似乎是计算科学的常见情况。


您提到在其他一条评论中使用字符串作为键。你关心计算字符串散列所需的时间,因为它包含几个字符?正如其他人再次指出的那样,你不一定需要查看所有字符来计算哈希值,尽管如果你这样做可能会产生更好的哈希值。在这种情况下,如果您的密钥中平均有m个字符,并且您使用了所有字符来计算哈希值,那么我认为您是对的,查找将采用O(m)。如果m >> n那么您可能会遇到问题。在这种情况下,你可能会更好地使用BST。或者选择更便宜的散列函数。

答案 2 :(得分:4)

散列是固定大小的 - 查找适当的散列桶是固定成本操作。这意味着它是O(1)。

计算哈希不一定是特别昂贵的操作 - 我们这里不讨论加密哈希函数。但那是由by。哈希函数计算本身不依赖于元素的数量 n ;虽然它可能取决于元素中数据的大小,但这不是 n 所指的。因此哈希的计算不依赖于 n ,也是O(1)。

答案 3 :(得分:2)

只有当表中只有一定数量的键并且做出其他一些假设时,散列才是O(1)。但在这种情况下它有优势。

如果您的密钥具有n位表示,则您的散列函数可以使用这些位的1,2,... n。考虑使用1位的哈希函数。评估肯定是O(1)。但是您只是将密钥空间划分为2.因此,您将多达2 ^(n-1)个密钥映射到同一个bin中。使用BST搜索,如果几乎已满,则需要n-1步才能找到特定的密钥。

你可以对此进行扩展,看看如果你的哈希函数使用K位,你的bin大小是2 ^(n-k)。

所以K位散列函数==>不超过2 ^ K个有效箱==>每个bin最多2 ^(n-K)个n位密钥==> (n-K)步骤(BST)解决冲突。实际上大多数散列函数都不那么有效"并且需要/使用多于K比特来产生2 ^ k个箱。所以即使这是乐观的。

您可以这样查看 - 在最坏的情况下,您需要步骤才能唯一地区分n位的一对键。实际上没有办法绕过这个信息理论限制,哈希表。或者没有。

但是,这不是你何时/何时使用哈希表!

复杂性分析假设对于n位密钥,表中可以有O(2 ^ n)个密钥(例如,所有可能密钥的1/4)。但是大多数情况下,如果不是所有时间我们都使用哈希表,我们在表中只有一个恒定数量的n位密钥。如果你只想在表中使用一定数量的键,比如说C是你的最大数,那么就可以形成一个O(C)二进制的哈希表,它保证了预期的持续冲突(具有良好的哈希函数);以及使用密钥中n位的~logC的散列函数。然后每个查询都是O(logC)= O(1)。这就是人们声称"哈希表访问是O(1)" /

的方式

这里有几个捕获 - 首先,说你不需要所有的位可能只是一个计费技巧。首先,你不能真正将键值传递给散列函数,因为这将在内存中移动n位(O(n))。所以你需要做,例如参考传递。但你仍然需要将它存储在已经是O(n)操作的地方;你只是不向哈希收费;你的整体计算任务无法避免这种情况。其次,你进行散列,找到bin,找到1个以上的键;你的成本取决于你的解决方法 - 如果你做基于比较(BST或List),你将有O(n)操作(调用键是n位);如果你做第二个哈希,那么,如果第二个哈希发生冲突,你会遇到同样的问题。所以O(1)不是100%保证,除非你没有碰撞(你可以通过拥有一个比键更多的箱子,但仍然可以提高机会)。

考虑替代方案,例如BST,在这种情况下。有C键,所以平衡的BST深度为O(logC),因此搜索需要O(logC)步骤。然而,在这种情况下的比较将是O(n)操作...因此在这种情况下看起来散列是更好的选择。

答案 4 :(得分:1)

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的{​​{1}}个元素的列表。随机计算一堆元素的哈希值;然后在(3)中他们可以给您该列表。但是请注意,由于所有hash modulo m元素都散列到同一个存储桶,因此您的算法将花费n的时间来遍历该存储桶中的链接列表。无论我们重试挑战多少次,对手总是会获胜,这就是您的算法最糟糕的情况,O(n)

O(1)为何是哈希?

在上一个挑战中使我们脱颖而出的是,对手非常了解我们的哈希函数,并且可以使用该知识来进行最坏的输入。 如果我们实际上有一组哈希函数O(n)可以在运行时随机选择,而不是总是使用一个固定的哈希函数怎么办?如果您感到好奇,可以将H称为通用散列函数族 [3]。好吧,让我们尝试为此添加一些 randomness

首先假设我们的哈希表还包含种子H,并且r在构造时被分配给一个随机数。我们分配一次,然后针对该哈希表实例进行固定。现在,让我们重新访问我们的伪代码。

r

如果我们再尝试一次挑战:从步骤(1)开始,对手可以知道我们在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 中拥有的所有哈希函数,但是现在我们使用的特定哈希函数取决于Hr的值对于我们的结构是私有的,对手无法在运行时对其进行检查,也无法提前对其进行预测,因此他无法编制一份对我们始终不利的列表。假设在步骤(2)中,对手在r中随机选择一个函数hash,然后在H下制作n冲突的列表,并将其发送给步骤(3),在运行时hash modulo m和他们选择的H[r]相同的地方交叉手指。

这对对手来说是一个很大的赌注,他精心制作的列表在hash下发生了冲突,但是在hash中任何其他哈希函数下只是一个随机输入。如果他赢了这个赌注,我们的运行时间将是最糟糕的情况H,就像以前一样,但是如果他输了,那么我们将得到一个随机输入,平均花费O(n)时间。实际上,在大多数情况下,对手会输掉比赛,他每O(1)次挑战就只赢一次,我们可以使|H|很大。

将此结果与先前的算法进行对比,在先前的算法中,对手总是赢得挑战。稍微挥霍一下手,但是由于对手大多数时候都会失败,因此对手可以尝试的所有可能策略都是如此,尽管最坏的情况是|H|,但<实际上,strong>预期的最坏情况是O(n)


同样,这不是正式证明。从预期的最坏情况分析中得到的保证是,我们的运行时间现在独立于任何特定输入。这是真正的随机保证,与平均案例分析相反,在平均案例分析中,我们表明有动机的对手很容易制造出错误的输入。

答案 5 :(得分:0)

有两种设置可以获得 O(1)最坏情况时间。

  1. 如果您的设置是静态的,那么FKS哈希将为您提供最坏情况 O(1)保证。但正如您所指出的,您的设置不是静态的。
  2. 如果您使用Cuckoo哈希,则查询和删除是 O(1) 最坏的情况,但插入只是 O(1)预期。如果你有一个插入总数的上限,并且将表格大小设置为大约25%,那么Cuckoo散列效果很好。
  3. here

    复制

答案 6 :(得分:0)

似乎基于这里的讨论,如果X是(表格中的元素数量/#箱子的数量)的上限,那么更好的答案是O(log(X)),假设有效实现bin查找。< / p>

答案 7 :(得分:0)

  

A。该值是一个小于哈希表大小的整数。因此,该值是其自己的哈希,因此没有哈希表。但是,如果有,那将是O(1),但效率仍然很低。

在这种情况下,您可以将键平凡地映射到不同的存储桶,因此与哈希表相比,数组似乎是更好的数据结构选择。尽管如此,效率低下并不会随着表的大小而增加。

(您可能仍会使用哈希表,因为您不相信int会随着程序的发展而保持小于表的大小,您想要使代码在这种关系不成立时可能可重用,或者只是不想让阅读/维护代码的人不得不浪费精力来理解和维护关系。

  

B。您必须计算值的哈希值。在这种情况下,对于要查找的数据大小,顺序为O(n)。在您完成O(n)工作后,查找可能是O(1),但在我眼里仍然是O(n)。

我们需要区分密钥的大小(例如以字节为单位)和哈希表中存储的密钥数量的大小。声称哈希表提供O(1)操作意味着随着密钥数量从数百个增加, (插入/擦除/查找)操作不会趋于进一步减速 数以千计至数百万至数十亿(至少如果不是在同等快速的存储中访问/更新所有数据的情况下,至少不是这样),无论是RAM还是磁盘-缓存效果可能会发挥作用,但即使是最坏情况下的缓存未命中的代价也往往要高一些最佳命中率的常数倍)。

考虑一本电话簿:您那里的名称可能很长,但是无论该簿中有100个名称还是1000万个名称,平均名称长度都将保持一致,这是历史上最糟糕的情况。

  

Adolph Blaine查尔斯·戴维·厄尔·弗雷德里克·杰拉尔德·休伯特·艾尔文·约翰·肯尼思·劳埃德·马丁·尼罗·奥利弗·保罗·昆西·兰道夫·谢尔曼·托马斯·安卡斯·维克托·威廉·谢尔克斯·西恩斯

... wc告诉我这是215个字符-不是密钥长度的上限 ,但是我们不必担心会出现大量

这适用于大多数现实世界的哈希表:平均密钥长度不会随着使用的密钥数量而增加。有例外,例如,密钥创建例程可能返回嵌入递增整数的字符串,但是即使如此,每次将密钥数量增加一个数量级时,密钥长度也只会增加1个字符:

不重要。

还可以根据固定大小的键数据创建哈希。例如,Microsoft的Visual C ++附带了std::hash<std::string>的标准库实现,该实现创建的哈希仅包含沿字符串均匀间隔的十个字节,因此,如果字符串仅在其他索引处变化,则会发生冲突(因此在实践中不O(1)在冲突后搜索方面的行为),但是创建哈希的时间有一个硬上限。

  

除非您有完美的哈希表或大型哈希表,否则每个存储桶中可能有几项。因此,无论如何它会演变成小的线性搜索。

通常是正确的,但是关于哈希表的很棒的事情是,在那些“小的线性搜索”期间访问的键的数量是-对于单独链接冲突方法-哈希表的功能 负载系数 (桶的键之比)。

例如,负载因子为1.0时,与线性搜索的长度无关,平均约为1.58,而与键的数量无关(请参见my answer here)。对于closed hashing,这有点复杂,但是当负载系数不太高时,情况并不会更糟。

  

从技术上讲,这是正确的,因为不需要散列函数来使用键中的所有信息,因此可以是恒定时间,并且因为足够大的表可以使冲突降低到接近恒定时间。

这种错点了。最终,任何类型的关联数据结构最终都必须在键的每个部分上进行操作(有时可能仅从键的一部分确定不平等,但通常需要考虑到平等性)。至少,它可以哈希一次密钥并存储哈希值,如果它使用足够强大的哈希函数-例如64位MD5-实际上,它甚至可能忽略将两个密钥散列为相同值的可能性(我工作的公司对分布式数据库所做的正是这样:与WAN范围内的网络传输相比,散列生成时间仍然微不足道)。因此,对于处理密钥的成本并没有太多的困扰:不管数据结构如何,密钥的存储都是固有的,并且如上所述-随着密钥的增加,平均而言不会变得更糟。

至于足够大的哈希表可以减少冲突,这也没有抓住重点。对于单独的链,在任何给定的负载系数下,您仍然具有恒定的平均碰撞链长度-当负载系数较高时,该长度才刚长,并且这种关系是非线性的。 SO用户Hans对my answer also linked above的评论是:

  

以非空桶为条件的平均桶长度是衡量效率的更好方法。它是a /(1-e ^ {-a})[其中a是负载系数,e是2.71828 ...]

因此,负载因数 决定了在插入/擦除/查找操作期间必须搜索的平均冲突键数。对于单独的链接,当负载系数很低时,它不只是趋于恒定,而是始终恒定。对于开放式寻址,尽管您的主张具有一定的有效性:某些冲突元素将重定向到备用存储桶,然后可能会干扰其他键的操作,因此,在较高的负载因子(尤其是> 0.8或.9)下,碰撞链长度会变得更糟。

  

在实践中确实如此,因为随着时间的推移,只要选择哈希函数和表大小以最大程度地减少冲突,就可以解决问题,尽管这通常意味着不使用恒定时间哈希函数。

好吧,表的大小应该导致合理的负载因子,因为可以选择紧密散列或单独的链接,但是如果散列函数有点弱并且键不是很随机的话,那么桶数就是素数通常也有助于减少冲突(hash-value % table-size然后回绕,以使仅对哈希值中的高阶位或两位进行更改仍然可以解析为在哈希表的不同部分伪随机分布的存储桶)。