为什么哈希表查找一个键只需要O(1)的时间,而在搜索一个键时需要O(n)的时间?

12

就技术而言,根据我在这里阅读的帖子,哈希表在最坏情况下确实需要O(n)时间来查找。但我不理解内部机制如何保证平均情况下是O(1)时间。

我的理解是,对于给定的n个元素,理想情况是有n个桶,这样可以得到O(1)的空间复杂度。这就是我卡住的地方。假设我想要查找字典中是否存在某个键,这肯定需要O(n)的时间。那么当我想要通过使用其键的哈希值来搜索哈希表中的元素时,为什么会产生差异呢?简单来说,使用原始键值进行搜索需要O(n)时间,但是使用哈希值则需要O(1)时间。为什么?

我难道不仍然需要一个一个地查找哈希值才能看到哪一个匹配吗?为什么哈希让我立即知道要检索哪个元素或是否存在这样的元素?


你的第二段话意思不太清楚。哈希表的工作方式是根据其哈希值将每个元素分配到一个桶中。理想情况下,每个元素都被分配到一个唯一的桶中,但在实践中这是不可行的。人们希望通过足够多的桶,将恒定数量的元素分配到每个桶中,以便在桶内进行暴力搜索仍然是O(1)的。但在最坏的情况下,非恒定比例的元素(例如n/10)被分配到一个桶中;任何涉及该哈希的查找都不再是常数时间。 - chepner
2
“内部机制”不能保证平均O(1)时间。没有任何保证。 - President James K. Polk
1
假设每个元素都被分配了一个唯一的“桶”,那么查找如何根据哈希函数精确定位到相应的“桶”呢?我知道在“桶”内部的搜索可能是O(n),但我不知道定位“桶”的时间复杂度为何是O(1)。 - oldselflearner1959
关于上一个评论:计算哈希仅取决于键的大小(而不取决于键/值的数量)。哈希确定了内存地址。由于O(n)基于元素数量(而不是键大小),因此这是O(1)。想象一下一个数组,它可以随机访问其元素。如果您的键在0-n中,则这是一个恒等哈希函数的示例(x->x)。 - sascha
3
哈希表最重要的“特性”是使用键的哈希码将键映射到桶数组中的位置。因此,给定一个键,找到桶数组中的索引只需要固定的时间(假设哈希码计算仅基于键的值)。 - fps
关于如何计算这个索引,最简单的方法是通过以下方式进行:index = hashFunction(key) % number_of_buckets(这是概念性的,实际上在现实世界中计算索引会更加复杂) - fps
4个回答

20

我认为您混淆了术语,并且通过考虑桶(buckets)使问题变得更加复杂。

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

我们可以通过将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?”和“散列表中是否有来自纽约的人?”之间的区别。我们可以对"Bob Jones"进行哈希并查看相应的数组位置上是否有什么东西(因为"Bob Jones"就是这样被插入的),但我们没有同样快捷的方法来查找"New York"。我假设这就是你想问的,而且你可能把术语搞混了。如果不是这个问题,请留言说明。

9
听起来你正在寻求更详细的解释!
我假设你已经理解数组元素的查找需要O(1)的时间,也就是说,如果我已经知道要查找数组的第100个元素,那么只需要O(1)的时间,因为这是一个简单的内存地址查找(通过将100添加到第一个元素的地址)。
哈希方法利用了这种内存地址查找以实现平均O(1)的时间。显然,这意味着您需要能够将查找关键字转换为内存地址。让我给你举一个非常简化的例子,展示在哈希表中如何工作(请注意,字典在底层实现时使用哈希表,因此当我提到哈希表时,完全相同的原则也适用于字典)。
简化的例子场景; 我们需要按客户的名字查找邮寄地址。为简单起见,假设名称将是唯一的,并且它们的字母是从a到z。假设我们最初只为10个客户(即他们的姓名和地址)设计这个功能。
现在假设我们必须通过将名称-地址对存储在哈希表中并创建我们自己的哈希函数来解决此问题!这个哈希函数需要将名称作为参数并将其转换为内存查找!
现在请你花点时间思考这里需要多少个数组?它们应该是什么类型,大小又应该是多少呢?
我们肯定需要一个数组来存储邮寄地址。它的大小应该是多少呢?我们需要存储10个邮寄地址,所以大小必须是10! 我们还需要第二个数组来存储第一个数组中元素的索引!!换句话说,我们需要第二个数组来为客户名称存储邮寄地址(从第一个数组中)。这个数组的大小应该是多少呢?肯定比10大!但这实际上取决于我们设计的哈希函数。为简单起见,让我们创建一个哈希函数,它只是将名称参数的第一个字母作为索引。例如,如果名称以A开头,则哈希值为1,对于b,它为2,对于c,它为3……对于z,它为26。因此,至少我们的查找数组大小必须是26(您可能认为这浪费了很多空间来存储10个地址!!但它可能值得,因为它将为我们提供性能) 让我们通过一个例子来理解这一点。假设我们的第一个客户名字是Bob。为Bob存储地址的第一步是找到邮寄地址数组中的第一个空元素。这是第一个名称,所以整个邮寄地址数组都是空的。我们可以将Bob的地址存储在邮寄地址数组的索引0处。当我们存储这个地址时,我们也会标记它为Bob的地址在索引0处。(我使用这个“标记”术语来稍后解释查找与搜索)然后我们找出名称Bob的哈希值。在这种情况下,它将是2!因此,在查找数组中,在位置2处,我们存储0。(即Bob的邮寄地址的索引)。现在假设我们的第二个客户是Hamish;我们在邮寄地址数组的索引1处存储Hamish的邮寄地址;标记它为Hamish的地址,然后我们找出Hamish的哈希值。由于Hamish以'H'开头,值将为8。因此,在我们的查找数组中,在位置8处,我们存储值1(即Hamish的地址的索引)。我们可以重复这个过程,为所有10个客户存储他们的地址。现在,如果您想查找Bob的地址,您可以通过遵循一个简单的两步过程快速查找。第一步-将名称Bob转换为哈希值;答案是2;继续检查邮寄地址数组中的位置2;如果它被标记为Bob的地址,则返回位置2!!对于Hamish也是如此;H->给出8。继续从位置8查找地址;如果它被标记为Hamish的地址,则返回位置8的地址。这种机制称为“查找”。如果您没有创建第二个数组(查找数组),那么您只会有邮寄地址数组,您必须逐个检查每个地址,以查看它是否被标记为您要查找的客户名称! 现在,如果有两个以相同字母开头的客户名称怎么办?这就是哈希冲突,可以用不同的方法来处理。如果我们需要存储10000个名称怎么办?那意味着我们必须使用更好的哈希函数,这将使我们获得较少的哈希冲突。我不在这里涵盖这两个术语,因为我认为问题只要求解释查找与搜索。

2

好问题!

假设

  1. 我们想要将 string 映射到 value
  2. hashFunction(string) => hashedIndex : int 在 O(1) 的时间内完成
  3. valueArray : [any] 存储 value
  4. valueIndex : intvalueArray 中第一个空的索引
  5. lookupArray : [int] 存储每个 hashedIndex 上的 valueIndex
  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

很多细节被省略了,以便清晰地回答您的问题。
希望这有所帮助!

1

我认为“哈希”这个词吓到了人们。在幕后,哈希表是一种数据结构,它将键/值对存储在数组中。

唯一的区别在于,我们不关心键值对的位置。这里没有索引。查找数组项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”。因此,编程引擎将查看此地址,将查看数组并返回值。正如您所看到的,这个操作与数组查找相同。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接