如何使用CRC32算法实现哈希表

761 阅读7分钟

使用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():处理提供给哈希函数的密钥的编码。使用多项式滚动哈希方案对任意长度的字符串进行编码。变量pm 必须是正数。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():处理insertupdate 到哈希表的键值对。如果键的索引没有被占用,它就向该键插入;否则,它就更新该键。
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-算法的哈希过程,以及实现基本的哈希函数。这个实现证明了散列是一种使用键值对轻松访问数据的有效方式。

鉴于散列表执行功能的速度如此之快,散列表肯定会对优化项目有用。