前缀和 & HASH
560. 和为K的子数组(中等)
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。
方法:前缀和+哈希表
思路和算法
定义 pre[i] 为 [0..i]里所有数的和,则 pre[i] 可以由pre[i−1] 递推而来,即:
pre[i]=pre[i−1]+nums[i]
那么 [j..i] 这个子数组和为 k这个条件我们可以转化为
pre[i]−pre[j−1]==k
简单移项可得符合条件的下标 j需要满足
pre[j−1]==pre[i]−k
考虑以 i 结尾的和为 k的连续子数组个数时只要统计有多少个前缀和为 pre[i]−k 的pre[j] 即可。建立哈希表 mp,以和为键,出现次数为对应的值,记录 pre[i] 出现的次数,从左往右边更新边计算答案,那么以 i 结尾的答案 mp[pre[i]−k] 即可在 O(1) 时间内得到。最后的答案即为所有下标结尾的和为 k 的子数组个数之和。
需要注意的是,从左往右边更新边计算的时候已经保证了mp[pre[i]−k] 里记录的 pre[j] 的下标范围是0≤j≤i 。同时,由于pre[i] 的计算只与前一项的答案有关,因此我们可以不用建立 pre 数组,直接用 pre 变量来记录 pre[i−1] 的答案即可。
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
# pre_times存储“前缀和”出现的次数
pre_times = collections.defaultdict(int)
pre_times[0] = 1 # 先给一个初始值,代表前缀和为0的出现了一次
cur_sum = 0 # 记录当前位置的前缀和
n, res = len(nums), 0
for i in range(n):
cur_sum += nums[i]
if cur_sum - k in pre_times: # 如果当前前缀和减去目标值k所得到的值在字典中出现
res += pre_times[cur_sum-k]
pre_times[cur_sum] += 1
return res
974. 和可被 K 整除的子数组(中等)
给定一个整数数组 A
,返回其中元素之和可被 K
整除的(连续、非空)子数组的数目。
示例:
输入:A = [4,5,0,-2,-3,1], K = 5
输出:7
解释:
有 7 个子数组满足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
方法1:哈希表+逐一统计
令 P[i] = A[0] + A[1] + ... + A[i]。
那么每个连续子数组的和 sum(i,j) 就可以写成 P[j] - P[i-1](其中0<i<j)的形式。
此时,判断子数组的和能否被 K 整除就等价于判断 (P[j] - P[i-1]) mod K == 0,根据 同余定理,只要 P[j] mod K == P[i-1] mod K,就可以保证上面的等式成立。
因此可以考虑对数组进行遍历,在遍历时统计答案。当遍历到第 i 个元素时,可以维护一个以前缀和模 K的值为键,出现次数为值的哈希表 record,在遍历的同时进行更新。这样在计算以 i 结尾的符合条件的子数组个数时,根据上面的分析,答案即为 [0..i-1] 中前缀和模 K也为 P[i]modK 的位置个数,即 record[P[i]modK]。
需要注意的一个边界条件是,我们需要对哈希表初始化,记录 record[0]=1,这样就考虑了前缀和本身被 K整除的情况。
class Solution:
def subarraysDivByK(self, A: List[int], K: int) -> int:
record = {0: 1}
total, ans = 0, 0
for elem in A:
total += elem
modules = total % K
same = record.get(modules, 0)
ans += same
record[modules] = same + 1
return ans
方法2:哈希表+单次统计
https://leetcode-cn.com/problems/subarray-sums-divisible-by-k/solution/he-ke-bei-k-zheng-chu-de-zi-shu-zu-by-leetcode-sol/
差分
253(会员)
1109. 航班预订统计
这里有 n
个航班,它们分别从 1
到 n
进行编号。
我们这儿有一份航班预订表,表中第 i
条预订记录 bookings[i] = [i, j, k]
意味着我们在从 i
到 j
的每个航班上预订了 k
个座位。
请你返回一个长度为 n
的数组 answer
,按航班编号顺序返回每个航班上预订的座位数。
示例:
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
提示:
1 <= bookings.length <= 20000
1 <= bookings[i][0] <= bookings[i][1] <= n <= 20000
1 <= bookings[i][2] <= 10000
- 换一种思路理解题意,将问题转换为:某公交车共有 n 站,第 i 条记录 bookings[i] = [i, j, k] 表示在 i 站上车 k 人,乘坐到 j 站,在 j+1 站下车,需要按照车站顺序返回每一站车上的人数
- 根据 1 的思路,定义 counter[] 数组记录每站的人数变化,counter[i] 表示第 i+1 站。遍历 bookings[]:bookings[i] = [i, j, k] 表示在 i 站增加 k 人即 counters[i-1] += k,在 j+1 站减少 k 人即 counters[j] -= k
- 遍历(整理)counter[] 数组,得到每站总人数: 每站的人数为前一站人数加上当前人数变化 counters[i] += counters[i - 1]
class Solution:
def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
ans = [0 for i in range(n+1)]
for i, j, k in bookings:
ans[i-1] += k
ans[j] -= k
for s in range(1, n):
ans[s] += ans[s-1]
return ans[:n]
1094. 拼车(中等)
假设你是一位顺风车司机,车上最初有 capacity
个空座位可以用来载客。由于道路的限制,车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向,你可以将其想象为一个向量)。
这儿有一份乘客行程计划表 trips[][]
,其中 trips[i] = [num_passengers, start_location, end_location]
包含了第 i
组乘客的行程信息:
必须接送的乘客数量;
乘客的上车地点;
以及乘客的下车地点。
这些给出的地点位置是从你的 初始 出发位置向前行驶到这些地点所需的距离(它们一定在你的行驶方向上)。
请你根据给出的行程计划表和车子的座位数,来判断你的车是否可以顺利完成接送所有乘客的任务(当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true
,否则请返回 false
)。
示例 1:
输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false
示例 2:
输入:trips = [[2,1,5],[3,3,7]], capacity = 5
输出:true
示例 3:
输入:trips = [[2,1,5],[3,5,7]], capacity = 3
输出:true
示例 4:
输入:trips = [[3,2,7],[3,7,9],[8,3,9]], capacity = 11
输出:true
方法:差分
class Solution:
def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
n = len(trips)
curAns = [0] * 1001
for i in range(n):
curAns[trips[i][1]] += trips[i][0]
curAns[trips[i][2]] -= trips[i][0]
for i in range(1, 1001):
curAns[i] += curAns[i-1]
if curAns[i] > capacity:
return False
return True
122. 买卖股票的最佳时机 II(简单)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
解题思路:
股票买卖策略:
(1)单独交易日:设今天价格 p1、明天价格 p2,则今天买入、明天卖出可赚取金额 p2- p1(负值代表亏损)。
(2) 连续上涨交易日:设此上涨交易日股票价格分别为 p1, p2, ... , pn,则第一天买最后一天卖收益最大,即 pn - p1;等价于每天都买卖,即 pn - p1=(p2 - p1)+(p3 - p2)+...+(pn - pn-1)。
(3)连续下降交易日:则不买卖收益最大,即不会亏钱。
算法流程:
遍历整个股票交易日价格列表 price,策略是所有上涨交易日都买卖(赚到所有利润),所有下降交易日都不买卖(永不亏钱)。
设 temp 为第 i-1 日买入与第 i 日卖出赚取的利润,即 temp = prices[i] - prices[i - 1] ;
当该天利润为正 temp > 0,则将利润加入总利润 profit;当利润为 0或为负,则直接跳过;
遍历完成后,返回总利润 profit。
复杂度分析:
时间复杂度 O(N):只需遍历一次price;
空间复杂度 O(1):变量使用常数额外空间。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
profit = 0
n = len(prices)
for i in range(1, n):
temp = prices[i] - prices[i-1]
if temp > 0:
profit += temp
return profit
拓扑排序
字符串
5
227
387. 字符串中的第一个唯一字符(简单)
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
示例:
s = "leetcode"
返回 0
s = "loveleetcode"
返回 2
提示:你可以假定该字符串只包含小写字母。
方法:使用哈希表存储频数
对字符串进行两次遍历:
在第一次遍历时,我们使用哈希映射统计出字符串中每个字符出现的次数。
在第二次遍历时,我们只要遍历到了一个只出现一次的字符,那么就返回它的索引,否则在遍历结束后返回 −1。
class Solution:
def firstUniqChar(self, s: str) -> int:
hash = collections.Counter(s)
for i, ch in enumerate(s):
if hash[ch] == 1:
return i
return -1
451. 根据字符出现频率排序(中等)
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
示例 1:
输入:
"tree"
输出:
"eert"
解释:
'e'出现两次,'r'和't'都只出现一次。
因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
示例 2:
输入:
"cccaaa"
输出:
"cccaaa"
解释:
'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。
注意"cacaca"是不正确的,因为相同的字母必须放在一起。
示例 3:
输入:
"Aabb"
输出:
"bbAa"
解释:
此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
注意'A'和'a'被认为是两种不同的字符。
方法1:collections.Counter
简单介绍一下 Counter。
它是一个用来统计出现次数的类,在 collections 包里。
Counter(s) 就可以返回一个类似字典的结构,键是s中的项,值是这个项出现的次数。
对于字符串,就是每个字符和它出现的次数。
Counter类有一个函数,叫most_common(int n),可以返回最常出现的几项,如果不加参数,就全部返回,相当于按照出现次数从大到小输出。
输出的格式形如 [('s',4),('a',3)]。
即字符s出现4次,a出现3次。
class Solution:
def frequencySort(self, s: str) -> str:
return ''.join(i * j for i, j in Counter(s).most_common())
方法2:堆排序
大根堆
class Solution:
def frequencySort(self, s: str) -> str:
hash = collections.defaultdict(int)
for i in s:
hash[i] += 1
res = []
heapq.heapify(res)
for i in hash:
for j in range(hash[i]):
heapq.heappush(res, (-hash[i], i))
return ''.join([heapq.heappop(res)[1] for _ in range(len(s))])
方法3:桶排序
93. 复原IP地址(中等)
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效的 IP 地址。
示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000"
输出:["0.0.0.0"]
示例 3:
输入:s = "1111"
输出:["1.1.1.1"]
示例 4:
输入:s = "010010"
输出:["0.10.0.10","0.100.1.0"]
示例 5:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示:
0 <= s.length <= 3000
s
仅由数字组成
方法:dfs
由于我们需要找出所有可能复原出的 IP 地址,因此可以考虑使用递归的方法,对所有可能的字符串分隔方式进行搜索,并筛选出满足要求的作为答案。
设题目中给出的字符串为 s。我们用递归函数dfs(segId,segStart) 表示我们正在从s[segStart] 的位置开始,搜索 IP 地址中的第 segId 段,其中segId∈{0,1,2,3}。由于 IP 地址的每一段必须是 [0,255] 中的整数,因此我们从 segStart 开始,从小到大依次枚举当前这一段 IP 地址的结束位置 segEnd。如果满足要求,就递归地进行下一段搜索,调用递归函数dfs(segId+1,segEnd+1)。
特别地,由于 IP 地址的每一段不能有前导零,因此如果 s[segStart] 等于字符 0,那么 IP 地址的第segId 段只能为 0,需要作为特殊情况进行考虑。
在递归搜索的过程中,如果我们已经得到了全部的 4段 IP 地址(即segId=4),并且遍历完了整个字符串(即 segStart=∣s∣,其中 |s| 表示字符串 s 的长度),那么就复原出了一种满足题目要求的 IP 地址,我们将其加入答案。在其它的时刻,如果提前遍历完了整个字符串,那么我们需要结束搜索,回溯到上一步。
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
SEG_COUNT = 4
ans = list()
segments = [0] * SEG_COUNT
def dfs(segId: int, segStart: int):
# 如果找到了4段IP地址并且遍历完了字符串,那么就是一种答案
if segId == SEG_COUNT:
if segStart == len(s):
ipAddr = ".".join(str(seg) for seg in segments)
ans.append(ipAddr)
return
# 如果还没有找到4段IP地址就已经遍历完了字符串,那么提前回溯
if segStart == len(s):
return
# 由于不能有前导零,如果当前数字为0,那么这一段IP地址只能为0
if s[segStart] == "0":
segments[segId] = 0
dfs(segId+1, segStart+1)
# 一般情况,枚举每一种可能性并递归
addr = 0
for segEnd in range(segStart, len(s)):
addr = addr * 10 + (ord(s[segEnd]) - ord("0"))
if 0 < addr <= 0xFF:
segments[segId] = addr
dfs(segId+1, segEnd+1)
else:
break
dfs(0, 0)
return ans