为什么在搜索密钥时只有O(1)时间的哈希表查找是O(n)?

时间:2018-02-03 23:42:47

标签: algorithm hash hashmap

从技术上讲,根据我在这里读到的帖子,哈希表在最坏的情况下确实是O(n)时间查找。但我不知道内部机制如何保证它平均为O(1)时间。

我的理解是,给定n个元素,理想情况是有n个桶,这导致O(1)空间。这就是我被困住的地方。假设我想查找一个键是否在字典中,这肯定需要O(n)时间。那么,当我想通过使用其键的哈希值来搜索元素是否在哈希表中时,为什么会有所不同呢?简而言之,使用原始键值进行搜索会得到O(n)时间,但使用哈希值时会得到O(1)时间。这是为什么?

我是否仍然需要逐个查找哈希值以查看哪一个匹配?为什么散列让我立即知道要检索哪个元素或者是否存在这样的元素?

4 个答案:

答案 0 :(得分:5)

我认为你会混淆术语,并且通过考虑桶来使事情变得复杂。

让我们设想一个以长度为a的数组n实现的哈希表。我们还假设我们有n个可能的密钥和一个完美的哈希函数H,它将每个密钥k映射到i中的唯一索引a

让我们通过将a中的每个值设置为nil来初始化哈希表。

我们可以通过将值放在数组中的适当位置,将键值(k1, v1)插入到哈希表中:

a[H(k1)] = v1

现在让我们说稍后我们忘记了k1是否在哈希表中,我们想检查它是否在那里。要执行此操作,我们只需查找a[H(k1)]并查看是否存在任何值,即a[H(k1)] != nil。这显然是一个恒定的时间查找。

但是如果我们想看看v1,甚至其他v2是否在我们的哈希表中的任何地方呢?这并不容易,因为我们没有将vi映射到数组中的位置的函数。它可以与任何密钥相关联。因此,查看表中是否存在的唯一方法是扫描整个数组,检查每个值:

for i in 0..n-1:
  if a[i] == v2:
    return true
return false

为了使这一点更加具体,想象你的钥匙是名字,你的价值观是居住的城市。现在比较询问" Bob Jones是否在哈希表中?" to"哈希表中是否有来自纽约的人?"。我们可以哈希" Bob Jones"并查看相应阵列位置中是否有任何内容(因为那样" Bob Jones"会被插入),但我们没有类似的快速查找方式&#34 ;纽约"。

我假设这是你要问的,你有点混淆术语。如果这不是你想要的,请评论。

答案 1 :(得分:1)

听起来你正在寻找更详细的解释!

我假设你已经明白数组元素查找需要O(1),即如果我已经知道我想在数组中查找第100个元素那么它只需要我O(1),因为这是一个简单的内存地址查找(通过向第一个元素的地址添加100)。

散列方法利用此内存地址查找来实现O(1)平均时间。现在显然这意味着您需要能够将查找键转换为内存地址。让我举一个非常简单的示例,说明它如何在哈希表中工作(为了清楚起见,字典在引擎盖下实现哈希表,所以当我提到哈希表时,完全相同的原则也适用于字典)。

简化示例场景;我们需要用他们的名字查找客户的邮寄地址。为简单起见,假设名称将是唯一的,并且它们是正常的a到z字母。让我们说最初我们只为10个客户(即他们的名字和地址)设计这个。

现在假设我们必须通过在哈希表中存储名称 - 地址对来解决这个问题,我们必须创建自己的哈希函数!一个哈希函数,它将name作为参数并将其转换为内存查找!!

现在花一点时间想想这里需要多少个数组?什么是他们的类型和他们的大小? 我们肯定需要一个数组来存储邮件地址。应该是什么尺寸?那么我们需要存储10个邮件地址,所以大小必须是10! 我们还需要第二个数组来存储第一个数组的元素索引!!或者换句话说,我们需要第二个数组来存储对我们客户名称的邮寄地址(来自第一个数组)的引用。这个数组的大小应该是多少?绝对大于10!但它实际上归结为我们设计的哈希函数。为简单起见,我们创建一个哈希函数,它只接受name参数的第一个字母并将其转换为索引。 ie如果name从A开始,那么hashvalue是1,b是2,c是3 ...对于z,它是26.所以至少我们的查找数组大小必须是26(你必须认为这是用于存储10个地址的大量空间的浪费!!但它可能是值得的,因为它会给我们提供性能) 让我们试着通过一个例子来理解这一点。假设我们的第一个客户名称是Bob。为Bob存储地址的第一步是找到邮件地址数组中的第一个空元素。这是第一个名称,因此整个邮件地址数组为空。我们可以将Bob的地址存储在邮件地址数组的索引零处。当我们存储这个地址时,我们也会在索引0处将其标记为Bob的地址。(我正在使用这个'标记'术语以便稍后解释查找与搜索)然后我们找到名称Bob的哈希值。在这种情况下,它将是2!因此,在位置2的查找数组中,我们存储0(即Bob的邮件地址的索引)。现在让我们说我们的第二个客户是Hamish;我们在邮件地址数组的索引1(即第二个元素)存储Hamish的邮件地址;将它标记为Hamish的地址然后我们找出Hamish的哈希值。由于Hamish从'H'开始,因此值为8.因此,在位置8的查找数组中,我们存储值1(即Hamish地址的索引)。我们可以为所有10个客户重复此过程并存储他们的地址。现在,稍后当您想查找Bob的地址时,只需按照简单的两步程序即可快速查找。步骤1-将名称Bob转换为hashvalue;答案是2;继续检查邮寄地址阵列中的位置2;如果它被标记为Bob的地址,那么返回位置2 !!哈米什也一样; H->给出8.继续从位置8查找地址;如果它被标记为Hamish的地址,则从位置8返回地址。这种机制称为“查找”。如果你还没有创建第二个数组(查找数组)那么你只有邮件地址数组,你必须逐个查看每个地址并检查它是否标有你要查找的客户名称或者不!。 现在,如果有两个客户名称以相同的字母开头怎么办?这称为哈希冲突,可以用不同的方法处理。如果我们需要存储10000个名字怎么办?这意味着我们必须使用更好的散列函数,这样可以减少散列冲突。我没有在这里涵盖这两个术语,因为我认为这个问题只要求解释查找与搜索。

答案 2 :(得分:1)

好问题!

假设

  1. 我们想将string映射到value s
  2. hashFunction(string) => hashedIndex : int in O(1)
  3. valueArray : [any]个商店value
  4. valueIndex : intvalueArray中的第一个空索引
  5. lookupArray : [int]将每个valueIndex存储在hashedIndex
  6. 数组查找为O(1)。
// Setting a value

valueArray[valueIndex] = value 

hashedIndex = hashFunction(string)

lookupArray[hashedIndex] = valueIndex


// Looking up a value

hashedIndex = hashFunction(string) // O(1)

valueIndex = lookupArray[hashedIndex]; // O(1) array lookup

value = valueArray[valueIndex]; // O(1) array lookup

省略了许多细节,可以清楚地回答您的问题。

希望有帮助!

答案 3 :(得分:0)

我认为“哈希”一词正在吓到人们。在后台,哈希表是将键/值对存储在数组中的数据结构。

这里唯一的区别是,我们不在乎键值对的位置。这里没有索引。查找数组项O(1)。它与数组大小无关,与位置无关。您只需输入索引号,即可检索项目。

因此需要花多少时间才能完成。是O(1)。

在哈希表中,当存储键/值对时,键值将被哈希并存储在相应的内存插槽中。

{name:"bob"} //name will be hashed

hash(name) = ab1234wq //this is the memory address
[["name","bob"]] // will be store at memory adress ab1234wq

当您查找“名称”时,它将被散列,并且作为散列函数的主要功能,它将返回相同的结果“ ab1234wq”。因此,编程引擎将查看该地址,将看到数组并返回值。如您所见,此操作与数组查找相同。