为什么即使哈希函数可能不是O(1),也要通过键O(1)访问字典的元素?


75

我了解了如何通过密钥访问您的收藏集。但是,哈希函数本身在幕后有很多操作,不是吗?

假设您有一个非常有效的很好的哈希函数,它仍然可能需要执行许多操作。

可以解释吗?


39
注记法是关于the growth使用不同输入量度复杂度的方法。这与您有多少操作无关。例如:使用1值,您有x秒,使用n值,您需要roughly x*n秒=> O(n)。x可能将许多操作组合在一起。
汗到

33
数据结构不具有O表示法复杂性,对其进行操作即可。
user6144226

3
那我们要做什么呢?
帕特里克·霍夫曼

@PatrickHofman它确实解释了有关字典上O(1)复杂性的一些事实,也许相关是一个更好的词。
user6144226

1
“很多操作”和O(1)完全兼容-O(1)或恒定时间意味着,随着元素数量接近无穷大,存在一些限制执行时间的有限常数。该常数可以是任意大的-使用保证在一年内完成的哈希函数不会阻止系统成为O(1)。
彼得斯(Peteris)'16

Answers:


118

HashFunc本身有很多幕后操作

确实是这样。但是,这些操作的数量取决于的大小,而不取决于插入键的哈希表的大小:对于具有十个或10个表的键,计算哈希函数的操作数是相同的一万个条目。

这就是为什么通常将哈希函数的调用视为O(1)的原因。这对于固定大小的键(整数值和固定长度的字符串)可以很好地工作。它还为具有实际上限的大小可变的按键提供了不错的近似值。

但是,通常,哈希表的访问时间为O(k),其中k哈希键大小的上限为。


8
还应考虑到,n除非至少一个项目至少由log(n)位表示,否则不可能有一个包含不同项目的哈希表。
欧文

可悲的是,如果不限制输入的位大小,则所有操作都是指数运算。但这不是一个非常有趣或有用的结果,对吗?
Joker_vD

1
@Owen:在内存中的哈希表中,也不可能有比适合指针大小的变量的唯一分配键更多的项。
约书亚

the number of these operations depends on the size of the key以及散列数据的大小。
埃里克(Eric J.)

k不需要是一个上限。查找时间在密钥大小中是线性的,因此密钥大小确实O(k)在哪里k。如果k被理解为上限,则实际上是O(1)
usr

136

O(1)并不意味着即时。O(1)表示常量,不考虑数据大小。散列函数需要花费一定的时间,但是该时间不会随集合的大小扩展。


1
但它可以编写一个哈希函数依赖于集合的大小。这将是愚蠢的,人为的,但您可以做到。搜索散列集的声明实际上以计算散列为O(1)的假设为前提,这实际上总是(但不一定)是这种情况。
Servy '16

@Servy甚至不一定都那么愚蠢和人为。想要允许包含相同项目的两个列表进行自身比较的自定义列表实现可以重写GetHashCode()以某种方式组合这些项目的哈希码。如果我要实现这样的类,那么对于最初的实现,我将GetHashCode()完全像那样实现。我当然也会在以后更改它。

1
@hvd这将是一个O(m)哈希,其中m是内部集合的大小。它仍然与外部集合的大小(基于实际哈希的结构)无关。您需要让集合中的项目查看它们当前基于的同一基于散列的集合中的所有项目,以使这些项目的哈希码具有O(n)(或n的任何函数)。 将是非常愚蠢和人为的。
Servy '16

1
@Servy哦,那就是你的意思。是的,那太愚蠢了。:)我无法提出您可能想要的任何遥不可及的方案。

@Servy哈希的一般要点是避免O(n)的搜索时间,因此创建O(n)的哈希函数将完全无法达到目的。您可以执行此操作,但这就像使用Peano编号递归实现加法:可能,但并不实际。
巴马尔

15

这意味着无论您的集合有多大,都将花费几乎相同的时间来检索其任何成员。

因此,换句话说,具有5个成员的Dictionary将假设coud需要大约0.002 ms来访问其中一个成员,而由25个成员组成的Dictionary应当花费相似的时间。大O表示算法的复杂度超过集合大小,而不是实际的语句或执行的函数


1
但是同时,如果您的哈希函数确实很糟糕,则可能会在存储桶中包含很多值,因此O(1)将不再成立
klappvisor

3
@klappvisor,没必要具有功能不好。输入数据可能是经过精心设计的。这就是为什么O(1)在这里是摊余的复杂性,而不是“真实”的复杂性。
n0rd

这并不意味着每个成员都将花费相同的时间,而是(大致)意味着该访问时间的上限不会随集合的大小而增加。考虑哈希表如何处理歧义冲突。类似地,查找项目以查找二叉搜索树是O(log2 n),因为最坏的情况是log2的大小为N,但是靠近根的项目所花的时间比叶项目少。
蓬松的

@ n0rd这实际上不是O(1)的“摊销”澄清的意思。它是摊销O(1)的事实是要考虑以下事实:大约有1 / N的添加项(如果要添加到集合中)将需要重新分配新的支持数组,这是O(N)操作,因此您可以在O(N)的时间内执行N次加法,以实现摊销的O(1)加法,而单个加法实际上也是O(N)(未摊销时)。假设哈希值分布充分,这是对渐进复杂度的单独说明。
Servy '16

12

如果将字典/地图实现为HashMap,则它的最佳情况下复杂度O(1),因为在没有键冲突的情况下,最好的情况是它需要精确计算要检索的关键元素的哈希码。

一个哈希地图可能有最坏情况下运行复杂O(n),如果你有很多关键的碰撞或非常糟糕的散列函数,因为在这种情况下,它会降低到保存数据的整个阵列的线性扫描。

另外,O(1)并不意味着立即,而是意味着它具有恒定的数量。因此,为字典选择正确的实现方式也可能取决于集合中元素的数量,因为如果只有很少的条目,则该函数具有非常高的恒定成本将变得更加糟糕。

这就是为什么字典/地图在不同情况下的实现方式不同的原因。对于Java,有多种不同的实现,C ++使用红色/黑色树,等等。您是根据数据数量和最佳/平均/最坏情况下的运行效率来选择它们的。


1
不必那样,例如Java 8HashMap在检测到多个冲突的情况下诉诸平衡树。
快速

@acelent可能是正确的,但后来它不再是经典的哈希映射。地图/字典有许多不同的实现,正是这种情况。我修改了答案以指出这一点。
Martin C.

6

从理论上讲,它仍然是O(n),因为在最坏的情况下,您的所有数据最终都可能具有相同的哈希值,并捆绑在一起,在这种情况下,您必须线性遍历所有数据。


3

请参阅文章“ O(1)访问时间”是什么意思?

散列函数中的操作数无关紧要,只要集合中每个元素花费相同(恒定)的时间即可。例如,访问2个元素的集合中的一个元素需要0.01毫秒,但是访问2,000,000,000个元素的集合中的一个元素也需要0.01毫秒。尽管哈希函数可以包含数百个if语句和多个计算。


6
时间不变,不是线性的。
库萨兰达

散列函数是否需要包含更多的“ if语句和多个计算”以产生足够长的散列值来唯一标识20亿个元素(而不是200个元素)?
Damian Yerrick '16

1

从文档:

由于T:System.Collections.Generic.Dictionary`2类被实现为哈希表,因此使用键检索值非常快,接近O(1)。

因此它可以是O(1),但可能更慢。在这里,您可以找到有关哈希表性能的另一个线程:哈希表-为什么它比数组快?


1

一旦考虑到越来越大的字典占用更多的内存,进一步降低缓存层次结构并最终减慢磁盘上的交换空间这一事实,就很难说这确实是O(1)。字典的性能会随着它的增大而变慢,这可能会增加O(log N)的时间复杂度。不相信我吗 自己尝试使用1、100、1000、10000等字典元素(最多1000亿),并测量实际查找一个元素所需的时间。

但是,如果您做一个简化的假设,即系统中的所有内存都是随机访问内存,并且可以在恒定时间内访问,那么您可以声明字典为O(1)。这种假设是很普遍的,即使对于具有磁盘交换空间的任何计算机而言并非如此,并且在各种情况下,考虑到CPU缓存的不同级别,这种假设仍然值得商de。


您确实有一点要说,但是当我们谈论算法复杂性时,假设使用完美的硬件确实有意义。关键是要定义算法的特征,而不是不同的现实生活中的硬件实现。此外,如果您有足够大的数据,那么算法的复杂性实际上是最重要的:是O(1),(logN),O(n)还是O(n ^ 2)。
Tero Lahtinen

1
还有更大的字典与哈希键冲突的问题。一旦足够大,大多数新条目将与现有条目发生冲突,从而在每个哈希存储桶中进行线性搜索,并最终显示为O(n)。除非您使哈希键随着大小的增加而增长更长的时间...但是您也不会具有O(1)。我同意在实践中您可以将其视为固定时间,但是我宁愿不要使用正式的O标记,因为这只是对于足够小的尺寸的粗略近似,而不是任何尺寸的正式证明。
艾德·阿维斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.