1. 两数之和
560. 和为K的子数组
1248. 统计「优美子数组」
437. 路径总和 III
1. 两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
题目难度: Easy
对于第一次接触这种类型的题目的人而言, 最能想到的方式就是类似冒泡排序的暴力算法, 时间复杂度为$O(n^2)$, 空间复杂度为$O(1)$.
但题目要求每一个元素只能使用一次, 显然上面的做法并不能满足要求, 那么怎样才可以每个元素只使用一次呢
回到最直观的想法, 我们需要两两元素进行判断: nums[i] + nums[j] = target?, 如果每个元素只能使用一次, 换一种思路, nums[j] = target - nums[i], 可以想到将target - nums[i]提前保存下来 由于本题返回的是索引, 于是自然想到使用字典来保存, 后续只要判断nums[j]是否在字典中就可以了.
顺着思路, 可以写出如下的代码
def twoSum(self, nums: List[int], target: int) -> List[int]:
if not nums or len(nums) < 2:
return
# 使用字典保存target - nums[i]
prefix_sum = {}
n = len(nums)
for i in range(n):
if nums[i] in prefix_sum:
return [prefix_sum[nums[i]], i]
prefix[target-nums[i]] = i
时间复杂度$O(n)$, 空间复杂度$O(n)$
560. 和为K的子数组
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。
一开始看到这个题目, 确实不知如何下手, 主要问题在于:
- 子数组中可能包含负数.
- 并没有限定子数组长度.
现假设子数组开始位置为$i$, 结束位置为$j$, 即$nums[i]+...+nums[j]=target$
而
$nums[i]+...+nums[j]=sum(nums_{0,...,j})-sum(nums_{0,...,i-1})$
因此, 我们可以用一个字典保存所有可能的连续子数组(上式中右边部分)和, 字典的值为和等于k的子数组个数.
这样, 可以一次遍历, 即可得到和为k的所有子数组个数.
按照这个思路, 可以写出如下的代码
def subarraySum(nums, k):
if not nums:
return 0
n = len(nums)
res = 0
# 从索引为0的位置到当前位置的数组和
prefix_sum = 0
# 保存了所有前缀和及其对应的子数组个数
dicts = {0: 1}
for i in range(n):
prefix_sum += nums[i]
if prefix_sum - k in dicts:
res += dicts[prefix_sum - k]
#
dicts[prefix_sum] = dicts.get(prefix_sum, 0) + 1
return res
时间复杂度$O(n)$, 空间复杂度$O(n)$
1248. 统计「优美子数组」
给你一个整数数组 nums 和一个整数 k。
如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中「优美子数组」的数目。
示例 1:
输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
示例 2:
输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。
示例 3:
输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
输出:16
提示:
- 1 <= nums.length <= 50000
- 1 <= nums[i] <= 10^5
- 1 <= k <= nums.length
该题和上一题(560题)的思路基本一致, 只是由元素和变为了奇数个数, 本质上都是一样的.
def numberOfSubarrays(nums, k):
if not nums or len(nums) < k:
return 0
res = 0
# 值表示所有可能的奇数个数, 及其对应的子数组的个数
dicts = {0: 1}
# 表示从索引为0的位置到当前位置, 奇数的个数
prefix_sum = 0
n = len(nums)
for i in range(n):
if nums[i] % 2:
prefix_sum += 1
if prefix_sum - k in dicts:
res += dicts[prefix_sum - k]
if prefix_sum not in dicts:
dicts[prefix_sum] = 0
# 奇数个数为prefix_sum的子数组个数加1
dicts[prefix_sum] += 1
return res
时间复杂度$O(n)$, 空间复杂度$O(n)$
437. 路径总和 III
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例:
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
返回 3。和等于 8 的路径有:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
这题可以看作是前缀和在二叉树中的应用, 思路还是一样的
def pathSum(self, root, sum):
self.dicts = {0: 1}
self.res = 0
def helper(root, prefix_sum, sum):
if not root:
return 0
prefix_sum += root.val
if prefix_sum - sum in self.dicts:
self.res += self.dicts[prefix_sum - sum]
self.dicts[prefix_sum] = self.dicts.get(prefix_sum, 0) + 1
helper(root.left, prefix_sum, sum)
helper(root.right, prefix_sum, sum)
# Note: 回到上一层时, 需要将当前的前缀和对应的路径数目减1
self.dicts[prefix_sum] -= 1
helper(root, 0, sum)
return res
时间复杂度$O(n)$, 空间复杂度$O(n)$
总结
前缀和的方法通常用于解决序列中满足条件的连续子序列的数目, 对于这类问题, 通常会使用字典来记录每个前缀和及其出现的次数, 这样每次只需要判断(当前前缀和 - target) 是否在字典中, 如果在, 则结果加上该前缀和对应的个数.否则将其添加到字典中.
值得注意的是, 前缀和字典通常会初始化为{0, 1}, 因为如果满足条件的连续子数组是从0开始时, 当前前缀和等于连续子数组和, (当前前缀和 - target) = 0 并不一定存在字典中.
以上内容, 如有错误或不当之处, 可以直接在评论区指出. 恳请赐教, 谢谢.