为啥即使哈希函数可能不是 O(1),也要通过键 O(1) 访问字典的元素?
Posted
技术标签:
【中文标题】为啥即使哈希函数可能不是 O(1),也要通过键 O(1) 访问字典的元素?【英文标题】:Why is accessing an element of a dictionary by key O(1) even though the hash function may not be O(1)?为什么即使哈希函数可能不是 O(1),也要通过键 O(1) 访问字典的元素? 【发布时间】:2016-09-17 19:44:42 【问题描述】:我知道您如何通过密钥访问您的收藏。但是,hash 函数本身在幕后有很多操作,不是吗?
假设你有一个非常有效的很好的哈希函数,它仍然可能需要很多操作。
这可以解释一下吗?
【问题讨论】:
O 表示法是关于使用不同输入测量the growth
的复杂性。这与您有多少操作无关。例如:对于 1 值,您有 x
秒,对于 n
值,您需要 roughly
x*n
秒 => O (n)。 x
可以是许多操作的组合。
数据结构没有 O 符号复杂性,对它们的操作有。
那我们要做什么操作呢?
OP 明确表示按键访问是有问题的操作。
“很多操作”和 O(1) 完全兼容 - O(1) 或常数时间意味着,随着元素数量接近无穷大,存在一些限制执行的有限常数时间。该常数可以任意大 - 使用保证在一年内完成的哈希函数不会阻止系统成为 O(1)。
【参考方案1】:
O(1)
并不意味着即时。 O(1)
表示常量不考虑数据大小。哈希函数需要一定的时间,但该时间不会随着集合的大小而变化。
【讨论】:
但是是可以编写一个依赖于集合大小的散列函数。这将是愚蠢的,做作的,但你可以做到。搜索散列集的说法实际上是以计算散列为 O(1) 的假设为前提的,这实际上总是,但不一定是这种情况。 @Servy 甚至不一定那么愚蠢和做作。一个自定义列表实现希望允许两个包含相同项目的列表相互比较,可以覆盖GetHashCode()
以某种方式组合项目的哈希码。如果我要实现这样一个类,对于初始实现,我会完全像那样实现GetHashCode()
。当然,我稍后也会改变它。
@hvd 这将是一个 O(m) 哈希,其中 m 是内部集合的大小。它仍然与外部集合的大小(实际的基于哈希的结构)无关。您需要让集合中的项目查看 它们当前所在的同一基于哈希的集合的所有项目,以使这些项目具有 O(n)(或n) 为他们的哈希码。 那将是非常愚蠢和做作的。
@Servy 哦,这就是你的意思。是的,那将是愚蠢的。 :) 我想不出任何你可能想要的看似合理的场景。
@Servy 散列的一般要点是避免 O(n) 搜索时间,因此创建 O(n) 的散列函数将完全违背目的。你可以这样做,但这就像用皮亚诺数递归地实现加法一样:可能,但并不实际。【参考方案2】:
HashFunc
本身有很多幕后操作
这当然是真的。但是,这些操作的数量取决于 key 的大小,而不是插入 key 的 散列表 的大小:要计算的操作数对于有十个或一万个条目的表中的键,哈希函数是相同的。
这就是为什么哈希函数的调用通常被认为是 O(1)。这适用于固定大小的键(整数值和固定长度的字符串)。它还为具有实际上限的可变大小键提供了一个不错的近似值。
不过,一般来说,哈希表的访问时间是 O(k),其中k
是哈希键大小的上限。
【讨论】:
还要考虑到不可能有一个包含n
不同项的哈希表,除非至少一项由至少log(n)
位表示。
遗憾的是,如果您不限制输入的位大小,所有操作都是指数级的。但这不是一个非常有趣或有用的结果,对吧?
@Owen:内存中的哈希表中的项目也不可能超过适合指针大小变量的唯一分配键。
the number of these operations depends on the size of the key
以及被散列的数据的大小。
k
不需要是上限。查找时间在密钥大小中是线性,因此它确实是O(k)
,其中k
是密钥大小。如果k
被理解为一个上限,那么它实际上是O(1)
。【参考方案3】:
这意味着无论您的集合有多大,检索其任何成员都需要几乎相同的时间。
所以换句话说,有 5 个成员的字典会让我们说访问其中一个成员需要大约 0.002 毫秒,而 25 个成员的字典应该需要类似的东西。大 O 意味着算法复杂度超过集合大小,而不是实际执行的语句或函数
【讨论】:
但是同时如果你的哈希函数真的很糟糕,你最终可能会在桶中得到很多值,所以 O(1) 将不再成立 @klappvisor,没必要有功能是不好的。输入数据可能是精心制作的。这就是为什么这里的 O(1) 是 摊销 复杂性,而不是“真正的”复杂性。 这并不意味着每个成员将花费相同的时间,它只是(大致)意味着该访问时间的上限不会随着集合的大小而增长。考虑一下哈希表如何处理消除歧义的冲突。类似地,为二叉搜索树查找项目是 O(log2 n),因为最坏的情况是大小为 N 的 log2,但例如,靠近根的项目将比叶项目花费更少的时间。 @n0rd 这实际上并不是 O(1) 的“摊销”澄清的意思。它是一个摊销 O(1) 的事实是为了说明这样一个事实,即大约 1/N 的添加(如果您要添加到集合中)将需要重新分配一个新的后备数组,这是一个 O(N) 操作,因此您可以在 O(N) 时间内执行 N 次加法,用于摊销 O(1) 加法,而单次加法实际上也是 O(N) (未摊销时)。这是对渐近复杂性的单独说明,它假设哈希分布足够好。【参考方案4】:如果字典/映射被实现为HashMap
,它的最佳情况复杂度为O(1)
,因为在最佳情况下,它需要精确计算检索的关键元素,如果没有关键冲突。
如果您有很多键冲突或非常糟糕的哈希函数,hash-map 的最坏情况运行时复杂度可能为O(n)
,因为在在这种情况下,它会降级为对保存数据的整个数组进行线性扫描。
另外,O(1)
并不意味着立即,它意味着它有一个恒定量。因此,为字典选择正确的实现也可能取决于集合中元素的数量,因为如果只有少数条目,函数的恒定成本会非常高。
这就是字典/地图在不同场景下实现不同的原因。对于 Java,有多种不同的实现,C++ 使用红树/黑树等。您根据数据的数量以及它们的最佳/平均/最差运行时效率来选择它们。
【讨论】:
不一定是这样,例如Java 8 的HashMap
在检测到多个冲突时使用平衡树。
@acelent 可能是真的,但它不再是经典的哈希映射。地图/字典有许多不同的实现,正是这种情况。我已经修改了答案以指出这一点。【参考方案5】:
理论上它仍然是 O(n),因为在最坏的情况下,您的所有数据最终可能具有相同的哈希值并被捆绑在一起,在这种情况下您必须线性地遍历所有数据。
【讨论】:
【参考方案6】:请看帖子What does "O(1) access time" mean?
散列函数中的操作数量无关紧要,只要集合中的每个元素花费相同(恒定)的时间即可。例如,访问包含 2 个元素的集合中的一个元素需要 0.001 毫秒,但访问包含 2,000,000,000 个元素的集合中的一个元素也需要 0.001 毫秒。虽然哈希函数可以包含数百个 if 语句和多次计算。
【讨论】:
恒定的时间,不是线性的。 哈希函数不需要包含更多的“if 语句和多次计算”来产生足够长的哈希值来唯一标识 20 亿个元素而不是 200 个元素吗?【参考方案7】:来自文档:
使用其键检索值非常快,接近 O(1),因为 T:System.Collections.Generic.Dictionary`2 类是作为哈希表实现的。
所以它可以是 O(1),但可能会更慢。 在这里你可以找到另一个关于哈希表性能的帖子:Hash table - why is it faster than arrays?
【讨论】:
【参考方案8】:一旦您考虑到越来越大的字典占用更多内存,进一步降低缓存层次结构并最终减慢磁盘上的交换空间,就很难说它是真正的 O(1)。字典的性能会随着它变大而变慢,可能会给 O(log N) 时间复杂度。不相信我?用 1、100、1000、10000 等字典元素自己尝试一下,最多说 1000 亿个,并测量在实践中查找一个元素需要多长时间。
但是,如果您简化假设系统中的所有内存都是随机存取内存,并且可以在恒定时间内访问,那么您可以声称字典是 O(1)。这种假设很常见,尽管它对于任何具有磁盘交换空间的机器都不是真的,并且在任何情况下考虑到不同级别的 CPU 缓存仍然值得商榷。
【讨论】:
你说得有道理,但是当我们谈论算法复杂性时,假设完美的硬件确实是有意义的。关键是定义算法的特征,而不是不同的现实生活中的硬件实现。此外,如果你有足够大的数据,算法的复杂性才是最重要的:例如O(1)、(logN)、O(n) 或 O(n^2)。 还有哈希键与大字典冲突的问题。一旦你变得足够大,大多数新条目将与现有条目发生冲突,导致对每个哈希桶进行线性搜索,最终结果为 O(n)。除非您使哈希键随着大小的增加而变长……但是您也没有 O(1) 。我同意在实践中您可以将其视为常数时间,但我更愿意远离正式的 O 表示法,因为它只是对足够小尺寸的粗略近似,而不是任何尺寸的正式证明。【参考方案9】:我们知道哈希函数需要 O(1) 来通过键访问值......所以这并不意味着它只需要 1 步即可获取值,它意味着恒定时间“t”,其中“t " 不依赖于数据结构的大小(例如:-python dict())。
【讨论】:
以上是关于为啥即使哈希函数可能不是 O(1),也要通过键 O(1) 访问字典的元素?的主要内容,如果未能解决你的问题,请参考以下文章