使用CRC32算法实现哈希表
哈希表是最方便的数据结构,在大多数编程语言中都可以轻松访问。它使用一个关联数组来存储数据和检索数据。每个数据值都有其独特的索引。与其在整个数组中搜索,索引被用来直接访问所需元素。
无论数据大小如何,它都能减少搜索操作的时间。给定一个键值对,计算出键的哈希代码,并将其作为存储在表中的值的索引。
先决条件
要开始学习,你将需要了解Python数组、列表和类。你还需要一个代码编辑器和一个Python解释器来运行你的代码。
我们将使用visual studio code,它有一个用于运行代码的集成终端。你也可以使用PyCharm来做同样的事情。
1.HashTable类
首先,我们需要在我们的环境中拥有所有我们要使用的Python模块。我们将导入randint,Typevar,Callable, 和List 。然后我们定义我们的HashTable 类及其数据成员。
from random import randint
from typing import TypeVar, Callable, List
from bisect import bisect_left
T = TypeVar('T')
class HashTable:
def __init__(self):
# Initial table size
self.table_size: int = 23
# initilizing all table slots to none
self.table: List[(T, T)] = [None] * self.table_size
# number of filled slots
self.filled_count: int = 0
# table resize threshold
self.resize_threshold: float = 0.75
# crc32 hash function
self.hash_function: Callable = self.crc32_hash
# crc32 table
self.crc32_table: List[int] = self.crc32_table()
# number of comparison
self.key_comparison_counts: int = 0
#random integer for use when the key is not a string
self.a: int = randint(1, 2**32)
# random integer for use in secondary hashing
self.b: int = randint(1, 2**32)
def __len__(self) -> int:
""" Returns number of (key, value) pairs in table """
return self.filled_count
def __repr__(self) -> str:
""" Returns HashTable's string representation
({key1: value1, key2: value2, ..., keyN: valueN})
"""
r: str = "{" + ''.join([(f'\"{pair[0]}\"' if isinstance(pair[0], str) else str(pair[0])) + ': ' +
(f'\"{pair[1]}\"' if isinstance(
pair[1], str) else str(pair[1])) + ', '
for pair in self.table if pair is not None])
return r[:-2] + "}" if len(r) > 1 else "{}"
def __setitem__(self, key: T, value: T) -> None:
""" Allows `table[key] = value` instead of `table.update(key, value)` """
self.update(key, value)
def __getitem__(self, key: T) -> T:
""" Allows `table[key]` instead of `table.lookup(key)` """
return self.lookup(key)
def __delitem__(self, key: T) -> None:
""" Allows `del table[key]` instead of `table.delete(key)` """
self.delete(key)
2.类方法和散列
我们已经成功地创建了数据成员。现在我们开始编写我们的类方法。这个实现是基于以下的类方法。
load_factor():这个函数负责计算表的负载因子。负载因子告诉我们表格槽的填充百分比。一旦负载因子达到75%,就应该调用调整大小函数。负载率是通过填充的槽的数量除以表的大小而得到的。
@property
def load_factor(self) -> float:
""" Calculate table's load factor """
return self.filled_count / self.table_size
encode():处理提供给哈希函数的密钥的编码。使用多项式滚动哈希方案对任意长度的字符串进行编码。变量p和m必须是正数。p应该是一个素数,大致等于输入字母表的字符数。
对于我们的情况,可打印的ASCII字符数是95;因此,我们的p 值是97。另一方面,m 应该是一个巨大的素数。我们选择了=115561578124838522881 ,它是在线整数序列百科全书A118839(OEIS)中的第20个质数。该函数返回所提供的键的一个整数hash value 。
@staticmethod
def encode(key: T) -> int:
"""
Encoding the key (string or integer)
Strings of arbitrary length are encoded using a polynomial rolling hash scheme
"""
if isinstance(key, str):
hash_value: int = 0
# p should be a prime number roughly equal to the number of characters in the input alphabet.
p: int = 97
# We have 95 printable ASCII characters, so we use 95
m: int = 115561578124838522881
# this should be a huge prime number 20th in the OEIS A118839
p_power: int = 1
for character in key:
hash_value = (hash_value + ord(character) * p_power) % m
p_power = (p_power * p) % m
return hash_value
elif isinstance(key, int):
return key
else:
raise Exception(
f"Cannot encode {type(key)} (Only integers & strings supported)")
crc32_table()生成一个用于CRC32哈希方法的数值表
@staticmethod
def crc32_table() -> List[int]:
"""Returns a table of values for use with the CRC32 hash"""
table: List[int] = []
for i in range(256):
k: int = i
for j in range(8):
if k & 1:
k ^= 0x1db710640
k >>= 1
table.append(k)
return table
crc32_hash():这个是Hashing方法。它对由encode ()方法生成的密钥进行哈希。它使用最小重要位数优先的顺序,将初始CRC设置为FFFFFFF16,并对最终的CRC进行补充。
CRC32算法产生的校验和分布非常好,我们将其作为散列函数使用。
def crc32_hash(self, key: T) -> int:
# if the key is a string
if isinstance(key, str):
crc32: int = 0xffffffff
# encode the characters as the function do not accept strings
for k in key.encode('utf-8'):
crc32 = (crc32 >> 8) ^ self.crc32_table[(crc32 & 0xff) ^ k]
crc32 ^= 0xffffffff # invert all bits
return crc32 % self.table_size
else:
""" Returns a hash of key using h(k) = (a * key) mod m where m is a prime number """
return (self.a * self.encode(key)) % self.table_size
secondary_hash():二级哈希值用于检测到碰撞时的线性探测。我们正在使用一个随机数b,对表的大小进行调制,以找到双重散列的间隔索引。
def secondary_hash(self, key) -> int:
""" Secondary hashing function for double hashing """
index: int = (self.b * self.encode(key)) % self.table_size
return index if index != 0 else 1
resize():一旦load factor达到阈值,这个函数就会增加表的大小。表的大小被调整为大于2*表的_current_size的最小的素数。我们应用Eratosthenes的筛子来寻找素数。在我们调整了表的大小之后,表中的所有条目都被重新洗牌。
def resize(self) -> None:
size: int = 2 * self.table_size + 1
if size > self.primes_table[len(self.primes_table) - 1]:
self.primes_table = self.primes_below_n(10 * size)
size: int = self.primes_table[bisect_left(self.primes_table, size)]
# rehash all entries of the hash table after the increase in table size
temp: List[(T, T)] = self.table
self.table_size = size
self.table = [None] * self.table_size
self.filled_count = 0
for pair in temp:
if pair is not None:
self[pair[0]] = pair[1]
3.对哈希表的操作
下面的方法列表显示了我们可以对Hashtables进行的基本操作。
find():使用双散列法获取第一个占用的位置。处理表内键的搜索。如果搜索成功,它返回找到的配对值;否则,它提出一个异常,即该键在表中不存在。
def find(self, key) -> int:
""" Finds the first occupied position using double hashing """
try:
# check with primary hashing if there is a matching value
index: int = self.hash_function(key)
if self.table[index][0] == key:
return index
# use secondary hashing function to find an interval to use
index2: int = self.secondary_hash(key)
i: int = 1
while self.table[(index + i * index2) % self.table_size][0] != key:
i += 1
self.key_comparison_counts += 1
except TypeError as err:
raise Exception("Key does not exist in hash table") from err
return (index + i * index2) % self.table_size
lookup():处理表内键的搜索。如果搜索成功,它返回找到的配对值;否则,它引发一个异常,即该键在表中不存在。
def lookup(self, key: T) -> T:
""" Handles lookup/search of key in table. Returns value if key is found """
index: int = self.hash_function(key) # get an index location for 'key'
if self.table[index] is None: # 'key' doesn't exists in hash table
raise Exception("Key doesn't exist in hashtable")
else:
self.key_comparison_counts += 1
return self.table[self.find(key)][1] # return pair value
delete():从哈希表中删除一个键值对。为删除提供的键被散列,然后产生的散列值被用来定位一个元素。如果找到该元素,它将被删除。
def delete(self, key: T) -> None:
""" Deletes a (key, value) pair from the hash table """
index: int = self.hash_function(key) # get an index location for 'key'
if self.table[index] is None: # 'key' doesn't exists in hash table
raise Exception("Key doesn't exist in hashtable")
else:
self.table[self.find(key)] = None # delete value at 'key'
self.filled_count -= 1
update():处理insert和update到哈希表的键值对。如果键的索引没有被占用,它就向该键插入;否则,它就更新该键。
def update(self, key: T, value: T) -> None:
# check if load fcator is equal to or greater than the resize threshold
if self.load_factor >= self.resize_threshold:
self.resize()
# get an index location for 'key'
index: int = self.hash_function(key)
# index location not occupied
if self.table[index] is None:
self.table[index] = (key, value)
self.filled_count += 1
else: # idx location occupied
if self.table[index][0] == key: # trying to insert to the same key
# update 'value' at 'key'
self.table[index] = (self.table[index][0], value)
else:
# probe for next free position using double hashing(secondary hash function)
index2: int = self.secondary_hash(key)
i: int = 1
while self.table[(index + i * index2) % self.table_size] is not None:
i += 1
# insert at an unoccupied location
self.table[(index + i * index2) %
self.table_size] = (key, value)
self.filled_count += 1
4.单元测试
这就完成了我们实现部分的代码。
test_empty_table().如果表的大小为零,则返回true。AssertEquals,用于确定长度和负载系数是否都为零。
def test_empty_table(self):
table = HashTable()
self.assertEqual(str(table), '{}')
self.assertEqual(len(table), 0)
self.assertEqual(table.load_factor, 0)
test_update_ops().该方法检查现有的键,并将与该键相关的值替换为所提供的输入值。AssertEqual,检查键的值是否与替换时提供的值相似。
def test_update_ops(self):
table = HashTable()
table["Apple"] = "Steve Jobs"
table["Apple"] = "Tim Cook"
self.assertEqual(table["Apple"], "Tim Cook")
test_delete().我们在表中插入Porshe,因为它必须存在才能被删除。通过AssertRaises,如果在`del table ["Porsche"] 操作之后,Porsche存在于表中,该方法应该返回一个错误,否则成功。
def test_delete(self):
table = HashTable()
table['Porsche'] = "Oliver Blume"
del table["Porsche"]
with self.assertRaises(Exception):
table["Porche"]
test_encoding().测试encode方法的成功。
def test_encoding(self):
self.assertEqual(HashTable.encode("Azc8{"), 10941154641)
test_crc32().使用binascii.crc32,我们检查我们的哈希函数对于相同的输入值是否返回与binascii.crc32相同的值。
def test_crc32(self):
table = HashTable()
table.hash_function = table.crc32_hash
self.assertEqual(table.crc32_hash("hello-world"),binascii.crc32(b"hello-world") % table.table_size)
总结
在这篇文章中,我们学习了如何创建一个哈希表,使用CRC32-算法的哈希过程,以及实现基本的哈希函数。这个实现证明了散列是一种使用键值对轻松访问数据的有效方式。
鉴于散列表执行功能的速度如此之快,散列表肯定会对优化项目有用。