LCR 008-长度最小的子数组

Raphael Liu Lv10

给定一个含有 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 <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

进阶:

  • 如果你已经实现 __O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

注意:本题与主站 209 题相同:https://leetcode-cn.com/problems/minimum-size-subarray-sum/

方法一:前缀和 + 二分查找

为了使用二分查找,需要额外创建一个数组 sums 用于存储数组 nums 的前缀和,其中 sums}[i] 表示从 nums}[0] 到 nums}[i-1] 的元素和。得到前缀和之后,对于每个开始下标 i,可通过二分查找得到大于或等于 i 的最小下标 bound,使得 sums}[\textit{bound}]-\text{sums}[i-1] \ge s,并更新子数组的最小长度(此时子数组的长度是 bound}-(i-1))。

因为这道题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分的正确性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。

在很多语言中,都有现成的库和函数来为我们实现这里二分查找大于等于某个数的第一个位置的功能,比如 C++lower_boundJava 中的 Arrays.binarySearchC# 中的 Array.BinarySearch,Python 中的 bisect.bisect_left。但是有时面试官可能会让我们自己实现一个这样的二分查找函数,这里给出一个 C# 的版本,供读者参考:

[lowerbound-C#]
1
2
3
4
5
6
7
8
9
10
11
12
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;
}

下面是这道题的代码。

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
}
int ans = Integer.MAX_VALUE;
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];
int bound = Arrays.binarySearch(sums, target);
if (bound < 0) {
bound = -bound - 1;
}
if (bound <= n) {
ans = Math.min(ans, bound - (i - 1));
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
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
// 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];
auto bound = lower_bound(sums.begin(), sums.end(), target);
if (bound != sums.end()) {
ans = min(ans, static_cast<int>((bound - sums.begin()) - (i - 1)));
}
}
return ans == INT_MAX ? 0 : ans;
}
};
[sol1-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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];
int bound = LowerBound(sums, i, n, target);
if (bound != -1)
{
ans = Math.Min(ans, bound - i + 1);
}
}

return ans == int.MaxValue ? 0 : ans;
}
}
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func minSubArrayLen(s int, nums []int) int {
n := len(nums)
if n == 0 {
return 0
}
ans := math.MaxInt32
sums := make([]int, n + 1)
// 为了方便计算,令 size = n + 1
// sums[0] = 0 意味着前 0 个元素的前缀和为 0
// sums[1] = A[0] 前 1 个元素的前缀和为 A[0]
// 以此类推
for i := 1; i <= n; i++ {
sums[i] = sums[i - 1] + nums[i - 1]
}
for i := 1; i <= n; i++ {
target := s + sums[i-1]
bound := sort.SearchInts(sums, target)
if bound < 0 {
bound = -bound - 1
}
if bound <= n {
ans = min(ans, bound - (i - 1))
}
}
if ans == math.MaxInt32 {
return 0
}
return ans
}

func min(x, y int) int {
if x < y {
return x
}
return y
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0

n = len(nums)
ans = n + 1
sums = [0]
for i in range(n):
sums.append(sums[-1] + nums[i])

for i in range(1, n + 1):
target = s + sums[i - 1]
bound = bisect.bisect_left(sums, target)
if bound != len(sums):
ans = min(ans, bound - (i - 1))

return 0 if ans == n + 1 else ans
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int lower_bound(int *a, int l, int r, int q) {
if (a[r] < q) return -1;
while (l < r) {
int mid = (l + r) >> 1;
if (a[mid] >= q) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
int minSubArrayLen(int s, int *nums, int numsSize) {
if (numsSize == 0) {
return 0;
}
int ans = INT_MAX;
int *sums = (int *)malloc(sizeof(int) * (numsSize + 1));
// 为了方便计算,令 size = n + 1
// sums[0] = 0 意味着前 0 个元素的前缀和为 0
// sums[1] = A[0] 前 1 个元素的前缀和为 A[0]
// 以此类推
for (int i = 1; i <= numsSize; i++) {
sums[i] = sums[i - 1] + nums[i - 1];
}
for (int i = 1; i <= numsSize; i++) {
int target = s + sums[i - 1];
int bound = lower_bound(sums, 1, numsSize, target);
if (bound != -1) {
ans = fmin(ans, bound - (i - 1));
}
}
return ans == INT_MAX ? 0 : ans;
}

复杂度分析

  • 时间复杂度:O(n \log n),其中 n 是数组的长度。需要遍历每个下标作为子数组的开始下标,遍历的时间复杂度是 O(n),对于每个开始下标,需要通过二分查找得到长度最小的子数组,二分查找得时间复杂度是 O(\log n),因此总时间复杂度是 O(n \log n)。

  • 空间复杂度:O(n),其中 n 是数组的长度。额外创建数组 sums 存储前缀和。

方法二:滑动窗口

在方法一中,每次确定子数组的开始下标,然后得到长度最小的子数组,因此时间复杂度较高。为了降低时间复杂度,可以使用滑动窗口的方法。

定义两个指针 start 和 end 分别表示子数组(滑动窗口窗口)的开始位置和结束位置,维护变量 sum 存储子数组中的元素和(即从 nums}[\textit{start}] 到 nums}[\textit{end}] 的元素和)。

初始状态下,start 和 end 都指向下标 0,sum 的值为 0。

每一轮迭代,将 nums}[end] 加到 sum,如果 sum} \ge s,则更新子数组的最小长度(此时子数组的长度是 end}-\textit{start}+1),然后将 nums}[start] 从 sum 中减去并将 start 右移,直到 sum} < s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将 end 右移。

<fig1,fig2,fig3,fig4,fig5,fig6,fig7,fig8,fig9,fig10,fig11,fig12>

[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
}
int ans = Integer.MAX_VALUE;
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 == Integer.MAX_VALUE ? 0 : ans;
}
}
[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int n = nums.size();
if (n == 0) {
return 0;
}
int ans = INT_MAX;
int start = 0, end = 0;
int sum = 0;
while (end < n) {
sum += nums[end];
while (sum >= s) {
ans = min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == INT_MAX ? 0 : ans;
}
};
[sol2-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
}
}
[sol2-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func minSubArrayLen(s int, nums []int) int {
n := len(nums)
if n == 0 {
return 0
}
ans := math.MaxInt32
start, end := 0, 0
sum := 0
for end < n {
sum += nums[end]
for sum >= s {
ans = min(ans, end - start + 1)
sum -= nums[start]
start++
}
end++
}
if ans == math.MaxInt32 {
return 0
}
return ans
}

func min(x, y int) int {
if x < y {
return x
}
return y
}
[sol2-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0

n = len(nums)
ans = n + 1
start, end = 0, 0
total = 0
while end < n:
total += nums[end]
while total >= s:
ans = min(ans, end - start + 1)
total -= nums[start]
start += 1
end += 1

return 0 if ans == n + 1 else ans
[sol2-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int minSubArrayLen(int s, int *nums, int numsSize) {
if (numsSize == 0) {
return 0;
}
int ans = INT_MAX;
int start = 0, end = 0;
int sum = 0;
while (end < numsSize) {
sum += nums[end];
while (sum >= s) {
ans = fmin(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == INT_MAX ? 0 : ans;
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。指针 start 和 end 最多各移动 n 次。

  • 空间复杂度:O(1)。

 Comments
On this page
LCR 008-长度最小的子数组