如果我有一个每个元素的键,并且我不知道元素的索引到数组中,则hashtables的性能优于数组(O(1)vs O(n))。
为什么?我的意思是:我有一个密钥,我哈希它...我有哈希..算法不应该比较这个哈希与每个元素的哈希?我认为记忆处理背后有一些技巧,不是吗?
答案 0 :(得分:82)
如果我有一个每个元素的键,我不知道 将元素索引转换为数组,哈希表的执行效果要好于 数组(O(1)vs O(n))。
哈希表搜索在平均情况下执行O(1)。在最坏的情况下,哈希表搜索执行O(n):当你有冲突并且哈希函数总是返回相同的槽。人们可能会认为“这是一个遥远的情况”,但一个好的分析应该考虑它。在这种情况下,您应该遍历所有元素,如数组或链表(O(n))。
为什么?我的意思是:我有一把钥匙,我把它哈希..我有哈希.. 不应该算法将此哈希与每个元素进行比较 哈希?我认为记忆配置背后有一些技巧,不是 它?
你有一个密钥,你哈希它...你有哈希:元素存在的哈希表的索引(如果它之前已经找到)。此时,您可以访问O(1)中的哈希表记录。如果负载系数很小,则不太可能看到多个元素。因此,您看到的第一个元素应该是您要查找的元素。否则,如果您有多个元素,则必须将您在该位置找到的元素与您要查找的元素进行比较。在这种情况下,你有O(1)+ O(number_of_elements)。
在一般情况下,哈希表搜索复杂度为O(1)+ O(load_factor)= O(1 + load_factor)。
请记住,在最坏的情况下,load_factor = n。因此,在最坏的情况下,搜索复杂度为O(n)。
我不知道你对“记忆性格背后的伎俩”的意思。在某些观点下,哈希表(其结构和通过链接的冲突解决方案)可以被视为“智能技巧”。
当然,哈希表分析结果可以用数学证明。
答案 1 :(得分:29)
使用数组:如果您知道该值,则必须搜索平均值的一半(除非已排序)以查找其位置。
使用哈希:根据值生成位置。因此,再次给出该值,您可以计算插入时计算的相同哈希值。有时,多于1个值会产生相同的散列,因此实际上每个“位置”本身都是散列到该位置的所有值的数组(或链接列表)。在这种情况下,只需要搜索这个小得多的(除非它是一个糟糕的哈希)数组。
答案 2 :(得分:20)
哈希表有点复杂。他们根据哈希值将某些值放在不同的存储桶中。在理想情况下,每个铲斗只能容纳很少的物品,并且没有多少空桶。
一旦知道密钥,就可以计算哈希值。根据哈希,您知道要查找哪个存储桶。如上所述,每个桶中的项目数量应该相对较少。
哈希表在内部做了很多魔术,以确保存储桶尽可能小,同时不会为空存储桶消耗太多内存。此外,很大程度上取决于密钥的质量 - >哈希函数。
答案 3 :(得分:6)
哈希表不必比较哈希中的每个元素。它将根据密钥计算哈希码。例如,如果密钥是4,则哈希码可以是-4 * x * y。现在指针确切地知道要选择哪个元素。
如果它是一个数组,它必须遍历整个数组才能搜索这个元素。
答案 4 :(得分:2)
为什么[hashtables]按键优先于数组执行查找(O(1)vs O(n))]?我的意思是:我有一个密钥,我哈希它...我有哈希..不应该算法比较这个哈希与每个元素的哈希?我认为记忆处理背后有一些技巧,不是吗?
一旦你有了哈希,它就可以让你计算一个理想的"或桶中的预期位置:通常:
理想的存储桶=哈希%num_buckets
问题是,另一个值可能已经散列到该存储桶,在这种情况下,散列表实现有两个主要选择:
1)尝试另一个桶
2)让几个不同的值"属于"一个桶,也许是通过使桶指向一个链接的值列表
对于实施1,称为open addressing or closed hashing,你跳过其他桶:如果你找到了你的价值,那就太棒了;如果您找到一个从未使用过的存储桶,那么您可以在插入时将值存储在那里,或者您知道在搜索时从未找到您的值。如果您遍历替代存储桶的方式最终会多次搜索同一个存储桶,那么搜索可能会比O(n)更糟糕;例如,如果您使用quadratic probing,则尝试理想的存储区索引+1,然后是+4,然后是+9,然后是+16,依此类推 - 但是您必须使用例如以下方法来避免超出范围的存储区访问。 % num_buckets
,所以如果有12个桶,那么理想+ 4和理想+ 16搜索相同的桶。跟踪哪些桶已被搜索可能很昂贵,因此很难知道何时放弃:实施可以是乐观的并且假设它总是找到值或未使用的桶(冒着永远旋转的风险),它可以有一个计数器,在尝试阈值后,放弃或开始线性逐桶搜索。
对于实现2,称为closed addressing or separate chaining,您必须在容器/数据结构中搜索所有散列到理想存储桶的值。这有多高效取决于所用容器的类型。通常期望在一个桶处碰撞的元素的数量将是小的,这对于具有非对抗性输入的良好散列函数是正确的,并且通常足够甚至是平庸的散列函数,尤其是具有素数的素数。桶。因此,尽管有O(n)搜索属性,但经常使用链表或连续数组:链表很容易实现和操作,并且数组将数据打包在一起以获得更好的内存缓存位置和访问速度。最糟糕的情况是,表中的每个值都被散列到同一个存储桶,而该存储桶中的容器现在包含所有值:您的整个哈希表只有存储桶的效率< / em>容器。如果对相同存储桶进行散列的元素数超过阈值,则某些Java哈希表实现已开始使用二叉树,以确保复杂性永远不会比O(log2n)更差。
Python哈希是1 = open addressing = closed hashing的示例。 C ++ std::unordered_set
是封闭寻址=单独链接的一个例子。
答案 5 :(得分:0)
散列的目的是在基础数组中产生一个索引,这使您可以直接跳到相关元素。通常,这是通过将哈希除以数组的大小,然后取其余index = hash%capacity
来实现的。
散列的类型/大小通常是足够大以索引所有RAM的最小整数的类型/大小。在32位系统上,这是32位整数。在64位系统上,这是64位整数。在C ++中,这分别对应于unsigned int
和unsigned long long
。要学究式地,C ++从技术上为它的基元指定了最小大小,即至少32位和至少64位,但这并不重要。为了使代码具有可移植性,C ++还提供了一个size_t
基元,它对应于适当的无符号整数。您会在编写良好的代码中看到大量用于循环的类型,这些循环可索引到数组中。对于像Python这样的语言,整数基元会增长到所需的大小。这通常在其他语言的标准库中以“ Big Integer”命名。为了解决这个问题,Python编程语言只需将您从__hash__()
方法返回的任何值截断为合适的大小即可。
在这个分数上,我认为值得对智者说几句话。无论您是在过程的最后还是在每一步中计算余数,算术结果都是相同的。截断等效于计算余数以2 ^ n为模,其中n是保持不变的位数。现在,您可能会认为,由于在此过程的每一步都要进行额外的计算,因此在每一步计算余数都是愚蠢的。但是,由于两个原因,情况并非如此。首先,从计算上讲,截断非常便宜,远比广义除法便宜。第二,这是真正的原因,因为第一个是不充分的,并且即使没有索赔,索赔也通常成立,在每个步骤中采取其余措施可使索赔数量(相对)保持较小。因此,您将需要像product = 31*product + hash(array[index])
这样的东西,而不是product = hash(31*product + hash(array[index]))
。内部hash()调用的主要目的是获取一个可能不是数字的东西,然后将其转换为一个,而外部hash()调用的主要目的是获取一个可能过大的数字并截断它。最后,我要指出的是,在C ++之类的整数基元具有固定大小的语言中,此截断步骤会在每次操作后自动执行。
现在是房间里的大象。您可能已经意识到,哈希码通常说的要比它们所对应的对象小,更不用说从它们派生的索引了,说起来再小一点,两个对象完全有可能哈希到相同的索引。这称为哈希冲突。由哈希表(例如Python的set
或dict
或C ++的std::unordered_set
或std::unordered_map
支持的数据结构主要通过以下两种方式之一来处理。第一个称为separate chaining,第二个称为open addressing。在单独链接中,用作哈希表的数组本身就是一个列表数组(或者在某些情况下,开发人员觉得自己很喜欢,某些其他数据结构,如binary search tree),并且每次将元素散列到给定的索引将被添加到相应的列表中。在开放式寻址中,如果元素散列到已被占用的索引,则数据结构将探查下一个索引(或者在某些情况下,开发人员感觉像是花哨的一样,由其他函数定义的索引,如{{ 3}})等等,直到找到一个空插槽为止,当然,当它到达数组末尾时会自动换行。
接下来要介绍一下负载系数。当然,在增加或减少负载系数时,必然会存在固有的时空折衷。负载系数越高,表消耗的空间越少;然而,这是以增加性能降低冲突的可能性为代价的。一般而言,使用单独链接实现的哈希表比使用开放寻址实现的哈希表对负载因子更不敏感。这是由于被称为quadratic probing的现象,在这种情况下,开放寻址的哈希表中的簇在正反馈回路中趋于变得越来越大,原因是它们变得越大,就越有可能成为事实。包含新添加元素的首选索引。这实际上是为什么通常首选上述提到的二次探测方案的原因,该方案逐渐增加了跳跃距离。在极端情况下,如果负载系数大于1,则开放寻址根本无法工作,因为元素数量超过了可用空间。就是说,负载因子通常大于1。在编写Python的set
和dict
类时,使用的最大负载因子为2/3,其中Java的java.util.HashSet
和java.util.HashMap
与C ++的{{1}一起使用3/4 }和std::unordered_set
的最大负载系数为1。毫无疑问,Python的哈希表支持的数据结构使用开放地址来处理冲突,而Java和C ++同行则通过单独的链接来实现。
最后评论表的大小。当超过最大负载因子时,当然必须增加哈希表的大小。由于这要求重新索引其中的每个元素,因此将表固定增长一定数量的效率非常低。这样做会在每次添加新元素时引发订单大小操作。此问题的标准解决方案与大多数clustering实现中采用的解决方案相同。在需要扩展表的每个点,我们只需将其大小增加当前大小即可。毫不奇怪,这被称为表加倍。
答案 6 :(得分:-6)
我认为你在那里回答了自己的问题。 “算法不应该将此哈希值与每个元素的哈希值进行比较”。当它不知道你要搜索的索引位置时,它就是这样做的。它会比较每个元素以找到您要查找的元素:
E.g。假设你在一个字符串数组中寻找一个名为“Car”的项目。您需要遍历每个项目并检查item.Hash()==“Car”.Hash()以找出您正在寻找的项目。显然,它总是在搜索时不使用哈希,但示例代表。然后你有一个哈希表。哈希表的作用是创建一个稀疏数组,或者有时像上面提到的那样使用数组桶。然后它使用“Car”.Hash()来推断稀疏数组中你的“Car”项实际上在哪里。这意味着它不必搜索整个数组来查找您的项目。