前言:

查看 https://wiki.python.org/moin/TimeComplexity 中的数据可以看到

在 list 中查找元素的复杂度为 O(n) , 在 dict 中查找元素的复杂度 为 O(1)

本文来探讨以下其中的原理。

1. hash

字典建立在另一种技术之上:哈希表

hash 函数是一种可以将任意长度的数据映射到固定长度值的函数,称为哈希

hash 函数有 3 大特点:

  • 速度快:
  • 确定性: 同一数据hash计算后,永远产生唯一的确定值。
  • 固定长度:无论输入多大的数据, 输出永远是一个固定的预定长度

hash 函数通常是单向函数:您可以从字符串中获取哈希散列,但无法从哈希散列中获取原始字符串。常见哈希算法有 MD5SHA-1SHA-2NTLM

1.1 python 中的 hash 函数

Python 有一个内置函数来生成对象的哈希值,hash() 函数。该函数将一个对象作为输入,并返回int 类型的哈希值。

In [1]: hash('abc')
Out[1]: -3391815365988244928
In [2]: hash('dsjfkueriwufjasklklxcnvmsjaflkjoeurioeafklajfklasdklghksadhduiweyrioahfkasgkhakryaeiorhklaghkadshgkjahglkahtuehakjgfakjhgjkahgkjahjgkasdhjkncnmajkfahdkfhqieyrahfaklhoieior92374972398akjfhajk19207ashkdhfkjahgdklafiaufiou39274quriefiahrypi3q2i7prq')
Out[2]: -2151681143199748630

In [3]: hash(100.01)
Out[3]: 23058430092148836
In [4]: hash(23058430092148836)
Out[4]: 23058430092148836

通过以上代码可以看到,输入数据的长度为很短的 'abc'

另外,可以发现 hash(100.01) 和 hash(23058430092148836) 返回的值是相同的。

这是完全正常的,因为如果哈希任何数字或任何字符串以获得固定长度值,因为不能由固定长度值表示的无限值,也就是说必然会有重复的值。它们被称为碰撞,当两个对象具有相同的哈希值时,就说它们发生了碰撞。

1.2 python 中的可 hash 数据类型

默认情况下,只有不可变类型在 Python 中是可 hash 的。如果您使用的是不可变的容器(tuple),那么tuple 内的内容也应该是不可变的,数据整体是可 hash 的。因为 dict 构建在 哈希表之上, 所以要求 字典的key必须是可 hash 的。当我们尝试将 不可哈希的数据赋值给字典的key时 会引发 TypeError

In [1]: dict_a = {}
In [2]: dict_a['key'] = 'value'
In [3]: dict_a[('tuple', 'taple')] = 'tuple key'

In [4]: dict_a[[1, 2, 3]] = 'invalid key'
---------------------------------------------------------------------------
TypeError: unhashable type: 'list'

2. 哈希表

哈希表是一种允许您存储键值对集合的数据结构。存储的键值对通过使用其键的哈希来索引,所以查找表元素所需的平均指令数与存储在表本身中的元素数无关。

哈希表通常是通过创建可变数量的存储桶来实现,这些将包含您的数据 value,并通过对它们的键进行哈希来索引这些数据。键的哈希值将确定用于该特定数据的正确存储桶。

2.1 hash表索引

2.1.1 列表-线性扫描

列表中检查 是否存在元素代码  33 in [3, 47, 18, 52, 15, 9, 33, 20] ,执行过程如下图, 需要遍历扫描每一个位置的数据, 最坏情况下,要扫描整个列表才返回 False, 所以列表查找元素的复杂度为 O(n)

python opencv感知哈希算法 哈希函数python_python

 2.1.2 hash表-单独链接

单独链接方式的hash表,使用一个嵌套列表,存储对象的hash值作为索引坐标, 将value放到列表中。 查找特定值时必须完全扫描嵌套列表中的数据。

为了方便, 使用一个简单的 hash 函数, 对原列表求值。 

In [1]: def hash(n):
   ...:     return n

In [2]: for i in [3, 47, 18, 52, 15, 9, 33, 20]:
   ...:     print(f'data {i}\t index {hash(i)%8}')
data 3   index 3
data 47  index 7
data 18  index 2
data 52  index 4
data 15  index 7
data 9   index 1
data 33  index 1
data 20  index 4

[3, 47, 18, 52, 15, 9, 33, 20] 列表存储到 hash 表中的结果如下图所示,依次对数据求hash然后对列表长度取余数,然后存储到对应位置,若当前位置已经有值,则存储到嵌套列表后面。 

python opencv感知哈希算法 哈希函数python_python opencv感知哈希算法_02

查找元素的复杂度 为 O(1)

 

python opencv感知哈希算法 哈希函数python_python_03

 

  2.1.3 ⭐ hash表-开放寻址 

在开放寻址策略的hash表,如果当前的桶被占用,只需向后继续搜索要使用的新桶。如果我们使新列表与原始列表的大小相同,则会发生太多冲突, 本例子中使用1.5倍, 长度为12

In [3]: for i in [3, 47, 18, 52, 15, 9, 33, 20]:
   ...:     print(f'data {i}\t index {hash(i)%12}')
data 3   index 3
data 47  index 11
data 18  index 6
data 52  index 4
data 15  index 3
data 9   index 9
data 33  index 9
data 20  index 8

[3, 47, 18, 52, 15, 9, 33, 20] 列表存储到 hash 表中的结果如下图所示,依次对数据求hash然后对列表长度取余数,然后存储到对应位置,若当前位置已经有值,则向后查找第一个空槽存储。

如下图所示, 元素15 目标位置为索引3,但是索引3 已经有了值,所以15就向后查找第一个空槽存到了索引4,(如果后续还有索引为3的数据,将被存到索引7的位置)

python opencv感知哈希算法 哈希函数python_python opencv感知哈希算法_04

 查找元素的过程, 对数据求hash之后,找到对应的索引,若当前元素的值不是所求的值,则继续向后遍历,直到遍历到None, 返回 False。

python opencv感知哈希算法 哈希函数python_pycharm_05

开放寻址策略的主要问题是,如果您还必须处理表中元素的删除,则需要执行逻辑删除而不是物理删除,因为如果删除在冲突期间占用桶的值,另一个永远不会找到碰撞的元素。

如上图, 如果删掉了元素9,33 也将无法访问

 

 3. python的dict

Python中dict 实现是一个开放寻址模式的hash表。

In [8]: import sys

In [9]: dict_a = {}

In [10]: for i in range(20):
    ...:     dict_a[i] = 100
    ...:     print(f"elements = {i+1} size = {sys.getsizeof(dict_a)}")
    ...:
elements = 1 size = 240
elements = 2 size = 240
elements = 3 size = 240
elements = 4 size = 240
elements = 5 size = 240
elements = 6 size = 368
elements = 7 size = 368
elements = 8 size = 368
elements = 9 size = 368
elements = 10 size = 368
elements = 11 size = 648
elements = 12 size = 648
elements = 13 size = 648
elements = 14 size = 648
elements = 15 size = 648
elements = 16 size = 648
elements = 17 size = 648
elements = 18 size = 648
elements = 19 size = 648
elements = 20 size = 648

 如代码所见,在插入第六个和第十一个元素后,dict 增长了,但为什么呢?因为为了使我们的 Python 哈希表快速并减少冲突,解释器会在字典变满三分之二时不断调整大小。

然后我们删除字典中的所有元素

In [11]: keys = list(dict_a.keys())

In [12]: for key in keys:
    ...:     del dict_a[key]

In [13]: dict_a
Out[13]: {}

In [14]: sys.getsizeof(dict_a)
Out[14]: 648

 会发现即使字典为空,空间也没有被释放。

但是,如果您通过调用 clear() 方法清空字典,由于它是批量删除,空间将被释放并达到其最小值 72 字节:

In [15]: dict_a.clear()

In [16]: sys.getsizeof(dict_a)
Out[16]: 72

 总结

dict 的底层实现是哈希表, 特点是占用内存多,用空间换取速度。

删除数据时,是逻辑删除,并不会释放对应的内存