[LeetCode] 209. 长度最小的子数组

1. 问题描述
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 1091 <= nums.length <= 1051 <= nums[i] <= 104
进阶:
- 如果你已经实现 **
O(n)时间复杂度的解法, 请尝试设计一个O(n log(n))时间复杂度的解法。
2. 实现思路
2.1 自己的思路
无论实现与否,都是群星的一丝痕迹~~(唔,所以一定要写出来啊喂)~
我的天啊,这就是mid难度大人,初见跪了)
首次分析,没好好看题,错误以为是任取几个元素≥target,想了半天,才发现子数组格式是连续的不用任意选取
但是还是想不出来,看一眼tag,前缀和?滑动窗口又是什么?
无奈去问问AI吧,前缀和原来是状态累加,其数组公式为pref[i + 1] = pref[i] + nums[i],
诶?那这样有了前缀和,我们就可以获取任意连续区间的和了,只需要pref[right] - pref[left]即可
好,那接下来滑动窗口什么呢?
查了一下分可变大小和不可变大小的,就是用双指针当滑动窗口边界,然后从左到右滑~
那思路就很清晰了
public class Solution
{
public int MinSubArrayLen(int target, int[] nums)
{
// 前缀和
int[] pref = new int[nums.Length + 1];
for (int i = 0; i < nums.Length; i++)
pref[i + 1] = pref[i] + nums[i];
// 滑动窗口
int left = 0;
int right = nums.Length - 1;
int ans = int.MaxValue;
while (right <= nums.Length - 1 && right - left >= 0)
{
if (pref[right + 1] - pref[left] >= target)
ans = right - left < ans ? right - left + 1 : ans;
if (right == nums.Length - 1)
{
right--;
right -= left;
left = 0;
continue;
}
left++;
right++;
}
return ans == int.MaxValue ? 0 : ans;
}
}
但是对于某个压力测试TLE了...
2.2 题解思路
- 方法一:暴力(C#会超时)
public class Solution
{
public int MinSubArrayLen(int target, int[] nums)
{
if (nums.Length <= 0) return 0;
int ans = int.MaxValue;
for (int i = 0; i < nums.Length; i++)
{
int sum = 0;
for (int j = i; j < nums.Length; j++)
{
sum += nums[j];
if (sum >= target)
{
ans = Math.Min(ans, j - i + 1);
break;
}
}
}
return ans == int.MaxValue ? 0 : ans;
}
}
复杂度分析
时间复杂度:O(n^2),其中 n 是数组的长度。需要遍历每个下标作为子数组的开始下标,对于每个开始下标,需要遍历其后面的下标得到长度最小的子数组。
空间复杂度:O(1)。
- 方法二:前缀和+二分查找
方法一的时间复杂度是 O(n^2),因为在确定每个子数组的开始下标后,找到长度最小的子数组需要 O(n) 的时间。如果使用二分查找,则可以将时间优化到 O(logn)。
为了使用二分查找,需要额外创建一个数组 sums 用于存储数组 nums 的前缀和,其中 sums[i] 表示从 nums[0] 到 nums[i−1] 的元素和。得到前缀和之后,对于每个开始下标 i,可通过二分查找得到大于或等于 i 的最小下标 bound,使得 sums[bound]−sums[i−1]≥s,并更新子数组的最小长度(此时子数组的长度是 bound−(i−1))。
因为这道题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分的正确性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。
在很多语言中,都有现成的库和函数来为我们实现这里二分查找大于等于某个数的第一个位置的功能,比如 C++ 的 lower_bound,Java 中的 Arrays.binarySearch,C# 中的 Array.BinarySearch,Python 中的 bisect.bisect_left。但是有时面试官可能会让我们自己实现一个这样的二分查找函数,这里给出一个 C# 的版本,供读者参考:
private int LowerBound(int[] a, int l, int r, int target)
{
int mid = -1, originL = l, originR = r;
while (l < r)
{
mid = (l + r) >> 1;
if (a[mid] < target) l = mid + 1;
else r = mid;
}
return (a[l] >= target) ? l : -1;
}
最终实现
public class Solution {
private int LowerBound(int[] a, int l, int r, int target)
{
int mid = -1, originL = l, originR = r;
while (l < r)
{
mid = (l + r) >> 1;
if (a[mid] < target) l = mid + 1;
else r = mid;
}
return (a[l] >= target) ? l : -1;
}
public int MinSubArrayLen(int s, int[] nums)
{
int n = nums.Length;
if (n == 0)
{
return 0;
}
int ans = int.MaxValue;
int[] sums = new int[n + 1];
// 为了方便计算,令 size = n + 1
// sums[0] = 0 意味着前 0 个元素的前缀和为 0
// sums[1] = A[0] 前 1 个元素的前缀和为 A[0]
// 以此类推
for (int i = 1; i <= n; ++i)
{
sums[i] = sums[i - 1] + nums[i - 1];
}
for (int i = 1; i <= n; ++i)
{
int target = s + sums[i - 1]; // 由于要让sums[bound] - sums[i - 1] >= s,移项得sums[bound] >= s + sums[i - 1],即此行
int bound = LowerBound(sums, i, n, target);
if (bound != -1)
{
ans = Math.Min(ans, bound - i + 1);
}
}
return ans == int.MaxValue ? 0 : ans;
}
}
复杂度分析
时间复杂度:O(nlogn),其中 n 是数组的长度。需要遍历每个下标作为子数组的开始下标,遍历的时间复杂度是 O(n),对于每个开始下标,需要通过二分查找得到长度最小的子数组,二分查找得时间复杂度是 O(logn),因此总时间复杂度是 O(nlogn)。
空间复杂度:O(n),其中 n 是数组的长度。额外创建数组 sums 存储前缀和。
- 方法三:滑动窗口
这里就不写思路了,官方题解的动画挺清晰的
public class Solution {
public int MinSubArrayLen(int s, int[] nums)
{
int n = nums.Length;
if (n == 0)
{
return 0;
}
int ans = int.MaxValue;
int start = 0, end = 0;
int sum = 0;
while (end < n)
{
sum += nums[end];
while (sum >= s)
{
ans = Math.Min(ans, end - start + 1);
sum -= nums[start];
++start;
}
++end;
}
return ans == int.MaxValue ? 0 : ans;
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是数组的长度。指针 start 和 end 最多各移动 n 次。
- 空间复杂度:O(1)。
2.3 优化(或者敲一遍题解?)
先来试试方法二:
对于二分查找里使用左闭右开还是左闭右闭呢?建议以后算法题都写成左闭右开吧,左闭右闭处理边界好麻烦…
/*
* @lc app=leetcode.cn id=209 lang=csharp
*
* [209] 长度最小的子数组
*/
var s = new Solution();
s.MinSubArrayLen(7, [2, 3, 1, 2, 4, 3]);
// @lc code=start
public class Solution
{
public int MinSubArrayLen(int target, int[] nums)
{
int ans = int.MaxValue;
// 前缀和
int[] pref = new int[nums.Length + 1];
for (int i = 1; i < nums.Length + 1; i++)
pref[i] = pref[i - 1] + nums[i - 1];
// 子数组起点
for (int i = 1; i <= nums.Length; i++)
{
// sums bound - sums i >= target
int boundSum = target + pref[i - 1];
int bound = BinarySearch(pref, i, nums.Length, boundSum);
if (bound != -1)
ans = Math.Min(bound - i + 1, ans);
}
return ans == int.MaxValue ? 0 : ans;
}
private int BinarySearch(int[] nums, int l, int r, int target)
{
int originL = l;
int originR = r;
while (l < r)
{
int mid = l + ((r - l) >> 1);
if (nums[mid] < target) l = mid + 1;
else r = mid;
}
return nums[l] >= target ? l : -1;
}
}
// @lc code=end
自己敲一遍~
/*
* @lc app=leetcode.cn id=209 lang=csharp
*
* [209] 长度最小的子数组
*/
var s = new Solution();
s.MinSubArrayLen(7, [2, 3, 1, 2, 4, 3]);
// @lc code=start
public class Solution
{
public int MinSubArrayLen(int target, int[] nums)
{
int n = nums.Length;
if (n == 0) return 0;
int ans = int.MaxValue;
int start = 0;
int end = 0;
int sum = 0;
while (end < n)
{
sum += nums[end];
while (sum >= target)
{
ans = Math.Min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == int.MaxValue ? 0 : ans;
}
}
// @lc code=end
3. 补充知识と杂谈,总结
以后写算法还是严格遵守左闭右开吧

浙公网安备 33010602011771号