方法 1:暴力
想法
按照题目要求直接求。把所有可能的子数组求和并更新 \text{ans}ans ,直到我们找到最优子数组且和满足 \text{sum} \geq \text{s}sum≥s 。
算法
初始化 \text{ans}=\text{INT\_MAX}ans=INT_MAX
用变量 ii 从左到右遍历数组:
用变量 jj 从当前元素到数组尾部遍历:
将 ii 到 jj 这些元素求和得到 \text{sum}sum
如果和 \text{sum}sum 比 ss 大:
更新 \text{ans} = \min(\text{ans}, (j - i + 1))ans=min(ans,(j−i+1))
继续迭代
C++
int minSubArrayLen(int s, vector<int>& nums)
{
int n = nums.size();
int ans = INT_MAX;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int sum = 0;
for (int k = i; k <= j; k++) {
sum += nums[k];
}
if (sum >= s) {
ans = min(ans, (j - i + 1));
break; //Found the smallest subarray with sum>=s starting with index i, hence move to next index
}
}
}
return (ans != INT_MAX) ? ans : 0;
}
复杂度分析
时间复杂度:O(n^3)O(n
3
) 。
对数组里的每一个元素,我们从它开始枚举所有的子数组,需要的时间为 O(n^2)O(n
2
)。
将每一个子数组求和的时间复杂度为:O(n)O(n) 。
所以总时间复杂度为:O(n^2 * n) = O(n^3)O(n
2
∗n)=O(n
3
) 。
空间复杂度:O(1)O(1)。只是用了常数个额外变量。
方法 2:优化的暴力
想法
在方法 1 中,我们注意到求子数组的和需要的时间为 O(n)O(n) 。我们其实可以很容易地实现 O(1)O(1) 时间的求和,只需要从开始元素用一个累加器保存和。我们将累积和保存在 \text{sums}sums 中,通过这种方法,我们可以轻松求出任意子区间的和。
算法
算法流程与方法 1 类似。
唯一的不同是求子数组的和:
建立一个大小为 \text{nums}nums 的向量 \text{sums}sums
初始化 \text{sums}[0]=\text{nums}[0]sums[0]=nums[0]
遍历 向量 \text{sums}sums :
更新 \text{sums}[i] = \text{sums}[i-1] + \text{nums}[i]sums[i]=sums[i−1]+nums[i]
从 ii 到 jj 的和计算方法:
\text{sum}=\text{sums}[j] - \text{sums}[i] +\text{nums}[i]sum=sums[j]−sums[i]+nums[i] ,其中 \text{sums}[j] - \text{sums}[i]sums[j]−sums[i] 是从第 i+1i+1 个元素到第 jj 个元素的和。
C++
int minSubArrayLen(int s, vector<int>& nums)
{
int n = nums.size();
if (n == 0)
return 0;
int ans = INT_MAX;
vector<int> sums(n);
sums[0] = nums[0];
for (int i = 1; i < n; i++)
sums[i] = sums[i - 1] + nums[i];
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int sum = sums[j] - sums[i] + nums[i];
if (sum >= s) {
ans = min(ans, (j - i + 1));
break; //Found the smallest subarray with sum>=s starting with index i, hence move to next index
}
}
}
return (ans != INT_MAX) ? ans : 0;
}
复杂度分析
时间复杂度:O(n^2)O(n
2
) 。
找到所有子数组的时间复杂度为 O(n^2)O(n
2
) 。
计算子数组的和为 O(1)O(1) 的时间。
因此,总时间复杂度为:O(n^2 * 1) = O(n^2)O(n
2
∗1)=O(n
2
) 。
空间复杂度:O(n)O(n) 。
额外的 \text{sums}sums 数组空间大小为 O(n)O(n) 。
方法 3:使用二分查找
想法
我们可以用二分查找的方法优化方法 2 。我们找到从下标 ii 开始满足 \text{sum} \geq \text{s}sum≥s 的子数组需要 O(n)O(n) 的时间。但是我们可以用二分查找的方法把这个时间优化到 O(\log(n))O(log(n)) 。在方法 2 中,我们从 ii 开始找 jj ,直到找到 \text{sum}=\text{sums}[j] - \text{sums}[i] + \text{nums}[i]sum=sums[j]−sums[i]+nums[i] 大于等于\text{s}s 的。与其线性地查找这个和,我们可以使用二分搜索的方法找到 \text{sums}sums 中不小于 \text{s}+\text{sums[i]}-\text{nums[i]}s+sums[i]−nums[i] 的第一个 \text{sums[j]}sums[j] ,可以用 C++ STL 中的 \text{lower_bound} 函数做到。
算法
创建大小为 n+1n+1 的数组 sumssums :
\text{sums}[0]=0\text{, }\text{sums}[i]=\text{sums}[i-1]+\text{nums}[i-1]
sums[0]=0, sums[i]=sums[i−1]+nums[i−1]
从 i=1i=1 到 nn 枚举:
在 \text{sums}sums 中找到值 \text{to\_find}to_find ,满足从 ii 开始到这个位置的和大于等于 ss 且是最小子数组:
\text{to\_find}=\text{s}+\text{sums}[i-1]
to_find=s+sums[i−1]
+在 \text{sums}sums 中找到值满足大于等于 \text{to_find} 的下标,记作 \text{bound}bound
+如果我们在 \text{sums}sums 中找到了值 \text{to_find}, 那么:
- 当前子数组的大小为:
\text{bound} - (\text{sums.begin}()+i-1)bound−(sums.begin()+i−1)
- 将 ansans 与当前数组的大小做比较,并把较小值保存到 ansans 中
C++
int minSubArrayLen(int s, vector<int>& nums)
{
int n = nums.size();
if (n == 0)
return 0;
int ans = INT_MAX;
vector<int> sums(n + 1, 0); //size = n+1 for easier calculations
//sums[0]=0 : Meaning that it is the sum of first 0 elements
//sums[1]=A[0] : Sum of first 1 elements
//ans so on...
for (int i = 1; i <= n; i++)
sums[i] = sums[i - 1] + nums[i - 1];
for (int i = 1; i <= n; i++) {
int to_find = s + sums[i - 1];
auto bound = lower_bound(sums.begin(), sums.end(), to_find);
if (bound != sums.end()) {
ans = min(ans, static_cast<int>(bound - (sums.begin() + i - 1)));
}
}
return (ans != INT_MAX) ? ans : 0;
}
复杂度分析
时间复杂度:O(n\log(n))O(nlog(n)) 。
对向量中的每一个元素,从它开始用二分查找找到子数组,满足和大于 ss 。因此,遍历的时间复杂度是 O(n)O(n) ,二分查找的时间复杂度是 O(\log(n))O(log(n)) 。
因此,总时间复杂度是 O(n*\log(n))O(n∗log(n))
空间复杂度:O(n)O(n)。\text{sums}sums 需要额外的 O(n)O(n) 空间。
方法 4:使用两个指针
想法
到现在为止,我们都保持子数组的左端点不动去找右端点。其实一旦知道这个位置开始的子数组不会是最优答案了,我们就可以移动左端点。我们用 2 个指针,一个指向数组开始的位置,一个指向数组最后的位置,并维护区间内的和 \text{sum}sum 大于等于 ss 同时数组长度最小。
算法
初始化 \text{left}left 指向 0 且初始化 \text{sum}sum 为 0
遍历 \text{nums}nums 数组:
将 \text{nums}[i]nums[i] 添加到 \text{sum}sum
当 \text{sum}sum 大于等于 ss 时:
更新 \text{ans}=\min(\text{ans},i+1-\text{left})ans=min(ans,i+1−left) ,其中 i+1-\text{left}i+1−left是当前子数组的长度
然后我们可以移动左端点,因为以它为开头的满足 \text{sum} \geq ssum≥s 条件的最短子数组已经求出来了
将 \text{sum}sum 减去 \text{nums[left]}nums[left] 然后增加 \text{left}left
C++
int minSubArrayLen(int s, vector<int>& nums)
{
int n = nums.size();
int ans = INT_MAX;
int left = 0;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += nums[i];
while (sum >= s) {
ans = min(ans, i + 1 - left);
sum -= nums[left++];
}
}
return (ans != INT_MAX) ? ans : 0;
}
复杂度分析
时间复杂度:O(n)O(n) 。每个指针移动都需要 O(n)O(n) 的时间。
每个元素至多被访问两次,一次被右端点访问,一次被左端点访问。
空间复杂度: O(1)O(1) 。\text{left}left,\text{sum}sum,\text{ans}ans 以及 ii 这些变量只需要常数个空间。
作者:LeetCode
链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum/solution/chang-du-zui-xiao-de-zi-shu-zu-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。