什么使表查找如此便宜?

时间:2011-09-02 17:30:08

标签: algorithm dictionary big-o lookup

前段时间,我学到了一些关于大O符号和不同算法效率的知识。

例如,循环遍历数组中的每个项目以执行某些操作

foreach(item in array)
    doSomethingWith(item)

O(n)算法,因为程序执行的周期数与数组的大小成正比。

令我感到惊讶的是,表查找是O(1)。也就是说,在哈希表或字典中查找密钥

value = hashTable[key]
无论表格是否有一个密钥,十个密钥,一百个密钥或千兆万亿密钥,

都会占用相同的周期数。

这真的很酷,我很高兴这是真的,但这对我来说不直观,我不明白为什么这是真的。

我可以理解第一个O(n)算法,因为我可以将它与现实生活中的例子进行比较:如果我有要张印的纸张,我可以逐个浏览每篇论文并盖上每一个。对我来说很有意义的是,如果我有2000张纸,使用这种方法印花的时间比用1000张纸要长两倍。

但我无法理解为什么表查找是O(1)。我想如果我有一本字典,并且我想找到多态的定义,我需要O(logn)时间才能找到它:我会打开一些页面字典,并在多态之前或之后查看它是否按字母顺序排列。如果,例如,它是在 P 部分之后,我可以在打开的页面之后删除字典的所有内容,并使用字典的其余部分重复该过程,直到我找到单词多态性

这不是O(1)过程:在一千页的词典中找到单词比在两页词典中找到单词通常需要更长的时间。我很难想象一个过程需要相同的时间,而不管字典的大小。

tl; dr :您能解释一下如何以O(1)复杂度进行表查找吗?

(如果你告诉我如何复制惊人的O(1)查找算法,我肯定会得到一个很大的字典,所以我可以向所有朋友展示我的忍者字典查找技能)

编辑:大多数答案似乎都取决于这个假设:

  

您可以在恒定时间内访问字典的任何页面

如果这是真的,我很容易看到。但我不知道为什么这个基本假设是正确的:我会使用相同的过程来逐字查找页面。

内存地址也一样,用什么算法加载内存地址?是什么让从地址中找到一块内存变得如此便宜?换句话说,为什么内存访问O(1)

9 个答案:

答案 0 :(得分:10)

您应该阅读Wikipedia article

但实质是你首先将哈希函数应用于你的密钥,它将它转换为整数索引(这是O(1))。然后将其用于索引数组,该数组也是O(1)。如果散列函数设计得很好,那么在数组中的每个位置只应存储一个(或几个项),因此查找完成。

所以在大规模简化的伪代码中:

ValueType array[ARRAY_SIZE];

void insert(KeyType k, ValueType v)
{
    int index = hash(k);
    array[index] = v;
}

ValueType lookup(KeyType k)
{
    int index = hash(k);
    return array[index];
}

显然,这不会处理冲突,但您可以阅读该文章以了解如何处理冲突。

<强>更新

要解决已编辑的问题,索引到数组是 O(1)因为在底层,CPU正在执行此操作:

    ADD index, array_base_address -> pointer
    LOAD pointer -> some_cpu_register

其中LOAD加载存储在指定地址的内存中的数据。

更新2

内存加载O(1)的原因实际上只是因为这是我们在讨论计算复杂性时通常指定的公理(参见http://en.wikipedia.org/wiki/RAM_model)。如果我们忽略缓存层次结构和数据访问模式,那么这是一个合理的假设。当我们扩展机器的尺寸时,这可能不是真的(具有100TB存储的机器可能不会花费与具有100kB的机器相同的时间量)。但通常情况下,我们假设机器的存储容量是恒定的,并且比我们可能看到的任何问题大小都要大得多。因此,对于所有意图和目的,它是一个恒定的操作。

答案 1 :(得分:7)

答案 2 :(得分:4)

你是对的,很难找到真实世界的例子。当然,这个想法是你在寻找一些地址而不是价值的东西。

字典示例失败,因为您没有立即知道页面的位置如278.您仍需要查看与单词相同的内容,因为页面位置不在您的内存中。

但是说我在你的每个手指上标记了一个数字,然后我告诉你摆动一个写有15的手指。你必须看看它们中的每一个(假设它未分类),如果它不是15,你检查下一个。为O(n)。

如果我告诉你扭动你的小拇指。你不需要查看任何东西。你知道它在哪里,因为我只是告诉你它在哪里。我刚刚传递给你的价值是你在“记忆中”的地址。

与数据库类似,但规模远远超过10个手指。

答案 3 :(得分:1)

因为工作是预先完成的 - 所以将值放在一个桶中,该桶可以根据密钥的哈希码轻松访问。如果你想在字典中查找你的作品,但是已经标记了该单词的确切页面就好了。

答案 4 :(得分:1)

想象一下,你有一本字典,其中以字母A开头的所有内容都在第1页,第2页的字母B等等。因此,如果您想查找“气球”,您将确切知道要去哪个页面。这是O(1)查找背后的概念。

任意数据输入=&gt;映射到特定的内存地址

权衡当然是您需要为所有潜在地址分配更多内存,其中许多可能永远不会被使用。

答案 5 :(得分:1)

如果您的阵列中包含999999999个位置,那么按社会保险号查找记录需要多长时间?

假设你没有那么多内存,那么分配大约30%的数组位置,你想要存储的记录数,然后编写一个哈希函数来查找它。

一个非常简单(可能很糟糕)的哈希函数就是社交%numElementsInArray。

问题是碰撞 - 您不能保证每个位置只包含一个元素。但没关系,您可以存储链接的记录列表,而不是将记录存储在数组位置。然后,一旦您进行散列以获得正确的数组位置,就会线性扫描所需的元素。

最坏的情况是O(n) - 一切都进入同一个桶。平均情况是O(1),因为通常如果你分配了足够的桶并且你的哈希函数是好的,那么记录通常不会经常发生冲突。

答案 6 :(得分:1)

好吧,哈希表简而言之:

你采用常规数组(O(1)访问),而不是使用常规的Int值来访问它,你使用MATH。

你所做的,是把键值(比如一个字符串)计算成一个数字(字符上的某个函数),然后使用一个众所周知的数学公式,在数组范围内给出一个相对较好的分布

所以,在这种情况下,你只是做4-5计算(O(1))来从该数组中获取一个对象,使用一个不是int的键。

现在,避免碰撞,找到合适的数学公式以实现良好的分配是困难的部分。这就是维基百科中的解释:en.wikipedia.org/wiki/Hash_table

答案 7 :(得分:0)

查找表确切地知道如何在表格之前访问中的给定项目。 完全相反,通过它在排序数组中的值来查找项目,您必须访问项目以检查它是否是您想要的。

答案 8 :(得分:0)

理论上,哈希表是一系列桶(内存中的地址)和将域中的对象映射到这些桶中的函数。

假设您的域名是3个字母的单词,您将阻止所有可能的3个字母单词的26 ^ 3 = 17,576个地址,并创建一个将所有3个字母单词映射到这些地址的函数,例如,aaa = 0,aab = 1,等等。现在当你想要查找一个单词时,比如说“和”,你立即从你的O(1)函数知道它是地址编号367。