哈希表算法总结
著名的空间换时间算法。
leetcode1 两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashmap = {}
for index,var in enumerate(nums):
hashmap[var] = index
for i,var in enumerate(nums):
j = hashmap.get(target-var)
if j and i != j:
return [i,j]
例题二:(hashmap中搜索字符串,重点考察编程能力)
leetcode30
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例 1:
输入:
s = “barfoothefoobarman”,
words = [“foo”,“bar”]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 “barfoo” 和 “foobar” 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:
s = “wordgoodgoodgoodbestword”,
words = [“word”,“good”,“best”,“word”]
输出:[]
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
if not s or not words:
return []
hashmap = {}
res = []
for i in words:
if i not in hashmap:
hashmap[i] =1
else:
hashmap[i] +=1
one_word = len(words[0])
all_words = one_word*len(words)
for i in range(len(s)-all_words+1):
#之所以要新建一个字典是为了防止源字典被破坏
temp ,d = s[i:i+all_words],dict(hashmap)
for j in range(0,len(temp),one_word):
key = temp[j:j+one_word]
if key in d:
d[key] -=1
if d[key] == 0:
del d[key]
else:
break
#保存首坐标
if not d:
res.append(i)
return res
例题三:
leetcode37 解数独(hard级别的题目确实综合能力要求很高)
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
回溯+hashmap的综合应用
from collections import defaultdict
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
:type board: List[List[str]]
:rtype: void Do not return anything, modify board in-place instead.
"""
#判断这个位置是否在hashmap中已经存在
def could_place(d, row, col):
"""
Check if one could place a number d in (row, col) cell
"""
return not (d in rows[row] or d in columns[col] or \
d in boxes[box_index(row, col)])
#放置数字,并且更新三个hashmap
def place_number(d, row, col):
"""
Place a number d in (row, col) cell
"""
rows[row][d] += 1
columns[col][d] += 1
boxes[box_index(row, col)][d] += 1
board[row][col] = str(d)
#移除数字和hashmap
def remove_number(d, row, col):
"""
Remove a number which didn't lead
to a solution
"""
del rows[row][d]
del columns[col][d]
del boxes[box_index(row, col)][d]
board[row][col] = '.'
#下一次的递归条件,判断是否回溯到表格末端
def place_next_numbers(row, col):
"""
Call backtrack function in recursion
to continue to place numbers
till the moment we have a solution
"""
# if we're in the last cell
# that means we have the solution
if col == N - 1 and row == N - 1:
nonlocal sudoku_solved
sudoku_solved = True
#if not yet
else:
# if we're in the end of the row
# go to the next row
if col == N - 1:
backtrack(row + 1, 0)
# go to the next column
else:
backtrack(row, col + 1)
#回溯,其实就是调用上面的函数
def backtrack(row = 0, col = 0):
"""
Backtracking
"""
# if the cell is empty
if board[row][col] == '.':
# iterate over all numbers from 1 to 9
for d in range(1, 10):
if could_place(d, row, col):
place_number(d, row, col)
place_next_numbers(row, col)
# if sudoku is solved, there is no need to backtrack
# since the single unique solution is promised
if not sudoku_solved:
remove_number(d, row, col)
else:
place_next_numbers(row, col)
# box size
n = 3
# row size
N = n * n
# lambda function to compute box index
box_index = lambda row, col: (row // n ) * n + col // n
# init rows, columns and boxes
rows = [defaultdict(int) for i in range(N)]
columns = [defaultdict(int) for i in range(N)]
boxes = [defaultdict(int) for i in range(N)]
#筛选出数字的位置
for i in range(N):
for j in range(N):
if board[i][j] != '.':
d = int(board[i][j])
place_number(d, i, j)
sudoku_solved = False
backtrack()
例题四
leetcode 76 最小覆盖子串
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
from collections import defaultdict
class Solution:
def minWindow(self, s: str, t: str) -> str:
# 初始化counter_s和counter_t
counter_s = defaultdict(int)
counter_t = defaultdict(int)
for ch in t:
counter_t[ch] += 1
#左指针位置
left = 0
#有效位数很关键!!!
valid = 0
# 记录最小覆盖子串的起始索引及长度
start = -1
minlen = float('inf')
for right in range(len(s)):
# 移动右边界, ch 是将移入窗口的字符
ch = s[right]
if ch in counter_t:
counter_s[ch] += 1
#若t字符串中有n个重复字母,全部找到valid才加一
if counter_s[ch] == counter_t[ch]:
valid += 1
# 判断左侧窗口是否要收缩
while valid == len(counter_t):
# 更新最小覆盖子串
if right - left < minlen:
minlen = right - left
start = left
# left_ch 是将移出窗口的字符
left_ch = s[left]
# 左移窗口
left += 1
# 进行窗口内数据的一系列更新
if left_ch in counter_s:
counter_s[left_ch] -= 1
#count_s中可能有n+m个left_ch
#但是count_t中只有n个left_ch
if counter_s[left_ch] < counter_t[left_ch]:
valid -= 1
# 返回最小覆盖子串
if start == -1:
return ""
return s[start: start + minlen + 1]
例题五:快乐数
leetcode 202
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
class Solution:
def isHappy(self, n: int) -> bool:
# 初始化 visited
visited = set()
# 当 n != 1,并且没见过 n 时进行判断(只要见过就会陷入死循环)
while n != 1 and n not in visited:
# 把 n 放入 visited
visited.add(n)
# 重新初始化总和
nxt = 0
# 计算 n 的各位数字平方和
while n != 0:
nxt += (n % 10) ** 2
n //= 10
# 把下一轮的数字设定为 n
n = nxt
# 判断 n 的最终结果是否为 1
return n == 1
例题六:
leetcode 219 存在重复元素
给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。
示例 1:
输入: nums = [1,2,3,1], k = 3
输出: true
示例 2:
输入: nums = [1,0,1,1], k = 1
输出: true
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
if not nums:
return False
hashmap ={}
for i in range(len(nums)):
if nums[i] not in hashmap:
hashmap[nums[i]] = i
else:
if k>=i-hashmap.get(nums[i]):
return True
else:
#这个就可以每次更新hashmap[nums[i]],剔除不符合要求的
hashmap[nums[i]] = i
return False
例题7:前K个高频元素(本地着重掌握字典排序和取前K个元素的技巧)
leetcode219
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
if not nums:
return []
hashmap = {}
for i in nums:
if i not in hashmap:
hashmap[i] = 1
else:
hashmap[i] +=1
res = sorted(hashmap.items(), key= lambda x: x[1],reverse=True)
print(res)
result = []
for i in range(k):
result.append(res[i][0])
return result
例题8(人生苦短,我用python!!!)
leetcode350 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
import collections
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
#python库真牛逼,自己创建字典,掉包侠的快乐你们体会不到,哈哈!!
n1,n2 = collections.Counter(nums1),collections.Counter(nums2)
res= []
for i in n1:
#若不存在n1[i]或n2[i]会返回0
tmp = min(n1[i],n2[i])
while tmp>0:
res.append(i)
tmp -=1
return res
例题9:和为K的数组(前缀和)
leetcode560
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
import collections
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
#这里调用defaultdict的作用就是给每一个出现的key一个为0的value
#而hashmap={}则为null
hashmap = collections.defaultdict(int)
#hashmap是用来记录前N个位置的前缀和的,所以hashmap[0]==1
hashmap[0] = 1
#记录当前的前缀总和
cur_num = 0
res = 0
for i in range(len(nums)):
cur_num +=nums[i]
#这个其实不难理解,若举个例子:[1,1,0,0,2,3] k=2
#上面这个例子的hashmap={0:1,1:1,2:3,4:1,7:1}
#所以当i在列表2的位置的时候,cur_num-k=2 在hashmap中
#所以不管是[0,0,2],[0,2],[2]都可以满足题意
if cur_num-k in hashmap:
res +=hashmap[cur_num-k]
#这里由于前面是collections.defaultdict()定义的,所以可以用+=1
hashmap[cur_num] +=1
return res
例题10 :
leetcode 567 字符串的排列(动态创建字典)
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).
示例2:
输入: s1= “ab” s2 = “eidboaoo”
输出: False
import collections
class Solution(object):
def checkInclusion(self, s1, s2):
"""
:type s1: str
:type s2: str
:rtype: bool
"""
l1, l2 = len(s1), len(s2)
c1 = collections.Counter(s1)
c2 = collections.Counter()
cnt = 0 #统计变量,全部26个字符,频率相同的个数,当cnt==s1字母的个数的时候,就是全部符合题意,返回真
p = q = 0 #滑动窗口[p,q]
while q < l2:
c2[s2[q]] += 1
if c1[s2[q]] == c2[s2[q]]: #对于遍历到的字母,如果出现次数相同
cnt += 1 #统计变量+1
if cnt == len(c1): #判断结果写在前面,此时证明s2滑动窗口和s1全部字符相同,返回真
return True
q += 1 #滑动窗口右移
if q - p + 1 > l1: #这是构造第一个滑动窗口的特殊判断,里面内容是维护边界滑动窗口
if c1[s2[p]] == c2[s2[p]]: #判断性的if写在前面,因为一旦频率变化,这个统计变量就减1
cnt -= 1
c2[s2[p]] -= 1 #字典哈希表移除最前面的字符
if c2[s2[p]] == 0: #由于counter特性,如果value为0,必须删除它
del c2[s2[p]]
p += 1 #最前面的下标右移动
return False
任务调度器
'''
找出列表中最多的元素,然后求出最多元素的个数nbucket,再求出最多元素个数的元素个数last_bucket_size
'''
from collections import Counter
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
ct = Counter(tasks)
nbucket = ct.most_common(1)[0][1]
last_bucket_size = list(ct.values()).count(nbucket)
res = (nbucket - 1) * (n + 1) + last_bucket_size
return max(res, len(tasks))
前K个高频词汇
class Solution:
def topKFrequent(self, words: List[str], k: int) -> List[str]:
if not words:
return []
hashmap = {}
for i in words:
if i not in hashmap:
hashmap[i] = 1
else:
hashmap[i] +=1
res = sorted(hashmap.items(), key= lambda x: x[1],reverse=True)
result = []
for i in range(k):
result.append(res[i][0])
return result
统计优美子数组
import collections
class Solution:
def numberOfSubarrays(self, nums: List[int], k: int) -> int:
hashmap = collections.defaultdict(int)
if not nums:
return 0
res = 0
hashmap[0] = 1
cur_odd = 0
for i in range(len(nums)):
if nums[i] % 2 ==1:
cur_odd +=1
if cur_odd-k in hashmap:
res +=hashmap[cur_odd-k]
hashmap[cur_odd] +=1
return res
电话号码的字母组合
'''
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
'''
class Solution(object):
def letterCombinations(self, digits):
"""
:type digits: str
:rtype: List[str]
"""
if not digits:
return []
# 一个映射表,第二个位置是"abc“,第三个位置是"def"。。。
# 这里也可以用map,用数组可以更节省点内存
d = [" ","*","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
# 先往队列中加入一个空字符
res = [""]
for i in digits:
size = len(res)
# 由当前遍历到的字符,取字典表中查找对应的字符串
letters = d[ord(i)-48]
# 计算出队列长度后,将队列中的每个元素挨个拿出来
for _ in range(size):
# 每次都从队列中拿出第一个元素
tmp = res.pop(0)
# 然后跟"def"这样的字符串拼接,并再次放到队列中
for j in letters:
res.append(tmp+j)
return res
连续子数组和
'''
给定一个包含非负数的数组和一个目标整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,
总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数。
示例 1:
输入: [23,2,4,6,7], k = 6
输出: True
解释: [2,4] 是一个大小为 2 的子数组,并且和为 6。
示例 2:
输入: [23,2,6,4,7], k = 6
输出: True
解释: [23,2,6,4,7]是大小为 5 的子数组,并且和为 42。
'''
class Solution:
def checkSubarraySum(self, nums, k):
sumdict = {}
sumN = 0
sumdict[0] = -1
for i in range(len(nums)):
sumN += nums[i]
if k != 0: sumN = sumN % k
if sumN in sumdict:
if i - sumdict[sumN] > 1:#判断是否间隔两位以上[5,0,0]与[5,0] K=0的比较
return True
else:
sumdict[sumN] = i
print(sumdict)
return False
nums = [2,5,33,6,7,25,15]
k = 13
A = Solution()
print(A.checkSubarraySum(nums,k))
属性哈希
hashmap = {}
n,k = list(map(int,input().strip().split()))
b = [0 for i in range(k-1)]
res= 0
for i in range(n):
a = [int(i) for i in input().split()]
for j in range(1,k):
a[j] = a[j] - a[0]
b[j-1] = -a[j]
if tuple(b) in hashmap.keys():
res +=1
# hashmap[tuple(a[1:])] += 1
else:
hashmap[tuple(a[1:])] =1
print(res)
print(hashmap)
有效的括号
'''
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
'''
class Solution:
def isValid(self, s: str) -> bool:
dic = {'{': '}', '[': ']', '(': ')', '?': '?'}
stack = ['?']
for c in s:
if c in dic: stack.append(c)
elif dic[stack.pop()] != c: return False
return len(stack) == 1
砖墙
'''
你的面前有一堵方形的、由多行砖块组成的砖墙。 这些砖块高度相同但是宽度不同。你现在要画一条自顶向下的、穿过最少砖块的垂线。
砖墙由行的列表表示。 每一行都是一个代表从左至右每块砖的宽度的整数列表。
如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你需要找出怎样画才能使这条线穿过的砖块数量最少,并且返回穿过的砖块数量。
你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。
'''
class Solution:
def leastBricks(self, wall: List[List[int]]) -> int:
hashmap = {}
if not wall:
return 0
n = len(wall)
temp = 0
for i in range(n):
for j in range(len(wall[i])-1):
temp +=wall[i][j]
if temp not in hashmap.keys():
hashmap[temp] = 1
else:
hashmap[temp] += 1
temp =0
if not hashmap:
return n
max_value = max(hashmap.values())
res = n - max_value
return res