哈希表:高效数据结构背后的工作原理
哈希表是实现字典和符号表的基础数据结构之一,被广泛应用于算法和程序设计中。在哈希表中,键(key)和值(value)之间的映射关系通过散列函数(hash function)进行计算和存储。相比于传统数据结构,在哈希表中查找和插入数据的时间复杂度是常数级别的,因此具有高效的特点。本文将探讨哈希表的工作原理、关键概念和实现方法。
一、散列函数
散列函数是哈希表的核心组成部分,它将任意长度的数据(例如键值对)映射到固定大小的地址空间内。这个映射过程被称为散列(hashing),散列函数将输入数据映射为哈希表中的索引值,这个索引值也称为桶(bucket)。
由于哈希表的索引值是有限的,因此散列函数的映射存在概率冲突的风险。即不同的数据可能映射到同一个桶中,这个现象称为哈希冲突(hash collision)。解决哈希冲突的方法有多种,后文中会逐一介绍。
常见的散列函数有多种,其中最简单的是除法散列法(division hashing)。与取余操作不同的是,除法散列法是将输入数据除以某个质数,然后取余数作为索引值。这个过程可以用以下公式表示:
hash(x) = x % p
其中,x是输入数据,p是质数。这个散列函数被广泛应用于实际开发中。
另一种常见的散列函数是乘法散列法(multiplicative hashing),这个散列函数由Knuth提出。对于一个不超过w位的整数x,乘法散列法的计算公式为:
hash(x) = (ax mod 2^w ) >> (w - r)
其中a是一个介于1和2^w-1之间的整数,r是哈希表的大小。这个散列函数的优点是可以令哈希冲突的概率尽可能小,并且计算速度较快。
二、哈希冲突
哈希冲突是哈希表面临的一个重要问题,也是哈希表有效性的重要指标之一。产生哈希冲突的原因是散列函数映射的地址空间是有限的,当哈希表中的元素数量增多时,不可避免地会出现数据映射到同一个桶的情况。为了保证查询的正确性和效率,我们需要尽可能地避免哈希冲突的发生。
解决哈希冲突的方法有多种,以下是其中的几种:
1. 拉链法(Chaining)
拉链法是一种最基本的哈希冲突解决方法,它通过在哈希表中的每一个桶中维护一个单向链表,将冲突的元素放置在同一桶内。这个过程可以用以下伪代码表示:
class Node:
def __init__(self, key, val):
self.key, self.val = key, val
self.next = None
class HashTable:
def __init__(self, capacity):
self.capacity = capacity
self.buckets = [None] * capacity
def put(self, key, val):
index = self._hash(key)
node = self.buckets[index]
while node:
if node.key == key:
node.val = val
return
node = node.next
new_node = Node(key, val)
new_node.next = self.buckets[index]
self.buckets[index] = new_node
def get(self, key):
index = self._hash(key)
node = self.buckets[index]
while node:
if node.key == key:
return node.val
node = node.next
raise KeyError(key)
在哈希表中插入元素时,我们首先计算元素的哈希值,然后将这个元素插入到哈希表中对应的桶中。如果这个桶之前已经有了一些元素,则将这些元素按照顺序连接成一个单向链表。
在查询一个元素时,我们首先计算这个元素的哈希值,然后遍历哈希表中对应的桶,逐个比较桶中元素的键值与查询键值是否相同。如果相同,则返回对应的值,否则抛出KeyError异常。
拉链法的优点是实现简单,而且可以动态扩展哈希表的大小,因此通常被广泛应用于实际开发中。它的缺点是会占用更多的空间,并且在删除元素时需要对链表进行遍历和修改。
2. 线性探测法(Linear Probing)
线性探测法是另一种常用的哈希冲突解决方法,它可以将发生冲突的元素依次放置到哈希表中的下一个空闲位置,从而避免链表等数据结构的使用。
当我们在哈希表中插入一个元素时,如果这个元素映射到的桶已经被占用,我们就从当前位置开始,依次查找下一个空桶,将元素插入到这个空桶中。这个过程可以用以下伪代码表示:
class HashTable:
def __init__(self, capacity):
self.capacity = capacity
self.buckets = [None] * capacity
self.size = 0
def put(self, key, val):
if self.size == self.capacity:
raise Exception("HashTable is full")
index = self._hash(key)
while self.buckets[index] is not None:
if self.buckets[index][0] == key:
self.buckets[index][1] = val
return
index = (index + 1) % self.capacity
self.buckets[index] = (key, val)
self.size += 1
def get(self, key):
index = self._hash(key)
while self.buckets[index] is not None:
if self.buckets[index][0] == key:
return self.buckets[index][1]
index = (index + 1) % self.capacity
raise KeyError(key)
当我们在哈希表中查询一个元素时,首先计算元素的哈希值,然后依次查找哈希表中对应的桶,判断桶中元素的键值是否等于查询键值。如果当前桶不是我们要找的元素,就继续检查下一个桶,直到找到元素或者遍历完整个哈希表。
线性探测法的优点是可以避免链表等数据结构的使用,并且可以减少哈希表占用的空间。缺点是容易产生聚集效应(clustering),即存在元素总是被插入到同一区域的情况,从而导致查询的效率降低。
三、哈希表的实现
哈希表作为一种常见的数据结构,其实现方式有多种,常见的包括动态哈希表、可扩展哈希表和动态调整阈值哈希表等。
动态哈希表
动态哈希表是一种最基本的哈希表实现方法,它的特点在于可以动态增加和减少哈希表的大小,从而适应不同的场景需求。
动态哈希表最简单的实现方式是拉链法,它通过在每个桶中维护一个链表来处理哈希冲突。这个过程中,我们需要记录当前哈希表的元素个数和桶的数量,当元素个数超过桶的数量时,我们需要重新分配桶的数量。具体的实现过程如下:
class Node:
def __init__(self, key, val):
self.key, self.val = (key, val)
self.next = None
class HashTable:
def __init__(self, capacity=8, ratio=0.75):
self.capacity = capacity
self.ratio = ratio
self.threshold = int(capacity * ratio)
self.buckets = [None] * capacity
self.size = 0
def put(self, key, val):
if self.size + 1 > self.threshold:
self._resize()
index = self._hash(key)
node = self.buckets[index]
while node:
if node.key == key:
node.val = val
return
node = node.next
new_node = Node(key, val)
new_node.next = self.buckets[index]
self.buckets[index] = new_node
self.size += 1
def get(self, key):
index = self._hash(key)
node = self.buckets[index]
while node:
if node.key == key:
return node.val
node = node.next
raise KeyError(key)
def _resize(self):
new_buckets = [None] * self.capacity * 2
for node in self:
index = self._hash(node.key, len(new_buckets))
new_node = Node(node.key, node.val)
new_node.next = new_buckets[index]
new_buckets[index] = new_node
self.buckets = new_buckets
self.capacity *= 2
self.threshold = int(self.capacity * self.ratio)
def _hash(self, key, capacity=None):
if capacity is None:
capacity = self.capacity
return hash(key) % capacity
def __iter__(self):
for node in self.buckets:
while node:
yield node
node = node.next
在这个实现中,我们首先定义了一个Node类,用于存储元素的键值对;然后定义了HashTable类,包含put、get和resize等方法。
在插入一个元素时,我们首先计算元素的哈希值,然后在哈希表中的对应桶中查找元素。如果这个桶中不存在元素,则将元素插入到这个桶中。如果这个桶中已经存在元素,则遍历链表中的元素,查找与当前元素键值相同的节点,如果找到了,则更新值;否则,将元素插入到链表的头部。
在查询一个元素时,我们也首先计算元素的哈希值,然后在哈希表中的对应桶中查找元素。如果找到了节点,则返回值;否则,抛出KeyError异常。
当哈希表中的元素数量达到一个阈值时,我们需要调整哈希表的大小。具体操作是创建一个新的哈希表,然后将旧表中的元素全部复制到新表中。如果新哈希表的大小是原哈希表大小的两倍,则把所有元素移动到新的哈希表中。这个过程中,我们需要重新计算元素的哈希值,并映射到新表中的桶中。
可扩展哈希表
可扩展哈希表是一种另类的哈希表实现方式,它通过在哈希表的最左侧添加一位标志位,根据标志位将哈希表中的元素分配到不同的子哈希表中。这样,可扩展哈希表可以在哈希表元素数量增长时自动扩展哈希表大小,并且实现简单。
可扩展哈希表的基本思路是将哈希表分为2^k个桶,k为标志位的长度。然后,在哈希表中的每个桶中,再把哈希表分为2^(k-1)个子哈希表中,以此类推。这样,哈希表中的元素就可以被分配到不同的子哈希表中,从而实现动态调整。
在可扩展哈希表中,我们定义了一个Bucket类,用于存储多个子哈希表;一个Node类,用于存储单个哈希表元素。具体实现如下:
class Node:
def __init__(self, key, val):
self.key, self.val = key, val
class Bucket:
def __init__(self):
self.is_leaf = True
self.bits = 0
self.nodes = [None] * 2
self.capacity = 2
self.size = 0
def put(self, key, val):
index = self._hash(key)
node = self.nodes[index]
while node:
if node.key == key:
node.val = val
return
node = node.next
new_node = Node(key, val)
new_node.next = self.nodes[index]
self.nodes[index] = new_node
self.size += 1
self._resize()
def get(self, key):
index = self._hash(key)
node = self.nodes[index]
while node:
if node.key == key:
return node.val
node = node.next
raise KeyError(key)
def _resize(self):
if self.size / self.capacity < 0.5:
return
new_capacity = self.capacity * 2
new_nodes = [None] * new_capacity
for node in self:
index = node.key % new_capacity
new_node = Node(node.key, node.val)
new_node.next = new_nodes[index]
new_nodes[index] = new_node
self.nodes = new_nodes
self.capacity = new_capacity
def _hash(self, key):
return (key >> self.bits) & 1
def __iter__(self):
if not self.is_leaf:
for node in self.nodes[0]:
if node is not None:
yield from node
for node in self.nodes[1]:
if node is not None:
yield from node
else:
for node in self.nodes:
if node is not None:
yield node
在这个实现中,我们首先定义了一个Node类,用于存储单个哈希表元素;然后定义了一个Bucket类,用于存储哈希表中的多个子哈希表。
在插入一个元素时,我们首先计算元素的哈希值,然后根据标志位将元素分配到不同的子哈希表中。如果当前子哈希表中已经存在了对应键值的元素,则更新它的值;否则,将元素插入到该子哈希表中的链表中。当子哈希表的元素数量达到一定阈值时,我们需要重新分配该子哈希表中的元素,同时将该子哈希表拆分成两个,以此类推。
在查询一个元素时,我们也首先计算元素的哈希值,然后根据标志位将元素定位到对应子哈希表中,依次查找该子哈希表中的链表,寻找与之匹配的元素。如果找到了元素,则返回它的值;否则,抛出KeyError异常。
四、总结
哈希表是一种高效的数据结构,能够快速地实现数据的查找、插入和删除。哈希表的实现方式有多种,常见的包括拉链法、线性探测法、可扩展哈希表和动态调整阈值哈希表等。不同的实现方式各有优缺点,我们需要根据场景需求来选择适合的实现方式