ARTS-算法-长度最小的子数组

74 阅读4分钟

题目

给定一个含有 n ****个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 ****target ****的长度最小的 子数组 [nums``l``, nums``l+1``, ..., nums``r-1``, nums``r``] ,并返回其长度 如果不存在符合条件的子数组,返回 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 <= 10``9
  • 1 <= nums.length <= 10``5
  • 1 <= nums[i] <= 10``4

进阶:

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

思路

方法一:滑动窗口法

使用滑动窗口的思想,通过两个指针 leftright 来维护一个窗口。right 指针不断向右移动,将元素加入窗口中,同时计算窗口内元素的总和。当窗口内元素总和大于等于 target 时,尝试通过移动 left 指针来缩小窗口大小,同时更新最小子数组的长度。不断重复这个过程,直到 right 指针遍历完整个数组。

JavaScript

/**
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function(target, nums) {
    let start = 0;
    let end = 0;
    let sum = 0;
    let len = nums.length;
    let ans = Infinity;

    while (end < len) {
        sum += nums[end];
        while (sum >= target) {
            ans = Math.min(ans, end -start + 1);
            sum -= nums[start];
            start++;
        }
        end++;
    }
    return ans === Infinity ? 0 : ans
};

TypeScript

function minSubArrayLen(target: number, nums: number[]): number {
    let left: number = 0;
    let res: number = Infinity;
    let subLen: number = 0;
    let sum: number = 0;
    for (let right: number = 0; right < nums.length; right++) {
        sum += nums[right];
        while (sum >= target) {
            subLen = right - left + 1;
            res = Math.min(res, subLen);
            sum -= nums[left];
            left++;
        }
    }
    return res === Infinity ? 0 : res;
};

Java

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int sum = 0;
        int result = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            while (sum >= target) {
                result = Math.min(result, right - left + 1);
                sum -= nums[left++];
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}

Python

def minSubArrayLen(target, nums):
    n = len(nums)
    # 初始化最小长度为一个较大的值
    min_len = float('inf')
    left = 0
    current_sum = 0
    for right in range(n):
        # 累加当前元素到总和
        current_sum += nums[right]
        # 当总和大于等于 target 时,尝试缩小窗口
        while current_sum >= target:
            # 更新最小长度
            min_len = min(min_len, right - left + 1)
            # 减去左指针指向的元素
            current_sum -= nums[left]
            # 左指针右移
            left += 1
    # 如果最小长度仍为初始值,说明不存在符合条件的子数组
    return min_len if min_len != float('inf') else 0

方法二:前缀和 + 二分查找(O (n log (n)) 时间复杂度)

  1. 前缀和数组
    • 构建 prefixSums,其中 prefixSums[i] 表示前 i 个元素的和(例如 prefixSums = nums+nums+nums)。
  2. 二分查找
    • 对每个起始点 i,计算 toFind = target + prefixSums[i],并在 prefixSums[i+1..n] 中查找第一个大于等于 toFind 的位置 j
    • 子数组长度为 j - i,记录最小值 minLen
  3. 边界处理
    • 若无满足条件的子数组,返回 0

此方法时间复杂度为 O(n log n) ,适用于大规模数据。

JavaScript

function minSubArrayLen(target, nums) {
    const n = nums.length;
    let minLen = Infinity;
    const prefixSums = new Array(n + 1).fill(0);
    for (let i = 0; i < n; i++) {
        prefixSums[i + 1] = prefixSums[i] + nums[i];
    }
    for (let i = 0; i <= n; i++) {
        const toFind = target + prefixSums[i];
        // 手动实现二分查找 bisect_left
        let low = i + 1, high = prefixSums.length;
        while (low < high) {
            const mid = Math.floor((low + high) / 2);
            if (prefixSums[mid] >= toFind) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        if (low <= n) {
            minLen = Math.min(minLen, low - i);
        }
    }
    return minLen === Infinity ? 0 : minLen;
}

TypeScript

function minSubArrayLen(target: number, nums: number[]): number {
    let left: number = 0;
    let res: number = Infinity;
    let subLen: number = 0;
    let sum: number = 0;
    for (let right: number = 0; right < nums.length; right++) {
        sum += nums[right];
        while (sum >= target) {
            subLen = right - left + 1;
            res = Math.min(res, subLen);
            sum -= nums[left];
            left++;
        }
    }
    return res === Infinity ? 0 : res;
};

Java

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int[] prefixSums = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefixSums[i + 1] = prefixSums[i] + nums[i];
        }
        int minLen = Integer.MAX_VALUE;
        for (int i = 0; i <= n; i++) {
            long toFind = (long) target + prefixSums[i]; // 防止整型溢出
            int j = Arrays.binarySearch(prefixSums, i + 1, n + 1, (int) toFind);
            if (j < 0) {
                j = -j - 1; // 计算插入点
            }
            if (j <= n) {
                minLen = Math.min(minLen, j - i);
            }
        }
        return minLen == Integer.MAX_VALUE ? 0 : minLen;
    }
}

Python

def minSubArrayLen(target, nums):
    n = len(nums)
    # 初始化最小长度为一个较大的值
    min_len = float('inf')
    # 计算前缀和数组
    prefix_sum = [0] * (n + 1)
    for i in range(1, n + 1):
        prefix_sum[i] = prefix_sum[i - 1] + nums[i - 1]

    for i in range(n + 1):
        # 二分查找满足条件的最小位置
        left, right = i + 1, n + 1
        while left < right:
            mid = (left + right) // 2
            if prefix_sum[mid] - prefix_sum[i] >= target:
                right = mid
            else:
                left = mid + 1
        # 如果找到满足条件的位置,更新最小长度
        if left <= n and prefix_sum[left] - prefix_sum[i] >= target:
            min_len = min(min_len, left - i)

    # 如果最小长度仍为初始值,说明不存在符合条件的子数组
    return min_len if min_len != float('inf') else 0

总结

  • 滑动窗口法:适用于元素均为正数的场景,时间复杂度最优(O(n))。
  • 前缀和 + 二分查找:通用性更强,时间复杂度为 O(n log n),适用于更大数据规模或需要兼容其他逻辑的场景。二分查找思想真好用,好多算法都用的到

在leetcode提交上的效率

看提交记录图,js和ts性能方面好像没区别,不过消耗内存方面,ts更小