为啥在搜索键是 O(n) 时,哈希表查找只有 O(1) 时间?

Posted

技术标签:

【中文标题】为啥在搜索键是 O(n) 时,哈希表查找只有 O(1) 时间?【英文标题】:Why is a hash table lookup only O(1) time when searching for a key is O(n)?为什么在搜索键是 O(n) 时,哈希表查找只有 O(1) 时间? 【发布时间】:2018-07-14 04:02:32 【问题描述】:

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

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

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

【问题讨论】:

您的第二段没有多大意义。哈希表的工作方式是每个元素根据其哈希值分配给一个桶。理想情况下,每个元素都分配有一个唯一的桶,但这在实践中是无法实现的。人们希望有足够的桶,恒定个元素被分配给每个桶,这样一个桶内的蛮力搜索仍然是 O(1)。但在最坏的情况下,元素的非常数部分(例如,n/10)被分配到一个桶中;任何涉及该哈希的查找都不再是常量。 “内部机制”保证平均 O(1) 时间。没有保证。 假设即使每个元素都分配了一个唯一的桶,查找如何知道使用哈希函数在哪里找到该桶?我知道存储桶内的搜索可以是 O(n),但我不知道存储桶的本地化是 O(1)。 关于最后一条评论:计算哈希仅取决于键的大小(而不取决于键/值的数量)。哈希确定内存地址。由于 O(n) 基于元素的数量(不是键大小),因此它在 O(1) 中。想象一个数组,它可以随机访问它的元素。如果您的密钥在 0-n 中,这是一个身份哈希函数 (x -> x) 的示例。 您缺少哈希表最重要的“功能”,即键的哈希码用于将键映射到存储桶数组中的某个位置。因此,给定一个键,在桶数组中查找索引需要一个恒定的时间(假设哈希码计算仅基于键的值)。 【参考方案1】:

我认为您混淆了术语,并且通过考虑存储桶也使事情复杂化。

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

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

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

a[H(k1)] = v1

现在假设稍后我们忘记了k1 是否在哈希表中,我们想检查它是否存在。为此,我们只需查找 a[H(k1)] 并查看是否存在 any 值,即 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”。

我假设这就是您要问的,并且您对术语有些混淆。如果这不是您想要的,请发表评论。

【讨论】:

【参考方案2】:

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

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

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

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

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

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

【讨论】:

【参考方案3】:

好问题!

假设

    我们想将strings 映射到values hashFunction(string) => hashedIndex : int 在 O(1) 中 valueArray : [any]店铺values valueIndex : intvalueArray 中的第一个空索引 lookupArray : [int] 将每个 valueIndex 存储在 hashedIndex 数组查找是 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

为了清楚地回答您的问题,省略了很多细节。

希望有帮助!

【讨论】:

【参考方案4】:

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

这里唯一的区别是,我们不关心键值对的位置。这里没有索引。查找数组项 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

当你查找“name”时,它会被散列,作为散列函数的主要特征,它会返回相同的结果“ab1234wq”。所以编程引擎会查看这个地址,会看到数组并返回值。可以看到,这个操作和数组查找是一样的。

【讨论】:

以上是关于为啥在搜索键是 O(n) 时,哈希表查找只有 O(1) 时间?的主要内容,如果未能解决你的问题,请参考以下文章

深度分析及实现哈希表

使用 O(1) 搜索链表

为啥我的 O(NLogN) 算法查找字谜比我的 O(N) 算法运行得更快?

哈希表的插入复杂度如何为 O(1)

[DS+Algo] 008 查找

为啥用二叉搜索树实现哈希表?