1 哈希表
散列表(hash table),又名‘hash表’,它用的是数组支持按照下标随机访问数据(时间复杂度O(1))的特性,所以散列表其实就是基于数组结构的一种扩展。简单的来说,就是把键值通过散列函数求得hash值之后,对数组容量进行取模运算,得到存放在数组位置的下标值,当我们按照键值查询元素时,我们用同样的方法将键值转化数组下标,从对应的数组下标的位置取数据。散列表这种数据结构虽然支持非常高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的。也就说,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,那我们需要将散列表中的数据拷贝到数组中,然后排序,再遍历。因为散列表是动态数据结构,不停地有数据的插入、删除,所以每当我们希望按顺序遍历散列表中的数据的时候,都需要先排序,那效率势必会很低。为了解决这个问题,我们常常会将散列表和链表(或者跳表)结合在一起使用。
1.1 什么是哈希表
散列表(Hash table,也叫哈希表),是根据关键码值(Key和value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
- 哈希函数(Hash Function):将哈希表中元素的关键键值映射为元素存储位置的函数。
- 哈希冲突(Hash Collision):不同的关键字通过同一个哈希函数可能得到同一哈希地址。
哈希表的两个核心问题是:「哈希函数的构建」 和 「哈希冲突的解决方法」。
常用的哈希函数方法有:直接定址法、除留余数法、平方取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。
常用的哈希冲突的解决方法有两种:开放地址法和链地址法。
有两种不同类型的哈希表:哈希集合和哈希映射。
- 哈希集合是集合数据结构的实现之一,用于存储非重复值。
- 哈希映射是映射数据结构的实现之一,用于存储(key, value)键值对。
在标准模板库的帮助下,哈希表是易于使用的。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能。
Python 将哈希表用于字典和集合。 哈希表是键值对的无序集合,其中每个键都是唯一的。 哈希表提供了有效的查找,插入和删除操作的组合。 这些是数组和链接列表的最佳属性。相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。
1.2 哈希表的原理
哈希表是一种数据结构,它使用哈希函数组织数据,以支持快速插入和搜索。哈希表的核心思想就是使用哈希函数将键映射到存储桶。更确切地说:
- 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
- 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
下面举一个简单的例子,我们来理解下:
在示例中,我们使用 y = x % 5 y = x % 5 y=x%5 作为哈希函数。让我们使用这个例子来完成插入和搜索策略:
- 插入:我们通过哈希函数解析键,将它们映射到相应的桶中。 例如,1987 分配给桶 2,而 24 分配给桶 4。
- 搜索:我们通过相同的哈希函数解析键,并仅在特定存储桶中搜索。 例如,如果我们搜索 23,将映射 23 到 3,并在桶 3 中搜索。我们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。
1.2 设计哈希函数
哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶。在之前的示例中,我们使用 y = x y = x % 5 y=x 作为散列函数,其中 x x x 是键值, y y y 是分配的桶的索引。
散列函数将取决于键值的范围和桶的数量。下面是一些哈希函数的示例:
哈希函数的设计是一个开放的问题。其思想是尽可能将键分配到桶中,理想情况下,完美的哈希函数将是键和桶之间的一对一映射。然而,在大多数情况下,哈希函数并不完美,它需要在桶的数量和桶的容量之间进行权衡。
当然,我们也可以自定义一些哈希函数。一般的方法有:
- 直接定制法。哈希函数为关键字到地址的线性函数。如, H ( k e y ) = a ∗ k e y + b H(key) = a * key + b H(key)=a∗key+b。这里,a 和 b 是设置好的常数。
- 数字分析法。假设关键字集合中的每个关键字 key 都是由 s 位数字组成(k1,k2,…,Ks),并从中提取分布均匀的若干位组成哈希地址。
- 平方取中法。如果关键字的每一位都有某些数字重复出现,并且频率很高,我们就可以先求关键字的平方值,通过平方扩大差异,然后取中间几位作为最终存储地址。
- 折叠法。如果关键字的位数很多,可以将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)作为哈希地址。
- 除留余数法。预先设置一个数 p,然后对关键字进行取余运算。即地址为 key % p。
1.3 解决哈希冲突
理想情况下,如果我们的哈希函数是完美的一对一映射,我们将不需要处理冲突。不幸的是,在大多数情况下,冲突几乎是不可避免的。例如,在我们之前的哈希函数(y = x % 5)中,1987 和 2 都分配给了桶 2,这就是一个哈希冲突。
解决哈希冲突应该要思考以下几个问题:
- 如何组织在同一个桶中的值?
- 如果为同一个桶分配了太多的值,该怎么办?
- 如何在特定的桶中搜索目标值?
那么一旦发生冲突,我们该如何解决呢?常用的方法有两种:开放定址法和链地址法。
1. 开放定址法
即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在哈希表中形成一个探测序列,然后沿着这个探测序列依次查找下去。当碰到一个空的单元时,则插入其中。
常用的探测方法是线性探测法。 比如有一组关键字 {12,13,25,23},采用的哈希函数为 key % 11。当插入 12,13,25 时可以直接插入,地址分别为 1、2、3。而当插入 23 时,哈希地址为 23 % 11 = 1。
然而,地址 1 已经被占用,因此沿着地址 1 依次往下探测,直到探测到地址 4,发现为空,则将 23 插入其中。如下图所示:
2. 链地址法
将哈希地址相同的记录存储在一张线性链表中。例如,有一组关键字 {12,13,25,23,38,84,6,91,34},采用的哈希函数为 key % 11。如下图所示:
1.4 哈希表的应用
1. 哈希表的基本操作
在很多高级语言中,哈希函数、哈希冲突都已经在底层完成了黑盒化处理,是不需要开发者自己设计的。也就是说,哈希表完成了关键字到地址的映射,可以在常数级时间复杂度内通过关键字查找到数据。
至于实现细节,比如用了哪个哈希函数,用了什么冲突处理,甚至某个数据记录的哈希地址是多少,都是不需要开发者关注的。接下来,我们从实际的开发角度,来看一下哈希表对数据的增删查操作。
哈希表中的增加和删除数据操作,不涉及增删后对数据的挪移问题(数组需要考虑),因此处理就可以了。
哈希表查找的细节过程是:对于给定的 key,通过哈希函数计算哈希地址 H (key)。
如果哈希地址对应的值为空,则查找不成功。反之,则查找成功。虽然哈希表查找的细节过程还比较麻烦,但因为一些高级语言的黑盒化处理,开发者并不需要实际去开发底层代码,只要调用相关的函数就可以了。
2. 哈希表的优缺点
优势:它可以提供非常快速的插入-删除-查找操作,无论多少数据,插入和删除值需要接近常量的时间。在查找方面,哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。
不足:哈希表中的数据是没有顺序概念的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。在数据处理顺序敏感的问题时,选择哈希表并不是个好的处理方法。同时,哈希表中的key 是不允许重复的,在重复性非常高的数据中,哈希表也不是个好的选择。
2 常见题型
2.1 题库列表
705. 设计哈希集合
题目描述
# 定义一个一维长度为 buckets 的二维数组 table。第一维度用于计算哈希函数,为 key 分桶。
# 第二个维度用于寻找 key 存放的具体位置。第二维度的数组会根据 key 值动态增长,模拟真正的链表。
class MyHashSet:
def __init__(self):
self.buckets = 1009
self.table = [[] for _ in range(self.buckets)]
def hash(self, key):
return key % self.buckets # 用取余数的方法实现集合
def add(self, key): # 向哈希集合中插入一个值
hashkey = self.hash(key)
if key in self.table[hashkey]:
return
self.table[hashkey].append(key)
def remove(self, key): # 将给定值从哈希集合中删除。如果哈希集合中没有这个值,什么也不做
hashkey = self.hash(key)
if key not in self.table[hashkey]:
return
self.table[hashkey].remove(key)
def contains(self, key): # 返回哈希集合中是否存在这个值
hashkey = self.hash(key)
return key in self.table[hashkey]
706. 设计哈希映射
题目描述
# 最简单的思路就是用模运算作为哈希方法,为了降低哈希碰撞的概率,通常取素数的模,例如 模 1009或2069
class MyHashMap:
def __init__(self):
self.buckets = 1009
self.table = [[] for _ in range(self.buckets)]
def hash(self, key): # 哈希映射关系
return key % self.buckets
def put(self, key: int, value: int) -> None: # 向哈希映射中插入(键,值)的数值对。如果键对应的值已经存在,更新这个值
hashkey = self.hash(key)
for item in self.table[hashkey]:
if item[0] == key:
item[1] = value
return
self.table[hashkey].append([key, value])
def get(self, key: int) -> int: # 返回给定的键所对应的值,如果映射中不包含这个键,返回-1
hashkey = self.hash(key)
for item in self.table[hashkey]:
if item[0] == key:
return item[1]
return -1
def remove(self, key: int) -> None: # 如果映射中存在这个键,删除这个数值对
hashkey = self.hash(key)
for i, item in enumerate(self.table[hashkey]):
if item[0] == key:
self.table[hashkey].pop(i)
return
136. 只出现一次的数字
题目描述:给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
class Solution:
def singleNumber(self, nums: List[int]) -> int:
#一、用字典统计元素个数
# hash_map = Counter(nums)
hash_map = {
}
#方法一
for i in nums:
hash_map[i] = hash_map.get(i, 0) + 1
#方法二
# for i in nums:
# if i not in hash_map:
# hash_map[i] = 1
# else:
# hash_map[i] += 1
#二、找出只出现一次的元素
#方法一
return list(hash_map.keys())[list(hash_map.values()).index(1)]
#方法二
# new_hash_map = {v:k for k,v in hash_map.items()}
# return new_hash_map[1]
#方法三
# return [key for key, value in hash_map.items() if value == 1]
# 或
# for key,value in hash_map.items():
# if value == 1:
# return key
137. 只出现一次的数字 II
题目描述:给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
class Solution:
def singleNumber(self, nums: List[int]) -> int:
hash_map = Counter(nums)
return min(hash_map.items(), key=lambda x:x[1])[0]
260. 只出现一次的数字 III
题目描述:给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
class Solution:
def singleNumber(self, nums: List[int]) -> List[int]:
hash_map = collections.Counter(nums)
res = []
for key, val in hash_map.items():
if val == 1:
res.append(key)
return res
217. 存在重复元素
题目描述:给你一个整数数组 nums。如果任一值在数组中出现 至少两次,返回 true;如果数组中每个元素互不相同,返回 false。
1. 哈希字典
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
hash_map = {
}
for num in nums:
if num in hash_map:
hash_map[num] += 1
else:
hash_map[num] = 1
for index in hash_map:
if hash_map[index] >= 2:
return True
return False
2. 哈希集合
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
hash_set = set()
for num in nums:
if num in hash_set:
return True
else:
hash_set.add(num)
return False
3. 集合的性质
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
# return sum(nums) != sum(set(nums)) or nums.count(0) > 1
return len(nums) != len(set(nums))
219. 存在重复元素 II
题目描述:给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 a b s ( i − j ) < = k abs(i - j) <= k abs(i−j)<=k。如果存在,返回 true;否则,返回 false 。
1. 哈希字典
# 找到重复元素和其索引
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
hash_map = {
}
for i in range(len(nums)):
# 已经存在重复的情况
if nums[i] in hash_map and abs(i - hash_map[nums[i]]) <= k:
return True
else:
hash_map[nums[i]] = i
return False
2. 哈希集合
# 维护一个长度为 k 的集合
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
hash_set = set()
for i in range(len(nums)):
# 存在重复元素
if nums[i] in hash_set:
return True
hash_set.add(nums[i])
# 及时删除超出数组长度的元素
if len(hash_set) > k:
hash_set.remove(nums[i - k])
return False
220. 存在重复元素 III
题目描述:给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 a b s ( n u m s [ i ] − n u m s [ j ] ) < = t abs(nums[i] - nums[j]) <= t abs(nums[i]−nums[j])<=t,同时又满足 a b s ( i − j ) < = k abs(i - j) <= k abs(i−j)<=k。如果存在则返回 true,不存在返回 false。
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
bucket_dict = dict()
for i in range(len(nums)):
# 将nums[i]划分到大小为 t + 1 的不同桶中,分桶操作
num = nums[i] // (t + 1)
# print(num)
# 如果桶中已经有元素,有相同的分桶结果,表示存在相同元素
if num in bucket_dict:
return True
# 将 nums[i] 放入桶中
bucket_dict[num] = nums[i]
# print(bucket_dict)
# 判断左侧桶是否满足条件
if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t:
return True
# 判断右侧桶是否满足条件
if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t:
return True
# 将 i - k 之前的旧桶清除,因为之前的桶已经不满足条件了
if i >= k:
bucket_dict.pop(nums[i-k] // (t + 1))
return False
349. 两个数组的交集
题目描述:给定两个数组 nums1 和 nums2,返回它们的交集。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序。
1. 集合的交集
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
l1 = set(nums1)
l2 = set(nums2)
return list(l1 & l2)
2. 哈希字典
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
hash_map = dict()
result = []
for num1 in nums1:
if num1 not in hash_map:
hash_map[num1] = 1 # 只做一次计数
for num2 in nums2:
if num2 in hash_map and hash_map[num2] != 0:
hash_map[num2] -= 1 # 及时对结果进行处理
result.append(num2)
return result
350. 两个数组的交集 II
题目描述:给你两个整数数组 nums1 和 nums2,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
hash_map = {
}
result = []
for num1 in nums1:
if num1 in hash_map:
hash_map[num1] += 1
else:
hash_map[num1] = 1
for num2 in nums2:
if num2 in hash_map and hash_map[num2] != 0:
hash_map[num2] -= 1
result.append(num2)
return result
36. 有效的数独
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
# 记录行数据、列数据、3x3格子数据,用于标记 1-9 共10个数字
rows = [[0 for _ in range(10)] for _ in range(10)]
columns = [[0 for _ in range(10)] for _ in range(10)]
boxes = [[0 for _ in range(10)] for _ in range(10)]
for i in range(9):
for j in range(9):
if board[i][j] != '.':
# 1-9数字是否重复出现
num = int(board[i][j])
board_index = (i // 3) * 3 + j // 3 # 方格角标的计算用 box[(i/3)*3+(j/3)][n] 来表示
if rows[i][num] > 0 or columns[j][num] > 0 or boxes[board_index][num] > 0:
return False
rows[i][num] = 1
columns[j][num] = 1
boxes[board_index][num] = 1
return True
389. 找不同
题目描述:给定两个字符串 s 和 t,它们只包含小写字母。字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。请找出在 t 中被添加的字母。
1. 哈希字典
class Solution:
def findTheDifference(self, s: str, t: str) -> str:
return list(Counter(t) - Counter(s))[0]
2. 异或运算
class Solution:
def findTheDifference(self, s: str, t: str) -> str:
return chr(reduce(xor, map(ord, s + t)))
496. 下一个更大元素 I
题目描述
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
hash_map = {
} # 字典存储结果
stack = [] # 列表模拟栈,单调栈
for i in range(len(nums2)): # 遍历每个字符串
if not stack: # 如果当前栈为空,则直接入栈
stack.append(nums2[i])
else:
if nums2[i] < stack[-1]: # 当前值小于栈顶元素,则入栈
stack.append(nums2[i])
elif nums2[i] > stack[-1]: # 当前值大于栈顶元素,不停出栈,把所有栈顶key值的value赋值为当前值
while stack and stack[-1] < nums2[i]:
hash_map[stack[-1]] = nums2[i]
stack.pop()
stack.append(nums2[i]) # 当前值入队列
while stack: # 如果栈中还有元素,则全部赋值为-1,表示右边没有更大值
hash_map[stack[-1]] = -1
stack.pop()
result = []
for i in nums1: # 返回每个key值对应的value
result.append(hash_map[i])
return result
哈希表暂时告一段落,后面在学习中持续补充,谢谢大家的鼓励和支持!
参考
- 哈希表:https://www.programmercarl.com/哈希表理论基础.html#哈希表
- Python数据结构-哈希表(Hash Table):https://www.jianshu.com/p/093346ca7f38
- 数据结构(Python实现)------ 哈希表:https://blog.csdn.net/Avery123123/article/details/103583115