摘要哈希表(Hash Table)是一种非常重要的数据结构,它通过哈希函数将键映射到数组索引,实现平均O(1)时间复杂度的查找、插入和删除操作。本文将深入讲解哈希表的基本原理、哈希函数设计、冲突处理方法以及各种实现方式。我们将详细介绍开放寻址法和链式地址法两种冲突处理策略,通过丰富的图示和Python代码示例,帮助读者全面掌握哈希表的核心知识和实践技巧,并探讨其在实际应用中的价值。

正文1. 引言哈希表(又称散列表)是一种根据键值对存储数据的数据结构,通过哈希函数计算键的哈希值,然后将数据存储在哈希值对应的数组位置上。哈希表具有平均O(1)时间复杂度的查找、插入和删除性能,是现代计算机科学中最重要的数据结构之一,广泛应用于数据库索引、缓存系统、编译器符号表等场景。

2. 哈希表基本概念2.1 哈希表定义哈希表是一种键值对存储结构,它通过哈希函数将键映射到数组索引,实现快速的数据访问。主要组成部分包括:

键(Key):用于标识数据的唯一标识符值(Value):与键关联的数据哈希函数(Hash Function):将键映射到数组索引的函数桶(Bucket):存储键值对的数组位置2.2 哈希函数哈希函数是哈希表的核心,它决定了键值对在数组中的存储位置。一个好的哈希函数应该具备以下特点:

确定性:相同键总是产生相同哈希值均匀分布:尽可能均匀地分布键值对高效性:计算速度快

def simple_hash(key: int, capacity: int) -> int:

"""简单哈希函数"""

return key % capacity

def multiplication_hash(key: int, capacity: int) -> int:

"""乘法哈希函数"""

A = 0.6180339887 # (√5 - 1) / 2

return int(capacity * ((key * A) % 1))

def djb2_hash(key: str) -> int:

"""DJB2字符串哈希函数"""

hash_value = 5381

for char in key:

hash_value = ((hash_value << 5) + hash_value) + ord(char)

return hash_value & 0x7FFFFFFF # 保证为正数

3. 哈希冲突处理由于哈希函数的输出空间通常小于键的输入空间,不同的键可能映射到相同的数组索引,这种现象称为哈希冲突。

3.1 链式地址法(Chaining)链式地址法通过在每个桶中维护一个链表来解决冲突,所有映射到同一索引的键值对都存储在该链表中。

class Pair:

"""键值对"""

def __init__(self, key: int, val: str):

self.key = key

self.val = val

class HashMapChaining:

"""链式地址哈希表"""

def __init__(self):

"""构造方法"""

self.size = 0 # 键值对数量

self.capacity = 4 # 哈希表容量

self.load_thres = 2.0 / 3.0 # 触发扩容的负载因子阈值

self.extend_ratio = 2 # 扩容倍数

self.buckets = [[] for _ in range(self.capacity)] # 桶数组

def hash_func(self, key: int) -> int:

"""哈希函数"""

return key % self.capacity

def load_factor(self) -> float:

"""负载因子"""

return self.size / self.capacity

def get(self, key: int) -> str | None:

"""查询操作"""

index = self.hash_func(key)

bucket = self.buckets[index]

# 遍历桶,若找到 key ,则返回对应 val

for pair in bucket:

if pair.key == key:

return pair.val

# 若未找到 key ,则返回 None

return None

def put(self, key: int, val: str):

"""添加操作"""

# 当负载因子超过阈值时,执行扩容

if self.load_factor() > self.load_thres:

self.extend()

index = self.hash_func(key)

bucket = self.buckets[index]

# 遍历桶,若遇到指定 key ,则更新对应 val 并返回

for pair in bucket:

if pair.key == key:

pair.val = val

return

# 若无该 key ,则将键值对添加至尾部

pair = Pair(key, val)

bucket.append(pair)

self.size += 1

def remove(self, key: int):

"""删除操作"""

index = self.hash_func(key)

bucket = self.buckets[index]

# 遍历桶,从中删除键值对

for pair in bucket:

if pair.key == key:

bucket.remove(pair)

self.size -= 1

break

def extend(self):

"""扩容哈希表"""

# 暂存原哈希表

buckets = self.buckets

# 初始化扩容后的新哈希表

self.capacity *= self.extend_ratio

self.buckets = [[] for _ in range(self.capacity)]

self.size = 0

# 将键值对从原哈希表搬运至新哈希表

for bucket in buckets:

for pair in bucket:

self.put(pair.key, pair.val)

def print(self):

"""打印哈希表"""

for bucket in self.buckets:

res = []

for pair in bucket:

res.append(str(pair.key) + " -> " + pair.val)

print(res)

# 使用示例

hashmap = HashMapChaining()

hashmap.put(12836, "小哈")

hashmap.put(15937, "小啰")

hashmap.put(16750, "小算")

hashmap.put(13276, "小法")

hashmap.put(10583, "小鸭")

print("添加完成后,哈希表为:")

hashmap.print()

name = hashmap.get(13276)

print(f"\n输入学号 13276 ,查询到姓名 {name}")

hashmap.remove(12836)

print("\n删除 12836 后,哈希表为:")

hashmap.print()

3.2 开放寻址法(Open Addressing)开放寻址法通过在哈希表数组中寻找空槽来解决冲突,当发生冲突时,按照某种探测序列寻找下一个可用位置。

class HashMapOpenAddressing:

"""开放寻址哈希表"""

def __init__(self):

"""构造方法"""

self.size = 0 # 键值对数量

self.capacity = 4 # 哈希表容量

self.load_thres = 0.75 # 触发扩容的负载因子阈值

self.extend_ratio = 2 # 扩容倍数

self.buckets: list[Pair | None] = [None] * self.capacity # 桶数组

self.deleted = Pair(-1, "-1") # 删除标记

def hash_func(self, key: int) -> int:

"""哈希函数"""

return key % self.capacity

def load_factor(self) -> float:

"""负载因子"""

return self.size / self.capacity

def find_bucket(self, key: int) -> int:

"""查找键值对应该所在的桶索引"""

index = self.hash_func(key)

first_tombstone = -1

# 线性探测,当遇到空桶时跳出

while self.buckets[index] is not None:

# 若遇到 key ,返回对应桶索引

if self.buckets[index].key == key:

# 若之前遇到了删除标记,则将键值对移动至该索引

if first_tombstone != -1:

self.buckets[first_tombstone] = self.buckets[index]

self.buckets[index] = self.deleted

return first_tombstone # 返回移动后的桶索引

return index # 返回桶索引

# 记录遇到的首个删除标记

if first_tombstone == -1 and self.buckets[index] is self.deleted:

first_tombstone = index

# 计算桶索引,越过尾部则返回头部

index = (index + 1) % self.capacity

# 若之前遇到了删除标记,则返回该索引

return index if first_tombstone == -1 else first_tombstone

def get(self, key: int) -> str | None:

"""查询操作"""

# 搜索 key 对应的桶索引

index = self.find_bucket(key)

# 若找到键值对,则返回对应 val

if self.buckets[index] not in [None, self.deleted]:

return self.buckets[index].val

# 若键值对不存在,则返回 None

return None

def put(self, key: int, val: str):

"""添加操作"""

# 当负载因子超过阈值时,执行扩容

if self.load_factor() > self.load_thres:

self.extend()

# 搜索 key 应该所在的桶索引

index = self.find_bucket(key)

# 若找到键值对,则更新 val 并返回

if self.buckets[index] not in [None, self.deleted]:

self.buckets[index].val = val

return

# 若键值对不存在,则添加该键值对

self.buckets[index] = Pair(key, val)

self.size += 1

def remove(self, key: int):

"""删除操作"""

# 搜索 key 对应的桶索引

index = self.find_bucket(key)

# 若找到键值对,则用删除标记覆盖它

if self.buckets[index] not in [None, self.deleted]:

self.buckets[index] = self.deleted

self.size -= 1

def extend(self):

"""扩容哈希表"""

# 暂存原哈希表

buckets = self.buckets

# 初始化扩容后的新哈希表

self.capacity *= self.extend_ratio

self.buckets = [None] * self.capacity

self.size = 0

# 将键值对从原哈希表搬运至新哈希表

for pair in buckets:

if pair not in [None, self.deleted]:

self.put(pair.key, pair.val)

def print(self):

"""打印哈希表"""

for pair in self.buckets:

if pair is None:

print("None")

elif pair is self.deleted:

print("Deleted")

else:

print(str(pair.key) + " -> " + pair.val)

# 使用示例

hashmap = HashMapOpenAddressing()

hashmap.put(12836, "小哈")

hashmap.put(15937, "小啰")

hashmap.put(16750, "小算")

hashmap.put(13276, "小法")

hashmap.put(10583, "小鸭")

print("添加完成后,哈希表为:")

hashmap.print()

name = hashmap.get(13276)

print(f"\n输入学号 13276 ,查询到姓名 {name}")

hashmap.remove(12836)

print("\n删除 12836 后,哈希表为:")

hashmap.print()

4. 哈希表性能分析4.1 时间复杂度操作平均情况最坏情况查找O(1)O(n)插入O(1)O(n)删除O(1)O(n)4.2 空间复杂度哈希表的空间复杂度为O(n),其中n为存储的键值对数量。

5. 哈希表优化策略5.1 动态扩容当哈希表的负载因子超过阈值时,需要进行扩容以维持性能:

def extend(self):

"""扩容哈希表"""

# 暂存原哈希表

buckets = self.buckets

# 初始化扩容后的新哈希表

self.capacity *= self.extend_ratio

self.buckets = [[] for _ in range(self.capacity)]

self.size = 0

# 将键值对从原哈希表搬运至新哈希表

for bucket in buckets:

for pair in bucket:

self.put(pair.key, pair.val)

5.2 哈希函数优化使用更好的哈希函数可以减少冲突,提高性能:

def better_hash_func(self, key: int) -> int:

"""更好的哈希函数"""

# 使用位运算优化模运算

if self.capacity & (self.capacity - 1) == 0: # 判断是否为2的幂

return key & (self.capacity - 1) # 使用位运算代替模运算

else:

return key % self.capacity

6. 哈希表应用案例6.1 LRU缓存

class LRUCache:

"""基于哈希表和双向链表实现的LRU缓存"""

def __init__(self, capacity: int):

self.capacity = capacity

self.cache = {} # 哈希表,存储键到节点的映射

self.head = Node(0, 0) # 虚拟头节点

self.tail = Node(0, 0) # 虚拟尾节点

self.head.next = self.tail

self.tail.prev = self.head

def get(self, key: int) -> int:

"""获取键对应的值"""

if key in self.cache:

node = self.cache[key]

self._move_to_head(node)

return node.value

return -1

def put(self, key: int, value: int) -> None:

"""插入键值对"""

if key in self.cache:

node = self.cache[key]

node.value = value

self._move_to_head(node)

else:

if len(self.cache) >= self.capacity:

# 删除最久未使用的节点

tail = self._pop_tail()

del self.cache[tail.key]

# 添加新节点

new_node = Node(key, value)

self.cache[key] = new_node

self._add_node(new_node)

def _add_node(self, node):

"""在头部添加节点"""

node.prev = self.head

node.next = self.head.next

self.head.next.prev = node

self.head.next = node

def _remove_node(self, node):

"""删除节点"""

prev = node.prev

new = node.next

prev.next = new

new.prev = prev

def _move_to_head(self, node):

"""将节点移动到头部"""

self._remove_node(node)

self._add_node(node)

def _pop_tail(self):

"""弹出尾部节点"""

res = self.tail.prev

self._remove_node(res)

return res

class Node:

"""双向链表节点"""

def __init__(self, key: int, value: int):

self.key = key

self.value = value

self.prev = None

self.next = None

# 使用示例

lru_cache = LRUCache(2)

lru_cache.put(1, 1)

lru_cache.put(2, 2)

print(lru_cache.get(1)) # 返回 1

lru_cache.put(3, 3) # 该操作会使得键 2 作废

print(lru_cache.get(2)) # 返回 -1 (未找到)

lru_cache.put(4, 4) # 该操作会使得键 1 作废

print(lru_cache.get(1)) # 返回 -1 (未找到)

print(lru_cache.get(3)) # 返回 3

print(lru_cache.get(4)) # 返回 4

6.2 字符串匹配

def rabin_karp(pattern: str, text: str) -> list[int]:

"""Rabin-Karp字符串匹配算法"""

# 哈希参数

base = 256 # 字符集大小

prime = 101 # 大质数

m = len(pattern)

n = len(text)

pattern_hash = 0 # 模式串哈希值

text_hash = 0 # 文本串哈希值

h = 1 # base^(m-1) mod prime

# 计算 h = base^(m-1) mod prime

for i in range(m - 1):

h = (h * base) % prime

# 计算模式串和文本串前m个字符的哈希值

for i in range(m):

pattern_hash = (base * pattern_hash + ord(pattern[i])) % prime

text_hash = (base * text_hash + ord(text[i])) % prime

# 查找匹配

matches = []

for i in range(n - m + 1):

# 检查哈希值是否匹配

if pattern_hash == text_hash:

# 逐字符检查以避免哈希冲突

if text[i:i + m] == pattern:

matches.append(i)

# 计算下一个窗口的哈希值

if i < n - m:

text_hash = (base * (text_hash - ord(text[i]) * h) + ord(text[i + m])) % prime

# 处理负数情况

if text_hash < 0:

text_hash += prime

return matches

# 使用示例

pattern = "abc"

text = "ababcabc"

matches = rabin_karp(pattern, text)

print(f"模式串 '{pattern}' 在文本串 '{text}' 中的匹配位置: {matches}")

6.3 布隆过滤器

import hashlib

class BloomFilter:

"""布隆过滤器"""

def __init__(self, size: int, hash_count: int):

self.size = size

self.hash_count = hash_count

self.bit_array = [0] * size

def _hash(self, item: str, seed: int) -> int:

"""哈希函数"""

hash_obj = hashlib.md5((item + str(seed)).encode())

return int(hash_obj.hexdigest(), 16) % self.size

def add(self, item: str):

"""添加元素"""

for i in range(self.hash_count):

index = self._hash(item, i)

self.bit_array[index] = 1

def check(self, item: str) -> bool:

"""检查元素是否存在"""

for i in range(self.hash_count):

index = self._hash(item, i)

if self.bit_array[index] == 0:

return False

return True

# 使用示例

bf = BloomFilter(1000, 3)

bf.add("apple")

bf.add("banana")

bf.add("orange")

print(f"'apple' 是否存在: {bf.check('apple')}")

print(f"'grape' 是否存在: {bf.check('grape')}")

print(f"'banana' 是否存在: {bf.check('banana')}")

7. Python内置字典和集合Python的dict和set都是基于哈希表实现的:

# 字典操作示例

student_scores = {

"Alice": 95,

"Bob": 87,

"Charlie": 92,

"David": 78

}

# 添加元素

student_scores["Eve"] = 88

# 查找元素

if "Alice" in student_scores:

print(f"Alice的分数: {student_scores['Alice']}")

# 删除元素

del student_scores["David"]

# 遍历字典

for name, score in student_scores.items():

print(f"{name}: {score}")

# 集合操作示例

set1 = {1, 2, 3, 4, 5}

set2 = {4, 5, 6, 7, 8}

# 集合运算

print(f"并集: {set1 | set2}")

print(f"交集: {set1 & set2}")

print(f"差集: {set1 - set2}")

print(f"对称差集: {set1 ^ set2}")

总结哈希表是一种极其重要的数据结构,具有以下特点和应用价值:

基本特性:

平均O(1)时间复杂度的查找、插入和删除操作通过哈希函数将键映射到数组索引需要处理哈希冲突问题冲突处理方法:

链式地址法:使用链表存储冲突的键值对开放寻址法:在数组中寻找空槽存储冲突的键值对关键优化策略:

动态扩容维持负载因子在合理范围设计良好的哈希函数减少冲突合理选择冲突处理方法主要应用:

数据库索引和缓存系统编译器符号表算法优化(如LRU缓存)大数据去重和存在性检测(布隆过滤器)实际价值:

是现代软件系统中不可或缺的基础数据结构在需要快速查找的场景中具有无可替代的优势是许多高级数据结构和算法的基础掌握哈希表的原理和实现对于理解现代计算机系统的工作原理具有重要意义。在实际开发中,我们应该根据具体需求选择合适的哈希表实现,并结合其他数据结构来解决复杂问题。

参考资料Hello 算法项目: https://www.hello-algo.com/《算法导论》第三版《数据结构与算法分析》Python官方文档: https://docs.python.org/Redis源码: https://github.com/redis/redis