让我们说有一个Hash函数。它存储'n'键值对。如果我需要特定键的值,则散列函数遍历所有键以找到我们正在寻找其值的键。如果是,那么复杂性如何变为O(1)? hash如何查找键?
答案 0 :(得分:3)
不,这不是哈希表的工作原理。散列表本质上是一个数组,它将值存储在与其键的散列相对应的索引处。所以,假设我想将字符串"abc"
映射到另一个字符串"xyz"
,并假设"abc"
哈希到42。我要做的是转到我的表的索引42并放置字符串"xyz"
。现在,如果稍后我想找到与字符串"abc"
关联的值,我会再次哈希它并转到相应的索引(42),在那里找到"xyz"
。这总体上是O(1)操作。总结:
Mapping "abc" to "xyz"... 1. hash("abc") = 42 2. Place in "xyz" table: ---+-----------------------------------+--- ... | | | "xyz" | | | ... ---+-----------------------------------+--- 40 41 42 43 44 Later... 1. Query "abc" 2. hash("abc") = 42 3. Look at index 42, find the value "xyz"
为了描述哈希表的工作原理,我略微过度简化了,我恳请您浏览一下hash table维基百科的文章,以获得更深入的描述。另请注意,很多时候您会看到哈希表实现为链接列表数组,以便考虑两个键散列到相同数字的情况(所谓的hash collisions)。使用普通数组将无法处理此类情况,因为我们无法在同一位置存储多个值。例如,这是Java如何实现HashMap
。
例如,请参考上面的示例并假设我们也想要将"123"
映射到"pqr"
,并假设"123"
也哈希到42.最终的结果看起来像这样:
40 41 42 43 44 ---+-----------------------------------+--- ... | | | + | | | ... ---+-----------------|-----------------+--- | +---------------+ | "abc" | "xyz" | +---------------+ | +---------------+ | "123" | "pqr" | +---------------+
请注意,我们知道必须将键与值一起显式存储。现在,如果我们想要使用键"123"
进行查询,我们将转到其哈希位置(42)并遍历在那里找到的链表,直到找到具有键"123"
的链表。然后我们将返回相应的值"pqr"
。
此时您可能有两个问题:
hash()
函数如何在O(1)中运行?关于第一个问题,在讨论哈希表的复杂性时,通常不考虑哈希过程(可能不是实际一个恒定时间操作),仅仅因为假设与其他后续流程相比,不会非常耗时。事实上,在许多情况下,实际上 的散列是不变的。例如,由于字符串在许多语言中是不可变的,因此它们的哈希值通常只计算一次然后缓存,从而导致在第一次哈希操作之后进行恒定时间哈希。
至于第二个问题,当我们有一个好的哈希函数和一个合理大小的表时,形成的链表应该非常短(大概不超过3个)。因此,遍历过程被认为是恒定时间。
答案 1 :(得分:1)
名称中的“哈希”是一个函数,它基本上将密钥转换为该密钥的(理想情况下)唯一索引。实际上,每个哈希都是一个“桶”,可能包含多个值,以允许冲突。
答案 2 :(得分:0)
尽管前面的两个答案都是正确的(并且我同时投了两票),但我觉得要加入一些见解。
首先,哈希表是一种抽象数据类型,这意味着可以有许多数据结构和检索算法可以在特定实现中使用。数组,二进制搜索树,字典等只是一些可能的实现的例子。
其次,重要的一点是,用于访问密钥值的平均用例检索是固定时间,即O(1),而不是最差情况。< / p>
因此密钥映射到理想的唯一存储位置。但是,在实际场景中总是存在冲突,并且通过存储那些多个值(例如维护链接列表,树或另一个第二级哈希)来处理冲突。
关键是,对于良好的散列函数,与正常的常量时间索引访问相比,冲突 quire rare 。因此,短语AVERAGE CASE。
在散列函数中最坏情况下针对键检索值永远不会是O(1)。它可以是log(n)甚至是最差的。但是,对于每个良好的散列函数,它的出现频率是如此之小,以至于平均情况复杂度仍然保持在O(1)即恒定时间。