python - 如果我们仍然需要检查每个项目,哈希的含义是什么?

标签 python python-3.x hash types set

我们知道tuple对象是不可变的,因此是散列的。我们还知道lists是可变的和不可散列的。
这很容易说明

>>> set([1, 2, 3, (4, 2), (2, 4)])
{(2, 4), (4, 2), 1, 2, 3}

>>> set([1, 2, 3, [4, 2], [2, 4]])
TypeError: unhashable type: 'list'

现在,在这个上下文中hash的含义是什么,如果为了检查唯一性(例如,在构建集合时),我们仍然必须检查集合中的任何iterable中的每个单独项?
我们知道两个对象可以具有相同的hash值,但仍然是不同的。因此,hash仅用于比较对象是不够的。那么,散列的意义是什么?为什么不直接检查iterables中的每个项目呢?
我的直觉是这可能是因为
hash只是一个(很快)的初步比较。如果hashes不同,我们知道对象是不同的。
表示对象是可变的。当与其他对象比较时,这应该足以引发一个异常:在特定的时间,对象可以是相等的,但也许以后,它们不是。
我的方向对吗?还是我错过了重要的一部分?
谢谢你

最佳答案

现在,hash在这个上下文中的意义是什么,如果,为了检查唯一性(例如,在构建集合时),我们仍然必须检查集合中的任何iterable中的每个单独项?
是的,但是哈希用于两个对象可以相等的情况下进行保守估计,也用于分配一个“桶”到一个项目。如果哈希函数是精心设计的,那么很可能(不确定),大多数,如果不是全部,最终在不同的桶,因此,因此,我们使成员检查/插入/删除/…算法平均运行在恒定时间O(1),而不是O(n),这是典型的列表。
所以,你的第一个答案是部分正确的,尽管你必须考虑到桶肯定会提高性能,而且实际上比保守检查更重要。
背景
注:我将在这里使用一个简化的模型,使原理清晰,实际上字典的实现更为复杂。例如散列在这里只是一些数字,显示了普林西比。
哈希集和字典被实现为一个“bucket”数组。元素的散列决定了我们将元素存储在哪个bucket中。如果元素的数量增加,那么bucket的数量就会增加,字典中已经存在的元素通常会被“重新分配”到bucket中。
例如,空字典在内部可能看起来像:

+---+
|   |
| o----> NULL
|   |
+---+
|   |
| o----> NULL
|   |
+---+

所以两个bucket,如果我们添加一个元素'a',那么散列就是123。让我们考虑一个简单的算法,将一个元素分配给一个桶,这里有两个桶,所以我们会给元素分配一个偶数散列给第一个桶,而一个奇数散列给第二个桶。由于'a'的散列是奇数,因此我们将'a'分配给第二个bucket:
+---+
|   |
| o----> NULL
|   |
+---+
|   |   +---+---+
| o---->| o | o----> NULL
|   |   +-|-+---+
+---+    'a'

所以这意味着如果我们现在检查'b'是否是字典的成员,我们首先计算hash('b'),也就是456,因此如果我们把它添加到字典中,它将在第一个bucket中。因为第一个bucket是空的,所以我们不必在第二个bucket中寻找元素来确定'b'不是成员。
例如,如果我们想检查'c'是否是一个成员,我们首先生成'c'的散列,即789,因此我们再次将其添加到第二个bucket,例如:
+---+
|   |
| o----> NULL
|   |
+---+
|   |   +---+---+   +---+---+
| o---->| o | o---->| o | o----> NULL
|   |   +-|-+---+   +-|-+---+
+---+    'c'         'a'

因此,现在如果我们再次检查'b'是否是一个成员,我们将查看第一个bucket,然后,我们再也不必遍历'c''a'来确定'b'不是字典的成员。
当然,现在有人可能会争辩说,如果我们继续添加更多的字符,比如'e''g'(这里我们认为这些字符有一个奇怪的散列),那么这个bucket将非常满,因此如果我们稍后检查'i'是否是成员,我们仍然需要遍历元素。但是,如果元素的数量增加,通常存储桶的数量也会增加,并且字典中的元素将被分配一个新的存储桶。
例如,如果我们现在想将'd'添加到字典中,字典可能会注意到插入后的元素数3大于桶的数量2,因此我们创建了一个新的桶数组:
+---+
|   |
| o----> NULL
|   |
+---+
|   |
| o----> NULL
|   |
+---+
|   |
| o----> NULL
|   |
+---+
|   |
| o----> NULL
|   |
+---+

我们重新分配成员'a''c'。现在所有具有哈希< cc> > h的元素将被分配给第一个桶,h % 4 == 0到第二个桶,h % 4 == 1到第三个桶,并且h % 4 == 2到最后一个桶。这意味着hashh % 4 == 3'a'将存储在最后一个bucket中,hash123'c'将存储在第二个bucket中,因此:
+---+
|   |
| o----> NULL
|   |
+---+
|   |   +---+---+
| o---->| o | o----> NULL
|   |   +-|-+---+
+---+    'c'
|   |
| o----> NULL
|   |
+---+
|   |   +---+---+
| o---->| o | o----> NULL
|   |   +-|-+---+
+---+    'a'

然后将hash789'b'添加到第一个bucket中,这样:
+---+
|   |   +---+---+
| o---->| o | o----> NULL
|   |   +-|-+---+
+---+    'b'
|   |   +---+---+
| o---->| o | o----> NULL
|   |   +-|-+---+
+---+    'c'
|   |
| o----> NULL
|   |
+---+
|   |   +---+---+
| o---->| o | o----> NULL
|   |   +-|-+---+
+---+    'a'

因此,如果我们想检查cc的成员数,我们计算散列,知道如果在字典中是cc,我们必须在第三个桶中搜索,并在那里找到它。如果我们寻找456'a',也会发生同样的情况(但是使用不同的桶),如果我们寻找'a'(这里用哈希'b'),那么我们将在第三个桶中进行搜索,并且永远不必检查单个元素的相等性,以知道它不是字典的一部分。
如果我们想检查'c'是否是一个成员,那么我们计算'd'的散列(这里是12),并在第二个bucket中搜索。因为bucket不是空的,所以我们开始遍历它。
对于桶中的每个元素(这里只有一个元素),算法首先查看我们搜索的密钥,而节点中的密钥引用相同的对象(两个不同的对象可以是相等的),因为情况并非如此,我们不能声称'e'在字典中。
接下来我们将比较我们搜索的密钥的散列和节点的密钥。大多数字典实现(如果我正确地回忆了),也可以将散列存储在列表节点中。因此,这里检查是否'e'等于< > >,因为事实并非如此,我们知道345和< >是不一样的。如果比较这两个对象比较昂贵,那么我们可以节省一些周期。
如果散列是相等的,这并不意味着元素是相等的,因此,在这种情况下,我们将检查这两个对象是否相等,如果是这样的话,我们知道元素在字典中,否则,我们知道它不是。

关于python - 如果我们仍然需要检查每个项目,哈希的含义是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53160472/

相关文章:

python - 如何将文件夹添加到给定 Anaconda 环境的搜索路径?

python - 删除列表中的元素,直到到达 Python 中的第一个空元素

python - tensorflow 在损失函数中使用输入

python - 根据条件转换数据框的列

python - 如何使用 tweepy/Python 在 Twitter 上关注某人?

python - Airflow 设置 dag 运行注意事项

python - 如何通过列中的分组值过滤数据框中的值

hash - 如何均衡两个 .ps 文件以获得相同的 md5 哈希值

php - 为什么我要把密码做成哈希码,然后保存在数据库中?

hash - 一致性哈希中副本和虚拟节点的区别