目录
哈希表知识全解析
在计算机科学领域,哈希表是一种极为重要的数据结构。它能够显著加快查找速度,提升数据的访问效率,在诸多场景中都有着广泛的应用。
哈希表的定义
哈希表(Hash Table),又称为散列表,是根据关键码值(Key value)而直接进行访问的数据结构。它通过一个哈希函数(Hash Function),将关键码值映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组叫做哈希表。
哈希函数
哈希函数是哈希表的核心部分,它的设计至关重要。一个好的哈希函数应该具备两个关键特性:易于计算和均匀分布。易于计算可以保证在进行映射时不会消耗过多的时间和资源;均匀分布则能使数据尽可能均匀地分布在哈希表中,减少冲突的发生。
常见的哈希函数有以下几种:
- 直接定址法:取关键字或关键字的某个线性函数值为哈希地址。即 Hash (key)=key 或 Hash (key) = a×key + b,其中 a 和 b 为常数。这种方法简单直接,但适用范围有限,通常在关键字分布连续且范围较小时使用。
- 除留余数法:取关键字被某个不大于哈希表表长 m 的数 p 除后所得的余数为哈希地址。即 Hash (key)=key % p(p ≤ m)。p 的选择很关键,一般取质数或不包含小于 20 的质因数的合数,这样能使哈希值分布更均匀。
- 平方取中法:先计算关键字的平方值,然后取中间几位作为哈希地址。这种方法适用于关键字的每一位取值都不够随机,或分布不均匀的情况,通过平方运算可以使中间几位的随机性增强,从而得到更均匀的哈希值。
哈希冲突解决方法
尽管我们努力设计好的哈希函数,但冲突仍然难以完全避免。当两个或多个不同的关键字通过哈希函数得到相同的哈希地址时,就发生了哈希冲突。以下是两种常见的解决哈希冲突的方法:
- 开放地址法:当发生冲突时,按照某种规则继续探测哈希表中的其他存储单元,直到找到一个空闲单元为止。常见的探测方法有线性探测法、二次探测法等。线性探测法就是在发生冲突时,顺序探测下一个单元,即 Hi=(H (key)+i) % m(i = 1, 2, …, n),其中 m 为哈希表表长。二次探测法的探测序列为 Hi=(H (key)+i^2) % m(i = 1, 2, …, n),它可以减少线性探测法中出现的 “聚集” 现象。
- 链地址法:将所有哈希地址相同的记录都链接在同一个链表中。当发生冲突时,将新记录插入到对应的链表中。这种方法适合数据量大、冲突较多的情况,它不会因为冲突而增加哈希表的查找时间,只需遍历链表即可。
哈希表在生活中的应用
哈希表在我们的日常生活中有着许多实际应用。例如,查字典就是一个典型的例子。字典中的每个单词都相当于一个关键字,通过某种规则(类似于哈希函数)可以快速定位到该单词所在的页码,从而大大提高查找效率。在计算机领域,哈希表也被广泛应用于数据库索引、缓存系统、编译器符号表等方面。
例题解析
给定一个数组,使用哈希表来判断每一行、每一列、每一个 3 乘 3 的 9 宫格是否出现了重复的数字,以确定数独是否有效。
解题思路:我们可以使用三个哈希表分别记录每一行、每一列和每一个 3 乘 3 宫格中出现的数字。遍历数独数组,对于每一个数字,检查它在对应的行、列和宫格中是否已经出现过。如果出现过,则说明数独无效;如果遍历完整个数组都没有发现重复数字,则数独有效。
def isValidSudoku(board):
rows = [{} for _ in range(9)]
cols = [{} for _ in range(9)]
boxes = [{} for _ in range(9)]
for i in range(9):
for j in range(9):
num = board[i][j]
if num != '.':
num = int(num)
box_index = (i // 3) * 3 + j // 3
if num in rows[i] or num in cols[j] or num in boxes[box_index]:
return False
rows[i][num] = 1
cols[j][num] = 1
boxes[box_index][num] = 1
return True
# 测试
board = [
["5", "3", ".", ".", "7", ".", ".", ".", "."],
["6", ".", ".", "1", "9", "5", ".", ".", "."],
[".", "9", "8", ".", ".", ".", ".", "6", "."],
["8", ".", ".", ".", "6", ".", ".", ".", "3"],
["4", ".", ".", "8", ".", "3", ".", ".", "1"],
["7", ".", ".", ".", "2", ".", ".", ".", "6"],
[".", "6", ".", ".", ".", ".", "2", "8", "."],
[".", ".", ".", "4", "1", "9", ".", ".", "5"],
[".", ".", ".", ".", "8", ".", ".", "7", "9"]
]
print(isValidSudoku(board))
训练营任务
- 实现一个简单的哈希表类,包含插入、查找和删除操作。
- 给定一个字符串数组,使用哈希表找出其中出现次数最多的字符串。
- 设计一个哈希函数,用于将学生的学号映射到一个班级编号,要求尽量减少冲突。
QA 环节
- 问:哈希表和数组有什么区别?
答:数组是一种顺序存储的数据结构,通过下标直接访问元素,查找时间复杂度为 O (1),但插入和删除操作可能需要移动大量元素,时间复杂度较高。哈希表通过哈希函数将关键字映射到存储位置,查找、插入和删除的平均时间复杂度都可以达到 O (1),但可能会发生冲突,需要额外的处理。
- 问:如何选择合适的哈希函数?
答:选择哈希函数时,要考虑关键字的特点、哈希表的大小以及冲突处理方法等因素。一般来说,要选择易于计算、分布均匀的哈希函数。对于不同类型的数据,可能需要针对性地设计哈希函数。例如,对于整数可以使用除留余数法,对于字符串可以将字符的 ASCII 码累加后再进行处理。
- 问:哈希冲突会对哈希表的性能产生什么影响?
答:哈希冲突会导致哈希表的查找、插入和删除操作的时间复杂度增加。如果冲突严重,哈希表可能会退化为链表,时间复杂度变为 O (n),从而降低哈希表的性能。因此,需要选择合适的哈希函数和冲突解决方法来减少冲突的影响。