1. 哈希查找
本质上就是个搜索,但是可以将在一个集合中查找一个元素的时间复杂度降低到O(1)。python中常用的有以下方式:
- set
- dict
- 数组模拟
2. 相关算法题
2.1. Leetcode 771 宝石与石头
- 题目链接
- 题目描述
给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。
字母区分大小写,因此 “a” 和 “A” 是不同类型的石头。
示例1
输入:jewels = “aA”, stones = “aAAbbbb”
输出:3
示例 2:
输入:jewels = “z”, stones = “ZZ”
输出:0
提示:
- 1 <= jewels.length, stones.length <= 50
- jewels 和 stones 仅由英文字母组成
- jewels 中的所有字符都是 唯一的
- 思路解析
- 可以尝试使用set,因为set的底层是哈希表,所以查找的时间复杂度是O(1)。
- 也可以用list构建一个虚拟的哈希表,然后查找。
class Solution:
# 利用set的哈希特性
def numJewelsInStones1(self, jewels: str, stones: str) -> int:
jewels = set(jewels)
count = 0
for i in range(len(stones)):
if stones[i] in jewels:
count += 1
return count
# 利用数组代替hash表
def numJewelsInStones(self,jewels:str,stones:str) -> int:
count = 0
counts = [0 for i in range(58)] # 虚拟哈希表
for c in jewels:
counts[ord(c) - ord('A')] = 1
for c in stones:
count += counts[ord(c) - ord('A')]
return count
2.2. Leetcode 128 最长连续序列
- 题目链接
- 题目描述
给定一个未排序的整数数组 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 <= 10^5
- -10^9 <= nums[i] <= 10^9
- 思路解析
有两种思路:
- 先排序,然后遍历一遍,找到最长的连续序列。因为要求为O(n),所以只能使用计数+基数排序的方式,时间复杂度为O(n)。
- 哈希表,先将所有的数字存入哈希表,然后遍历一遍,找到最长的连续序列。
class Solution:
def counter_sort(self,nums,n:int,base:int=0):
tmp_res = [[] for _ in range(10)]
for num in nums:
index = num//(10**base)%10
tmp_res[index].append(num)
res = []
for tr in tmp_res:
res.extend(tr)
return res
def base_sort(self,nums:List[int]):
if len(nums) == 0: return []
max_num = max(nums)
num_length = len(str(max_num))
for i in range(num_length):
nums = self.counter_sort(nums,num_length,i)
return nums
def longestConsecutive(self, nums: List[int]) -> int:
"""
基数排序 + 计数排序
"""
if len(nums) == 0:return 0
if len(nums) == 1:return 1
nums1 = [num for num in nums if num >= 0]
nums2 = [- num for num in nums if num < 0]
nums1 = self.base_sort(nums1) if len(nums1) > 0 else nums1
nums2 = self.base_sort(nums2) if len(nums2) > 0 else nums2
nums2 = [-num for num in nums2]
nums2.reverse()
nums2.extend(nums1)
nums = nums2
output = []
setnums = set(nums)
for item in nums:
if item not in setnums:
output.append(item)
output = []
i = 0
while(i<len(nums)):
while i<len(nums)-1 and nums[i+1] - nums[i] ==0:
i += 1
output.append(nums[i])
i += 1
nums = output
l = 0
r = 0
lengths = [0]
while(r<len(nums) and l<=r):
while(r<len(nums)-1 and nums[r+1]-nums[r] == 1):
r += 1
lengths.append(r-l+1)
r += 1
l = r
return max(lengths)
哈希表思路,先将nums存放到一个set中,因为判断元素是否在set中的时间复杂度为O(1),之后只需要针对每个元素,判断这个元素之后的元素是否连续就可以了。
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
nums_set = set(nums)
n = len(nums)
if n<2:return n
i = 0
max_num = 1
while(i<n):
if nums[i] - 1 in nums_set:
i += 1
continue
tmp_num = 1
j = i
while(j<n and (nums[j]+tmp_num) in nums_set):
tmp_num += 1
max_num = tmp_num if tmp_num > max_num else max_num
i += 1
return max_num
2.2.1. Leetcode 389 找不同
- 题目链接
- 题目描述
给定两个字符串 s 和 t ,它们只包含小写字母。
字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。
请找出在 t 中被添加的字母。
示例 1:
输入:s = "abcd", t = "abcde"
输出:"e"
解释:'e' 是那个被添加的字母。
示例 2:
输入:s = "", t = "y"
输出:"y"
提示:
0 <= s.length <= 1000
t.length == s.length + 1
s 和 t 只包含小写字母
- 思路解析
hash map,在python中其实就是字典
class Solution:
def findTheDifference(self, s: str, t: str) -> str:
hash_map = {}
for s_ in s:
if s_ not in hash_map:
hash_map[s_] = 1
else:
hash_map[s_] += 1
for t_ in t:
if t_ not in hash_map:
return t_
else:
hash_map[t_] -= 1
for k in hash_map:
if hash_map[k] < 0:
return k
2.3. leetcode 554 砖墙
- 题目链接
- 题目描述
你的面前有一堵矩形的、由 n 行砖块组成的砖墙。这些砖块高度相同(也就是一个单位高)但是宽度不同。每一行砖块的宽度之和相等。
你现在要画一条 自顶向下 的、穿过 最少 砖块的垂线。如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。
给你一个二维数组 wall ,该数组包含这堵墙的相关信息。其中,wall[i] 是一个代表从左至右每块砖的宽度的数组。你需要找出怎样画才能使这条线 穿过的砖块数量最少 ,并且返回 穿过的砖块数量 。
- 示例1
输入:wall = [[1,2,2,1],[3,1,2],[1,3,2],[2,4],[3,1,2],[1,3,1,1]]
输出:2
- 示例2
输入:wall = [[1],[1],[1]]
输出:3
- 提示
n == wall.length
1 <= n <= 104
1 <= wall[i].length <= 104
1 <= sum(wall[i].length) <= 2 * 104
对于每一行 i ,sum(wall[i]) 是相同的
1 <= wall[i][j] <= 231 - 1
- 思路解析
其实就是找到每一行的缝隙,然后找到最多的缝隙,然后用总行数减去最多的缝隙就是最少的穿过的砖块数量。
需要注意的一点,每行的最后一个缝隙一定是最右侧边界,它是不能算的,因为题目要求不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。
class Solution:
def leastBricks(self, wall: List[List[int]]) -> int:
n = len(wall)
hash_map = {}
for i in range(n):
sum_ = 0
for j in range(len(wall[i])-1): # 最后一块砖不算,如果算了此时的sum_一定是最右侧的边界,不符合题意
sum_ += wall[i][j]
if sum_ not in hash_map:
hash_map[sum_] = 1
else:
hash_map[sum_] += 1
return len(wall) - (max(hash_map.values()) if len(hash_map) > 0 else 0)
2.4. leetcode 205 同构字符串
- 题目链接
- 题目描述
给定两个字符串 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 * 104
t.length == s.length
s 和 t 由任意有效的 ASCII 字符组成
- 思路解析
其实就是一个哈希表的问题,只要保证s中的字符和t中的字符一一对应就可以了。为了保证正反的映射,我们可以用两个哈希表,一个记录s到t的映射,一个记录t到s的映射。
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
maps_s2t = {}
maps_t2s = {}
for s_char, t_char in zip(s, t):
if (
maps_s2t.get(s_char, t_char) != t_char
or maps_t2s.get(t_char, s_char) != s_char
):
return False
maps_s2t[s_char],maps_t2s[t_char] = t_char,s_char
return True
2.5. Leetcode 290 单词规律
- 题目链接
- 题目描述
给定一种规律 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 <= 300
pattern 只包含小写英文字母
1 <= s.length <= 3000
s 只包含小写英文字母和 ' '
s 不包含 任何前导或尾随对空格
s 中每个单词都被 单个空格 分隔
- 思路解析
这个的思路和205题一样,也是一个哈希表的问题,只要保证pattern中的字符和s中的单词一一对应就可以了。为了保证正反的映射,我们可以用两个哈希表,一个记录pattern到s的映射,一个记录s到pattern的映射。
class Solution:
def wordPattern(self, pattern: str, s: str) -> bool:
p2s, s2p = {}, {}
if len(pattern) != len(s.split(' ')):return False
for p, s_word in zip(pattern, s.split(" ")):
if p2s.get(p, s_word) != s_word or s2p.get(s_word, p) != p:
return False
p2s[p], s2p[s_word] = s_word, p
return True
2.6. Leetcode 242 有效的字母异位词
- 题目链接
- 题目描述
有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
提示:
1 <= s.length, t.length <= 5 * 104
s 和 t 仅包含小写字母
进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
- 思路解析
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
hash_map = {}
for s_char in s:
if s_char not in hash_map:
hash_map[s_char] = 1
else:
hash_map[s_char] += 1
for t_char in t:
if t_char not in hash_map:
return False
else:
hash_map[t_char] -= 1
for k,v in hash_map.items():
if v>0 or v<0:
return False
return True
2.7. Leetcode 49 字母异位词分组
- 题目链接
- 题目描述
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
提示:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母
Solution
- 思路解析
可以用排序后的字符串作为key,原字符串作为value,存入哈希表中,最后返回哈希表的value即可。
class Solution:
def sort_strs(self,s:str):
return ''.join(sorted(s))
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
res = {}
for s in strs:
sorted_s = self.sort_strs(s)
if sorted_s not in res:
res[sorted_s] = [s]
else:
res[sorted_s].append(s)
res_list = []
for s_list in res.values():
res_list.append(s_list)
return res_list
另一种方式,将每个字母的数量作为key,原字符串作为value,存入哈希表中,最后返回哈希表的value即可。
class Solution:
def sort_strs(self,s:str):
return ''.join(sorted(s))
def countmap_strs(self,s:str):
res = [0]*26
for s_char in s:
res[ord(s_char)-ord('a')] += 1
return str(res)
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
res = {}
for s in strs:
sorted_s = self.countmap_strs(s)
if sorted_s not in res:
res[sorted_s] = [s]
else:
res[sorted_s].append(s)
res_list = []
for s_list in res.values():
res_list.append(s_list)
return res_list
2.8. Leetcode 560 和为K的子数组
- 题目链接
- 题目描述
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数 。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
- 思路解析
前缀和+线性查找是我们最容易想到的方法,但是时间复杂度为O(n^2),不符合题意。
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
n = len(nums)
sums = [0]*(n+1)
for i in range(1,n+1):
sums[i] = sums[i-1] + nums[i-1]
count = 0
for i in range(1,n+1):
for j in range(i,n+1):
if sums[j] - sums[i-1] == k:
count += 1
return count
我们可以用哈希表来优化,将前缀和作为key,出现的次数作为value,这样我们就可以在O(1)的时间复杂度内找到前缀和为k的子数组的个数。
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
n = len(nums)
sums = [0]*(n+1)
for i in range(1,n+1):
sums[i] = sums[i-1] + nums[i-1]
count = 0
hash_map = {}
for i in range(1,n+1):
if sums[i] - k in hash_map:
count += hash_map[sums[i] - k]
if sums[i] not in hash_map:
hash_map[sums[i]] = 1
else:
hash_map[sums[i]] += 1
return count
内存还可以优化,我们可以用一个变量来记录前缀和为k的子数组的个数,这样就不需要哈希表了。
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
n = len(nums)
if n == 1:
if nums[0] == k:return 1
else: return 0
res = 0
map = {}
map[0] = 1
sum_ = 0
for i in range(n):
sum_ += nums[i]
diff = sum_-k # 当前和为sum,则如果存在前缀和为sum-k的,那么就有map[sum-k]个子数组的和为k
if diff in map:
res += map[diff] # 如果存在前缀和为diff的,那么就有map[diff]个子数组的和为k
map[sum_] = map.get(sum_,0) + 1 # 前缀和为sum的个数+1
return res
2.9. Leetcode 41 缺失的第一个正数
- 题目链接
- 题目描述
缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
提示:
1 <= nums.length <= 5 * 105
-231 <= nums[i] <= 231 - 1
Solution
- 思路解析
我们可以用哈希表来记录每个数字是否出现过,然后从1开始遍历,找到第一个没有出现过的数字即可。
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums) + 1
nums = set(nums)
for i in range(1,n):
if i not in nums:
return i
return n
但是这样并不满足进阶要求。如何改进呢,我们重新审视题干,可以发现我们要求查找的是正整数,对于正整数我们可以用下标的信息来存储。这样的话就不会有额外空间,我们来验证一下这个思路的可行性。
nums中的数字分为以下几类
- 小于等于0的,这部分不用考虑,因为题干中要求找到的是正整数,为了让数据一致,我们可以把这部分值变成大于n的值。
- 大于0小于等于n的,这部分可以用下标表示,为了不冲掉原始信息,我们可以将numsi看作下标,然后将nums[nums[i]-1]对应的值置为负数,表示nums[i]出现过;
- 大于n的,这部分无法用下标表示,而且如果出现大于n的值,一定表示在此之前已经有缺失的正整数,所以可以保持不变。
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n):
if nums[i] <=0:
nums[i] = n+1
for i in range(n):
# nums[i]作为下标
subscript = abs(nums[i])
if subscript <= n:
nums[subscript-1] = -abs(nums[subscript-1]) # 为什么不能直接-nums[subscript-1]?因为一个值有可能出现多次,这时两次操作就有可能把值还原为正的,这样就无法有效过滤掉这个值。
for i in range(n):
if nums[i] > 0: return i+1
return n+1
2.10. Leetcode 1122 数组的相对排序
- 题目链接
- 题目描述
数组的相对排序
给你两个数组,arr1 和 arr2,arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。
对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。
示例 1:
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]
示例 2:
输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
输出:[22,28,8,6,17,44]
提示:
1 <= arr1.length, arr2.length <= 1000
0 <= arr1[i], arr2[i] <= 1000
arr2 中的元素 arr2[i] 各不相同
arr2 中的每个元素 arr2[i] 都出现在 arr1 中
- 思路解析
实际上,这就是相当于两个排序,我们把arr1中数据分为在和不在arr2的两个部分,对第一个部分,两个元素之间的大小比较规则是根据arr2中的相对顺序,对于第二个部分,进行正常的比较。所以,我们可以用任意一种排序算法来实现。
class Solution:
def __init__(self) -> None:
self.arr2_indexes = {}
def compare(self, a: int, b: int):
if self.arr2_indexes[a] < self.arr2_indexes[b]: # 小于
return -1
elif self.arr2_indexes[a] == self.arr2_indexes[b]: # 等于
return 0
else:
return 1 # 大于
def mergesort(
self, nums: List[int], low: int, high: int, temp: List[int], prefix: bool
):
n = len(nums)
if low >= high:
return
mid = low + (high - low) // 2
self.mergesort(nums, low, mid, temp, prefix)
self.mergesort(nums, mid + 1, high, temp, prefix)
for i in range(low, high + 1):
temp[i] = nums[i]
i = low
j = mid + 1
for k in range(low, high + 1):
if i > mid:
nums[k] = temp[j]
j += 1
elif j > high:
nums[k] = temp[i]
i += 1
elif (
(self.compare(temp[i], temp[j]) in [-1, 0])
if prefix
else (temp[i] <= temp[j])
):
nums[k] = temp[i]
i += 1
else:
nums[k] = temp[j]
j += 1
def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
for i in range(len(arr2)):
self.arr2_indexes[arr2[i]] = i
arr2_set = set(arr2)
prefix = []
suffix = []
for item in arr1:
if item in arr2_set:
prefix.append(item)
else:
suffix.append(item)
self.mergesort(prefix, 0, len(prefix) - 1, [0] * len(prefix), True)
self.mergesort(suffix, 0, len(suffix) - 1, [0] * len(suffix), False)
prefix.extend(suffix)
return prefix
另外一种思路,我们可以把arr1中的元素都用字典存储起来其数量,然后遍历arr2中的元素,根据元素匹配形成第一步的res,之后我们只需要找到不在arr2中的元素放到后面即可。为了优化这个速度,我们可以直接遍历(0,max(arr1)),具体代码如下
class Solution:
def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
max_num = max(arr1)
hashmap = {}
for i in range(len(arr1)):
hashmap[arr1[i]] = hashmap.get(arr1[i],0) + 1
pre = []
for c in arr2:
if c in hashmap:
pre.extend([c]*hashmap[c])
hashmap[c] = 0
for i in range(max_num+1):
if i in hashmap and hashmap[i] > 0:
pre.extend([i]*hashmap[i])
return pre
其中,我们从hashmap中取值的时候,需要将对应的值置为0,这样可以避免重复取值。