Python 中的哈希表

哈希表

哈希表(Hash Table)是一种旨在实现快速操作的数据结构。

人们有时会选择使用哈希表而非数组或链表,原因在于,即使处理大量数据,哈希表也能非常快速地完成数据的搜索、添加和删除操作。

在链表中,查找名为 "Bob" 的人需要耗费时间,因为我们必须从一个节点遍历到下一个节点,逐个检查每个节点,直到找到包含 "Bob" 的节点。

如果已知索引,在列表/数组中查找“Bob”可能会很快,但当我们只知道名字 "Bob" 时,就需要逐个比较元素,这会耗费时间。

然而,在哈希表中,查找 "Bob" 的速度非常快,因为有一种方法可以直接定位到 "Bob" 的存储位置,这种方法就是使用哈希函数。

从零开始构建哈希表

为了理解哈希表是什么,让我们尝试从零开始构建一个,用于存储唯一的名字。

我们将分五个步骤构建哈希表:

  1. 创建一个空列表(也可以是字典或集合)。
  2. 创建一个哈希函数。
  3. 使用哈希函数插入元素。
  4. 使用哈希函数查找元素。
  5. 处理冲突。

第一步:创建一个空列表

为了简单起见,让我们创建一个包含 10 个空元素的列表。

my_list = [None, None, None, None, None, None, None, None, None, None]

在哈希表中,这些元素中的每一个都称为一个桶(bucket)

第二步:创建一个哈希函数

现在,我们来看看与哈希表交互的特殊方式。

我们希望将名字直接存储到数组中的正确位置,这时哈希函数就派上用场了。

哈希函数可以通过多种方式创建,这取决于哈希表的创建者。一种常见的方法是找到一种方法,将值转换为一个数字,该数字等于哈希表的索引号之一,在本例中是 0 到 9 之间的数字。

在我们的示例中,我们将使用每个字符的 Unicode 码点,将它们相加,然后进行模 10 运算,以得到 0 到 9 之间的索引号。

实例

创建一个哈希函数,该函数将每个字符的 Unicode 码点相加,并返回一个 0 到 9 之间的数字:

def hash_function(value):
  sum_of_chars = 0
  for char in value:
    sum_of_chars += ord(char)

  return sum_of_chars % 10

print("'Bob' has hash code:", hash_function('Bob'))

亲自试一试

字符 B 的 Unicode 码点是 66o111b98。将它们相加得到 275275 模 10 的结果是 5,因此 "Bob" 应该存储在索引 5 处。

哈希函数返回的数字称为哈希码(hash code)

Unicode 码点:我们计算机中的所有内容都以数字形式存储,Unicode 码点是每个字符独有的数字。例如,字符 A 的 Unicode 码点是 65

有关字符如何表示为数字的更多信息,请参阅此页面。

模运算:模运算将一个数除以另一个数,并给出余数。例如,7 % 3 将给出余数 1。(将 7 个苹果分给 3 个人,意味着每个人得到 2 个苹果,还剩下 1 个苹果。)

在 Python 和大多数编程语言中,模运算符写作 %

第三步:插入元素

根据我们的哈希函数,"Bob" 应该存储在索引 5 处。

让我们创建一个函数,用于向哈希表中添加项:

实例

def add(name):
  index = hash_function(name)
  my_list[index] = name

add('Bob')
print(my_list)

亲自试一试

在索引 5 处存储“Bob”后,我们的数组现在如下所示:

my_list = [None, None, None, None, None, 'Bob', None, None, None, None]

我们可以使用相同的函数来存储 "Pete"、"Jones"、"Lisa" 和 "Siri"。

实例

add('Pete')
add('Jones')
add('Lisa')
add('Siri')
print(my_list)

亲自试一试

使用哈希函数将这些名字存储在正确的位置后,我们的数组如下所示:

实例

my_list = [None, 'Jones', None, 'Lisa', None, 'Bob', None, 'Siri', 'Pete', None]

第四步:查找名字

现在,我们已经有了一个非常基础的哈希表,让我们看看如何从中查找一个名字。

要在哈希表中查找 "Pete",我们将名字 "Pete" 传递给哈希函数。哈希函数返回 8,这意味着 "Pete" 存储在索引 8 处。

实例

def contains(name):
  index = hash_function(name)
  return my_list[index] == name

print("'Pete' is in the Hash Table:", contains('Pete'))

亲自试一试

因为我们不需要逐个检查元素来确定 "Pete" 是否在其中,我们可以直接使用哈希函数跳转到正确的元素!

第五步:处理冲突

让我们也将 "Stuart" 添加到我们的哈希表中。

我们将 "Stuart" 传递给哈希函数,它返回 3,这意味着 "Stuart" 应该存储在索引 3 处。

尝试将 "Stuart" 存储在索引 3 处时,会出现所谓的冲突,因为 "Lisa" 已经存储在索引 3 处。

为了解决冲突,我们可以在同一个桶中为更多元素腾出空间。以这种方式解决冲突问题称为链地址法(chaining),即在同一个桶中为更多元素提供空间。

首先,创建一个与原始列表大小相同但桶为空的新列表:

my_list = [
  [],
  [],
  [],
  [],
  [],
  [],
  [],
  [],
  [],
  []
]

重写 add() 函数,并添加与之前相同的名字:

实例

def add(name):
  index = hash_function(name)
  my_list[index].append(name)

add('Bob')
add('Pete')
add('Jones')
add('Lisa')
add('Siri')
add('Stuart')
print(my_list)

亲自试一试

在将每个桶实现为列表后,"Stuart" 也可以存储在索引 3 处,我们的哈希集合现在如下所示:

结果

my_list = [
  [None],
  ['Jones'],
  [None],
  ['Lisa', 'Stuart'],
  [None],
  ['Bob'],
  [None],
  ['Siri'],
  ['Pete'],
  [None]
]

现在查找 "Stuart" 需要稍微多一点时间,因为我们还会在同一个桶中找到 "Lisa",但仍然比搜索整个哈希表快得多。

哈希表的用途

哈希表非常适合:

  • 检查某物是否在集合中(比如在图书馆中查找一本书)。
  • 存储唯一项并快速查找它们(比如存储电话号码)。
  • 将值与键关联起来(比如将名字与电话号码关联起来)。

哈希表之所以非常适合这些用途,最重要的原因是与数组和链表相比,哈希表的速度非常快,尤其是对于大数据集而言。数组和链表的搜索和删除操作的时间复杂度为 O(n),而哈希表的平均时间复杂度仅为 O(1)

哈希表总结

哈希表元素存储在称为的存储容器中。

哈希函数使用元素的键来生成哈希码

哈希码指示元素属于哪个桶,因此我们可以直接定位到该哈希表元素:对其进行修改、删除或检查其是否存在。

当两个哈希表元素具有相同的哈希码时,就会发生冲突,因为这意味着它们属于同一个

冲突可以通过链地址法解决,即使用列表来允许同一个桶中有多个元素。