目录
第一题 和为k的子数组
第二题 滑动窗口最大值
第三题 最小覆盖子串
第一题 和为k的子数组:
给你一个整数数组nums和一个整数k, 请你统计并该数组中和为k的子数组的个数。
子数组是数组中元素的连续非空序列。
示例:
输入:nums = [1, 1, 1],k = 2
输出: 2
问题分析:
方案一:首先考虑暴力破解,通过两层循环来遍历数字中的各个子数组,并且计算每个子数组的和,时间复杂度为O(n^3)。
代码:
class Solution: def subarraySum(self, nums: List[int], k: int) -> int: count = 0 n = len(nums) for i in range(n): for j in range(i, n): if sum(nums[i:j+1]) == k: count += 1 return count
方案二:由于在方案一中每次计算子数组和sum[j]时,都是从nums[i]加到nums[j],但在计算sum[j+1]时,我们可以直接通过sum[j] + nums[j+1]得到,不需要从nums[i]重新开始,避免了重复计算,时间复杂度减少到O(n^2)。
代码:
class Solution(object): def subarraySum(self, nums, k): """ :type nums: List[int] :type k: int :rtype: int """ count = 0 n = len(nums) for i in range(n): sum = 0 for j in range(i, n): sum += nums[j] if sum == k: count += 1 return count
方案三:O(n^2)时间复杂度仍然很高,为此我们进行进一步优化。借助前缀和+字典的方法,在遍历数组时计算当前子数组的前缀和sum[i],并且在字典中查询是否存在前缀和为sum[i]+k的子数组。(字典中保存该前缀和出现的次数)若出现,则将计数器加上该次数。之后,将sum[i]加入字典。时间复杂度为O(n)。
代码:
class Solution(object): def subarraySum(self, nums, k): """ :type nums: List[int] :type k: int :rtype: int """ # 前缀和 + 字典 sums_times = collections.defaultdict(int) sums_times[0] = 1 curr_sums = 0 res = 0 for i in range(len(nums)): curr_sums += nums[i] if (curr_sums - k) in sums_times: res += sums_times[curr_sums - k] sums_times[curr_sums] += 1 return res
第二题 滑动窗口最大值:
给你一个整数数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k个数字。滑动窗口每次只向右移一位。返回滑动窗口中的最大值。
示例:
输入:nums = [1, 3, -1, -3, 5, 3, 6, 7],k = 3
输出:[3, 3, 5, 5, 6, 7]
问题分析:
首先,考虑暴力解法,定义一个大小为k的优先队列来充当窗口,依次向有移动,每次移动取出窗口中的最大值,时间复杂度为O(n * k),当n和k都很大时,时间复杂度过高。我们注意到,移动前后两次的窗口有k-1个共同的元素,为此我们可以利用大根堆这种数据结构来优化求窗口中最大值的步骤,已知将新元素添加到堆中的时间复杂度为O(log k),所有总体时间复杂度为O(n * log k)。
代码如下:
class Solution(object): def maxSlidingWindow(self, nums, k): """ :type nums: List[int] :type k: int :rtype: List[int] """ n = len(nums) # 创建堆 q = [(-nums[i], i) for i in range(k)] heapq.heapify(q) ans = [-q[0][0]] for i in range(k, n): heapq.heappush(q, (-nums[i], i)) while q[0][1] < i - k + 1: heapq.heappop(q) ans.append(-q[0][0]) return ans
注意:在创建堆时,我们采用(nums[i], i)的二元组为元素。因为需要判断堆顶元素是否在窗口中,若不在则将其去除,保证选取的最大堆顶在窗口之内。其次,python中heapq函数默认创建的小根堆,为此我们将数据取反为-nums[i]来达到创建大根堆的效果。
第三题 最小覆盖子串:
给你一个字符串s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t的所有字符的子串,则返回""。
示例:
输入: s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串t的"A", "B", "C"。
问题分析:
本题可以应用滑动窗口的思想,建立一个可以变化大小的滑动窗口,从字符串左侧遍历到最右侧,具体步骤如下:
步骤一:创建两个变量i、j分别为窗口的左边界和右边界。j不断向右移动,扩大窗口,直到窗口中的子串包含了t中的所有字符。
步骤二:左边界i开始向右移动,缩小窗口,直到缩小目前包含t所有字符的最小窗口为止,将窗口中的长度和子串记录,最终取最短的即可。
步骤三:i往右移动一位,进行下一轮最小子串的寻找。
除了上述三个主要步骤之外,我们还需要解决如何判断是否为最小子串的问题。为此,我们可以创建哈希表来存储t中各个字符的个数,当窗口中每出现一个需要的字符,则哈希表中该字符个数减一,0为满足条件,负数则表示有多余的该字符;从窗口去除则相应字符加一。我们只需要每次遍历哈希表,查询相应字符是否都为0即可。但是这样的查询会带来O(k)的时间复杂度(k为哈希表中元素个数),为此我们可以定义一个needCut变量来记录当前总共需要的字符数,当needCut为0时,则找到当前最短子串,这样只需要O(1)的时间复杂度。
代码实现:
class Solution(object): def minWindow(self, s, t): """ :type s: str :type t: str :rtype: str """ # 滑动窗口 left = 0 right = -1 need = collections.defaultdict(int) needCut = len(t) res = "" min_length = len(s) if len(s) < len(t): return "" for i in range(len(t)): need[t[i]] += 1 while right < len(s) - 1: while needCut > 0 and right < len(s) - 1: right += 1 if need[s[right]] > 0: needCut -= 1 need[s[right]] -= 1 while needCut == 0: if s[left] in need: if need[s[left]] == 0: if right - left + 1 <= min_length: min_length = right - left + 1 res = s[left: right + 1] needCut += 1 need[s[left]] += 1 left += 1 return res