哈希表√ 嘻哈×🕺🏿
哈希表
本文是面试经典 150 题哈希表问题的详细解读。 LeetCode 上有很多哈希表问题,这里简单介绍一下哈希表和一些常见的哈希表问题。
哈希表是一种基于哈希函数实现的数据结构,可以用于高效地存储和查找键值对。它将键通过哈希函数映射到一个桶中,然后在桶中存储对应的值。
哈希表的实现原理是将键映射为一个整数,这个整数称为哈希值。哈希值通常是一个较小的整数,通常是一个非负整数。然后将哈希值映射到一个桶中,桶是一个数据结构,可以存储一个或多个键值对。
哈希函数是哈希表的核心,它将键映射为哈希值。好的哈希函数应该具有良好的散列性质,即不同的键应该被映射到不同的哈希值,而相同的键应该被映射到同一个哈希值。如果哈希函数不好,可能会导致键映射到同一个桶中,这就会影响哈希表的性能。
当我们要在哈希表中查找一个键时,先将键通过哈希函数计算出哈希值,然后根据哈希值找到对应的桶,然后在桶中查找对应的值。由于哈希函数具有良好的散列性质,我们可以期望在平均情况下,查找键的时间复杂度为 O(1)。
在实际应用中,哈希表常常用于高效地存储和查找大量的数据,比如搜索引擎的索引、数据库的索引等。哈希表也常常用于解决一些算法问题,比如 LeetCode 上的哈希表问题。
Python 中提供了几个常用的哈希表实现库,包括:
-
dict:Python 内置的字典类型,基于哈希表实现。可以用来存储键值对,支持常数时间的插入、删除和查找操作。
-
collections.defaultdict:基于 dict 类型实现的一个字典子类,可以自动给不存在的键设置默认值。比如,当我们访问一个不存在的键时,可以指定默认值为 0,这样就可以避免 KeyError 异常。以下是一个示例:
from collections import defaultdict my_dict = defaultdict(int) my_dict['apple'] += 1 my_dict['banana'] += 2 print(my_dict) # 输出 defaultdict(<class 'int'>, {'apple': 1, 'banana': 2}) -
collections.Counter:也是基于 dict 类型实现的一个字典子类,用于统计频率。它可以自动对键进行计数,支持常数时间的更新和查找操作。以下是一个示例:
from collections import Counter my_list = ['apple', 'banana', 'apple', 'orange', 'banana', 'banana'] my_counter = Counter(my_list) print(my_counter) # 输出 Counter({'banana': 3, 'apple': 2, 'orange': 1}) -
hashlib:Python 内置的哈希库,提供了多种哈希算法的实现,比如 MD5、SHA1、SHA256 等。可以用于对字符串、文件等进行哈希计算。以下是一个示例:
import hashlib my_str = 'hello, world!' my_hash = hashlib.md5(my_str.encode()).hexdigest() print(my_hash) # 输出 3e25960a79dbc69b674cd4ec67a72c62
除了这些库之外,Python 还有许多其他的哈希表实现库,比如 Redis、Memcached 等,它们可以用于分布式缓存、数据存储等场景。
除了自带的哈希表,python还有另一个黑科技,zip():
zip() 函数是 Python 内置的一个函数,它可以将多个可迭代对象打包成一个元组序列,然后返回这个序列。打包后的序列的长度由最短的可迭代对象决定。
zip() 函数的语法如下:
pythonCopy code
zip(iterable1, iterable2, iterable3, ...)
其中,iterable1、iterable2、iterable3 等都是可迭代对象,可以是列表、元组、字典、集合等。
下面是一个简单的示例,演示了如何使用 zip() 函数打包两个列表:
pythonCopy code
a = [1, 2, 3]
b = [4, 5, 6]
result = zip(a, b)
print(list(result)) # 输出 [(1, 4), (2, 5), (3, 6)]
在这个例子中,我们将两个列表 a 和 b 打包成了一个元组序列,然后将序列转换为列表并输出。
如果要解包打包后的元组序列,可以使用 zip() 函数的反函数 zip(*),如下所示:
pythonCopy code
a = [1, 2, 3]
b = [4, 5, 6]
zipped = zip(a, b)
unzipped = zip(*zipped)
print(list(unzipped)) # 输出 [(1, 2, 3), (4, 5, 6)]
在这个例子中,我们将两个列表 a 和 b 打包成了一个元组序列,然后使用 zip(*zipped) 反函数将其解包。反函数 zip(*) 的作用是将多个可迭代对象解包成多个元组,然后将这些元组打包成一个元组序列返回。这个例子中,我们使用了两次 zip() 函数,第一次是打包,第二次是解包,结果是原来的两个列表重新组合成了一个元组序列。
好了,python写就是调库,剩下的你只需要判断出这个题用的是哈希表就行了。如果你不嫌长,很多题目都可以直接一行代码AC😄。
383. 赎金信 - 力扣(Leetcode)
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入: ransomNote = "a", magazine = "b" 输出: false
示例 2:
输入: ransomNote = "aa", magazine = "ab" 输出: false
示例 3:
输入: ransomNote = "aa", magazine = "aab" 输出: true
提示:
1 <= ransomNote.length, magazine.length <= 105ransomNote和magazine由小写英文字母组成
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
r = Counter(ransomNote)
m = Counter(magazine)
return not r-m
代码中用到了 Python 的 Counter 类,它可以用来统计可迭代对象中各个元素的数量,返回一个字典,其中键为元素,值为元素的数量。例如,Counter('hello') 返回的是 {'h': 1, 'e': 1, 'l': 2, 'o': 1}。
具体来说,代码中首先通过 Counter 函数分别统计了字符串 ransomNote 和 magazine 中各个字符的数量,得到两个字典 r 和 m。然后使用集合运算符 - 来计算 r 减去 m,如果结果是一个空字典,说明 magazine 中的字符可以组成 ransomNote,返回 True;否则返回 False。
值得注意的是,集合运算符 - 在这里的作用是计算两个字典的差集,返回的是包含所有在 r 中但不在 m 中的键值对的字典。因此,如果 r-m 返回的是一个空字典,说明 r 中的所有键值对都在 m 中出现过,即 magazine 中的字符可以组成 ransomNote。
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
return not Counter(ransomNote)-Counter(magazine)
242. 有效的字母异位词 - 力扣(Leetcode)
这题和上一题异曲同工,不多做解释。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
return Counter(s) == Counter(t)
205. 同构字符串 - 力扣(Leetcode)
给定两个字符串 s 和 t ,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
示例 1:
输入: s = "egg", t = "add" 输出: true
示例 2:
输入: s = "foo", t = "bar" 输出: false
示例 3:
输入: s = "paper", t = "title" 输出: true
提示:
1 <= s.length <= 5 * 104t.length == s.lengths和t由任意有效的 ASCII 字符组成
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
s2t, t2s = {}, {}
for a, b in zip(s, t):
# 对于已有映射 a -> s2t[a],若和当前字符映射 a -> b 不匹配,
# 说明有一对多的映射关系,则返回 false ;
# 对于映射 b -> a 也同理
if a in s2t and s2t[a] != b or b in t2s and t2s[b] != a:
return False
s2t[a], t2s[b] = b, a
return True
这里是用两个字典实现双向绑定,该算法的时间复杂度为 O(n),其中 n 表示字符串 s 和 t 的长度。具体来说,需要遍历两个字符串并建立映射关系,每个字符最多只会遍历一次。同时,由于使用了两个哈希表,空间复杂度也为 O(n)。
算法的实现思路如下:
-
首先创建两个空的字典
s2t和t2s,分别用于记录s中的字符到t中字符的映射关系和t中的字符到s中字符的映射关系; -
然后使用
zip函数将字符串s和t中的字符一一打包,然后在循环中访问每个元组中的元素; -
对于每个字符,我们首先判断它是否已经在
s2t中出现过,并且s2t[x]是否等于y,或者是否已经在t2s中出现过,并且t2s[y]是否等于x。如果满足这些条件,则说明两个字符串不同构,我们直接返回 False; -
否则,我们就在
s2t和t2s中分别添加新的映射关系; -
最后,如果循环结束,仍然没有返回 False,则说明两个字符串同构,我们返回 True。
290. 单词规律 - 力扣(Leetcode)
给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 s ****中的每个非空单词之间存在着双向连接的对应规律。
示例1:
输入: pattern = "abba", s = "dog cat cat dog" 输出: true
示例 2:
输入: pattern = "abba", s = "dog cat cat fish" 输出: false
示例 3:
输入: pattern = "aaaa", s = "dog cat cat dog" 输出: false
提示:
1 <= pattern.length <= 300pattern只包含小写英文字母1 <= s.length <= 3000s只包含小写英文字母和' 's不包含 任何前导或尾随对空格s中每个单词都被 单个空格 分隔
class Solution:
def wordPattern(self, pattern: str, s: str) -> bool:
s = s.split(' ')
if len(s) != len(pattern):
return False
mapping = list(set([*zip(pattern, s)]))
for i in range(len(mapping)):
for j in range(i+1, len(mapping)):
if mapping[i][0] == mapping[j][0] or mapping[i][1] == mapping[j][1]:
return False
return True
这段代码是用来判断一个字符串 s 是否符合给定的字符串模式 pattern 的。函数名为 wordPattern,它接收两个参数 pattern 和 s,都是字符串类型的,分别表示模式和待匹配的字符串。函数返回一个布尔值,如果 s 符合模式 pattern,则返回 True,否则返回 False。
在函数中,首先使用字符串的 split() 方法将字符串 s 拆分成一个单词列表,存储在变量 s 中。然后,判断单词列表的长度是否与模式串的长度相同,如果不同,则直接返回 False。
接下来,我们需要构建一个映射表 mapping,用于记录模式串中每个字符与对应单词之间的映射关系。由于一个字符可能对应多个不同的单词,因此我们使用列表 mapping 来存储所有可能的映射关系。为了避免重复的映射关系,我们先将 pattern 和 s 中的字符和单词进行一一配对,然后使用 set 去重,最终得到一个不重复的映射关系列表 mapping。
最后,我们需要检查 mapping 中的映射关系是否满足要求,即每个字符和单词之间都是一一对应的。我们可以使用两层循环来遍历 mapping 列表中的所有映射关系,对于任意两个映射关系 (c1, w1) 和 (c2, w2),如果两个字符或两个单词相同,则说明不符合要求,直接返回 False。否则,继续检查下一对映射关系。
如果循环中没有返回 False,则说明所有的映射关系都符合要求,函数返回 True。
这里是用了一个散列表,该算法的时间复杂度是 O(n2)O(n^2)O(n2),其中 n 表示模式串 pattern 和待匹配的字符串 s 的长度。
当然也可以改成和上一题一样的写法
class Solution:
def wordPattern(self, pattern: str, s: str) -> bool:
s = s.split(' ')
if len(s) != len(pattern):
return False
p2s,s2p={},{}
for x,y in zip(pattern,s):
if x in p2s and p2s[x] != y or y in s2p and s2p[y] != x:
return False
p2s[x] = y
s2p[y] = x
return True
该算法的时间复杂度为 O(n),其中 n 表示字符串 s 的长度。具体来说,需要将字符串 s 拆分成单词,然后遍历单词和模式串中的字符并建立映射关系,每个单词和字符最多只会遍历一次。同时,由于使用了两个哈希表,空间复杂度也为 O(n)。
这里多了一个判断s和pattern长度是否相等的代码,因为存在这样样例:

49. 字母异位词分组 - 力扣(Leetcode)
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""] 输出: [[""]]
示例 3:
输入: strs = ["a"] 输出: [["a"]]
提示:
1 <= strs.length <= 1040 <= strs[i].length <= 100strs[i]仅包含小写字母
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
mapping = defaultdict(list)
for s in strs:
key = "".join(sorted(s))
mapping[key].append(s)
print(mapping)
return mapping.values()
算法通过使用Python内置的defaultdict数据结构创建一个名为mapping的字典。对于每个输入字符串s,算法通过将其字母按照升序排序来创建一个唯一的键key。然后,将原始字符串s添加到具有相同键的列表中。最后,将字典中的值转换为列表并返回。
时间复杂度:假设有N个输入字符串,每个字符串的长度为K,排序一个长度为K的字符串需要O(KlogK)的时间。因此,总时间复杂度为O(NKlogK)。
空间复杂度:算法使用了一个defaultdict来存储输入字符串,该数据结构需要O(NK)的空间,用于存储所有的输入字符串和对应的键。
1. 两数之和 - 力扣(Leetcode)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入: nums = [2,7,11,15], target = 9 输出: [0,1] 解释: 因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入: nums = [3,2,4], target = 6 输出: [1,2]
示例 3:
输入: nums = [3,3], target = 6 输出: [0,1]
提示:
2 <= nums.length <= 104-109 <= nums[i] <= 109-109 <= target <= 109- 只会存在一个有效答案
进阶: 你可以想出一个时间复杂度小于 O(n2) 的算法吗?
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
d = defaultdict()
for i,n in enumerate(nums):
if target - n in d.keys():
return [d[target-n],i]
d[n] = i
return [-1,-1]
该方法使用了 Python 内置的 defaultdict 数据结构来记录 nums 中每个数字的索引。然后,它遍历输入列表 nums 中的每个数字,并检查 target - n 是否已经在 defaultdict 中。如果 target - n 存在于 defaultdict 中,则返回包含对应索引的列表。否则,将数字 n 和它的索引 i 存储到 defaultdict 中,以备后续使用。
如果在遍历完整个 nums 后都没有找到符合条件的两个数字,该方法将返回 [-1, -1]。
该算法的时间复杂度为 O(n),其中 n 是输入列表 nums 的长度。该算法遍历了一次输入列表,每次循环的时间复杂度为 O(1),因为 defaultdict 中的查找和插入操作的时间复杂度均为 O(1)。
空间复杂度为 O(n),因为最坏情况下,defaultdict 中需要存储输入列表 nums 中的每个数字及其索引,即需要 O(n) 的空间。
# 借助Python特性硬着头皮判断
# 时间复杂度O(n^2),空间复杂度O(1)
for i in range(len(nums)):
if target - nums[i] in nums:
# 看看相减之后是不是也在列表中
if i != nums.index(target - nums[i]):
# 排除重复使用同一个数的情况
return [i,nums.index(target - nums[i])]
return [-1,-1]
这样写和暴力法没区别啊, 该算法使用了两重循环。外层循环 for i in range(len(nums)) 遍历列表中的每个数字,内层循环 if target - nums[i] in nums: 检查是否存在另一个数字与当前数字相加等于目标值 target,并且 if i != nums.index(target - nums[i]) 检查该数字不是当前数字本身。因为内层循环中使用了 in 和 .index() 方法来查找数字,这样做的时间复杂度为 O(n),因此总时间复杂度为 O(n2)O(n^2)O(n2)。
219. 存在重复元素 II - 力扣(Leetcode)
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 **i 和 **j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。
示例 1:
输入: nums = [1,2,3,1], k = 3 输出: true
示例 2:
输入: nums = [1,0,1,1], k = 1 输出: true
示例 3:
输入: nums = [1,2,3,1,2,3], k = 2 输出: false
提示:
1 <= nums.length <= 105-109 <= nums[i] <= 1090 <= k <= 105
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
d = dict.fromkeys(nums)
for i,n in enumerate(nums):
if d[n] != None and i - d[n] <= k:
return True
d[n] = i
return False
算法思路:
-
创建一个字典
d,将nums数组中的元素作为键,初始值都为None。 -
遍历
nums数组,对于每个元素n,如果d[n]不为None且i - d[n] <= k,则返回True。 -
更新
d[n]为当前下标i。 -
如果整个数组遍历结束后都没有返回
True,则返回False。
时间复杂度:O(n)O(n)O(n),其中 n 是数组 nums 的长度。
空间复杂度:O(n)O(n)O(n),其中 n 是数组 nums 的长度,最坏情况下所有元素都需要存入字典 d 中。
128. 最长连续序列 - 力扣(Leetcode)
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)的算法解决此问题。
示例 1:
输入: nums = [100,4,200,1,3,2] 输出: 4 解释: 最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入: nums = [0,3,7,2,5,8,4,6,0,1] 输出: 9
提示:
0 <= nums.length <= 105-109 <= nums[i] <= 109
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
maxLen = 0
nums = set(nums)
for n in nums:
if n-1 not in nums:
cur = n
length = 1
while cur+1 in nums:
cur += 1
length += 1
maxLen = max(maxLen,length)
return maxLen
这段代码实现了在给定列表 nums 中查找最长的连续子序列的长度。算法的基本思想是利用集合 (set) 的快速查找特性,逐个遍历 nums 中的每个数字,检查它是否是一个序列的起点,如果是,则向后搜索该序列,直到找到序列的末尾。在这个过程中,记录序列的长度并更新最长的长度值。最终返回最长的连续序列的长度。
具体来说,该算法分为以下步骤:
-
初始化最长连续序列长度为 0,并将 nums 转换为集合,以便快速查找数字是否在 nums 中出现过。
-
对 nums 中的每个数字 n 进行循环遍历,判断 n 是否是某个连续序列的起点。
- 如果 n-1 不在 nums 集合中,说明 n 是一个新序列的起点,此时需要在集合中向后查找,找到该序列的末尾,统计序列的长度,并更新最长连续序列长度。
- 如果 n-1 在 nums 集合中,说明 n 不是一个新序列的起点,可以直接跳过该数字继续循环遍历 nums。
-
返回最长连续序列的长度。
这个算法的时间复杂度为 O(n),其中 n 是输入列表中数字的数量。因为该算法在遍历集合中每个数字时,最多需要向后搜索整个连续子序列,而且每个数字只会被遍历一次,因此算法的时间复杂度是线性的。
转载自:https://juejin.cn/post/7223716173332971577