题目难度: 中等
原题链接
今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复
剑指offer2
就能看到该系列当前连载的所有文章了, 记得关注哦~
题目描述
- 给定一个正整数数组 nums 和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。
示例 1:
- 输入: nums = [10,5,2,6], k = 100
- 输出: 8
- 解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
- 需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
示例 2:
- 输入: nums = [1,2,3], k = 0
- 输出: 0
提示:
- 1 <= nums.length <= 3 * 10^4
- 1 <= nums[i] <= 1000
- 0 <= k <= 10^6
题目思考
- 如何尽可能优化时间复杂度?
解决方案
思路
- 分析题目, 最容易想到的做法就是暴力两重循环: 遍历每个下标起点, 累乘当前的数字, 当前乘积小于 k 时计入最终结果
- 但这种做法时间效率太低 (
O(N^2)
), 如何优化呢? - 注意到题目提到输入是正整数数组, 我们可以针对这一点对暴力法进行优化
- 假设外层遍历到 i 作为起点, 内层遍历到下标 j 时子数组乘积大于等于了 k
- 此时我们可以直接跳出内层循环 (因为数组元素全是正整数, 后面的子数组乘积只会更大), 无需继续遍历后面的数字, 并以 i+1 作为起点重新开始遍历
- 但就算有了上述优化, 对于所有子数组乘积都小于 k 的情况, 时间复杂度仍是
O(N^2)
- 回过头再看暴力法, 每次子数组不满足要求时, 都需要从上个起点加 1 处重新开始内层遍历, 这样其实是没必要的
- 因为
[i+1,j]
的子数组乘积可以直接根据[i,j]
的子数组乘积计算得到, 即除以nums[i]
, 完全可以用常数时间判断子数组[i+1,j]
是否满足要求 - 所以更优化的做法是固定当前的终点 j, 将起点 i 继续向右移动, 并更新子数组乘积, 直到子数组乘积小于 k, 此时[i,j]窗口内的以 j 为终点的子数组都满足条件, 将其个数计入最终结果, 并继续遍历下一个终点 j 即可
- 这样起点和终点都只需要遍历一遍, 时间复杂度降到了 O(N)
- 以上就是典型的滑动窗口的思想, 通常做法就是维护双指针代表窗口起点和终点, 然后根据当前窗口是否满足要求来进行不同的处理
- 下面代码中对上述每个步骤都有详细注释, 方便大家理解
复杂度
- 时间复杂度
O(N)
: 最多遍历数组每个元素两遍 - 空间复杂度
O(1)
: 只使用了几个常数空间的变量
代码
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
# 滑动窗口+前缀乘积
res = 0
cur = 1
i = 0
for j, num in enumerate(nums):
# 累乘当前数字
cur *= num
while i <= j and cur >= k:
# 注意窗口长度不能小于0, 所以需要额外限制条件i<=j
cur //= nums[i]
i += 1
# [i,j]窗口内以j为终点的子数组的乘积都小于k, 可以计入最终结果
# 注意i可能是j+1, 此时窗口长度为0, 表示不存在以j为终点的乘积小于k的子数组
res += j - i + 1
return res
大家可以在下面这些地方找到我~😊
我的 GitHub
我的 Leetcode
我的 CSDN
我的知乎专栏
我的头条号
我的牛客网博客
我的公众号: 算法精选, 欢迎大家扫码关注~😊