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. 设计哈希集合
706. 设计哈希映射
136. 只出现一次的数字
137. 只出现一次的数字 II
260. 只出现一次的数字 III
217. 存在重复元素
219. 存在重复元素 II
220. 存在重复元素 III
349. 两个数组的交集
350. 两个数组的交集 II
36. 有效的数独
389. 找不同
496. 下一个更大元素 I
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