算法中子数组问题详解,多种解法,包含对应题目!


java可以直接跳转到下面,内容一样!
本文练习题目可以对应 LeetCode 上的#53, #76, #209, #560, #713, #904题目。

子数组问题详解

子数组(Subarray)是指数组中一个或多个连续元素组成的序列。子数组问题是算法中常见的一类问题,通常涉及求和、乘积、最大/最小值、特定条件满足等操作。


1. 基本概念

  • 子数组:必须是连续的,例如 [1, 2, 3] 的子数组包括 [1], [2], [1, 2], [2, 3], [1, 2, 3],但 [1, 3] 不是子数组(因为它不连续)。
  • 子序列:可以不连续,如 [1, 3] 是子序列但不是子数组。
  • 子集:任意元素的组合,不要求顺序或连续。

2. 常见子数组问题及解法

(1) 最大/最小子数组

问题:给定一个整数数组(可能有负数),求子数组的最大和。
示例

输入: [-2, 1, -3, 4, -1, 2, 1, -5, 4]
输出: 6(对应子数组 [4, -1, 2, 1])

解法Kadane 算法(动态规划)

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
    代码
def max_subarray(nums):
    max_sum = current_sum = nums[0]
    for num in nums[1:]:
        current_sum = max(num, current_sum + num)  # 是否重新开始子数组
        max_sum = max(max_sum, current_sum)
    return max_sum

变种

  • 最小子数组和:类似,但取 min 代替 max
  • 环形子数组最大和(首尾相连):可以拆解为 max(最大子数组和, 总和 - 最小子数组和)

(2) 固定长度的子数组问题

问题:求所有长度为 k 的子数组的最大值/平均值等。
示例

输入: [1, 3, -1, -3, 5, 3, 6, 7], k = 3
输出: [3, 3, 5, 5, 6, 7](每个窗口的最大值)

解法滑动窗口(Sliding Window)

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)(或 O(n) 如果需要存储结果)
    代码(求最大值,使用单调队列优化):
from collections import deque

def max_sliding_window(nums, k):
    q = deque()
    res = []
    for i, num in enumerate(nums):
        while q and nums[q[-1]] <= num:  # 维护单调递减队列
            q.pop()
        q.append(i)
        if q[0] == i - k:  # 移除窗口外的元素
            q.popleft()
        if i >= k - 1:
            res.append(nums[q[0]])
    return res

(3) 满足条件的子数组个数

问题:统计满足某条件的子数组数量,如:

  • 和等于 k 的子数组个数。
  • 和不超过 k 的子数组个数。
  • 乘积小于 k 的子数组个数。

示例(和为 k 的子数组个数):

输入: nums = [1, 1, 1], k = 2
输出: 2([1,1] 和 [1,1])

解法:前缀和 + 哈希表

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
    代码
from collections import defaultdict

def subarray_sum(nums, k):
    prefix_sum = 0
    count = 0
    sum_map = defaultdict(int)
    sum_map[0] = 1  # 初始情况,前缀和为 0 出现 1 次
    for num in nums:
        prefix_sum += num
        if prefix_sum - k in sum_map:  # 查找是否有 prefix_sum - k 存在
            count += sum_map[prefix_sum - k]
        sum_map[prefix_sum] += 1
    return count

变种

  • 乘积小于 k 的子数组个数:滑动窗口 + 累积乘积。
  • 最长子数组满足某条件:滑动窗口或双指针。

(4) 最长无重复字符子数组

问题:给定一个字符串/数组,求最长子数组,其中元素不重复。
示例

输入: "abcabcbb"
输出: 3("abc")

解法滑动窗口 + 哈希表

  • 时间复杂度:O(n)
  • 空间复杂度:O(text{字符集大小})
    代码
def length_of_longest_substring(s):
    char_map = {}
    left = 0
    max_len = 0
    for right, char in enumerate(s):
        if char in char_map and char_map[char] >= left:  # 如果重复,移动左指针
            left = char_map[char] + 1
        char_map[char] = right
        max_len = max(max_len, right - left + 1)
    return max_len

3. 总结

问题类型典型解法时间复杂度
最大子数组和Kadane 算法O(n)
固定长度子数组极值滑动窗口 + 单调队列O(n)
子数组和等于 k前缀和 + 哈希表O(n)
最长无重复子数组滑动窗口 + 哈希表O(n)

关键技巧

  • 滑动窗口:适用于连续子数组的最值、计数问题。
  • 前缀和:用于快速计算子数组和。
  • 哈希表:存储中间结果,优化查找。

用java实现

在 Java 中,子数组问题的解决方法与 Python 类似,但语法和部分数据结构(如 DequeHashMap)的使用方式有所不同。


1. 最大子数组和(Kadane 算法)

问题:求数组中连续子数组的最大和。
示例

输入: [-2, 1, -3, 4, -1, 2, 1, -5, 4]
输出: 6(子数组 [4, -1, 2, 1]

Java 代码

public int maxSubArray(int[] nums) {
    int maxSum = nums[0], currentSum = nums[0];
    for (int i = 1; i < nums.length; i++) {
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        maxSum = Math.max(maxSum, currentSum);
    }
    return maxSum;
}

2. 滑动窗口最大值(单调队列)

问题:给定数组和窗口大小 k,返回每个窗口的最大值。
示例

输入: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
输出: [3, 3, 5, 5, 6, 7]

Java 代码

import java.util.Deque;
import java.util.LinkedList;

public int[] maxSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length == 0) return new int[0];
    Deque<Integer> deque = new LinkedList<>();
    int[] res = new int[nums.length - k + 1];
    for (int i = 0; i < nums.length; i++) {
        while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
            deque.pollLast(); // 维护单调递减队列
        }
        deque.offerLast(i);
        if (deque.peekFirst() == i - k) {
            deque.pollFirst(); // 移除窗口外的元素
        }
        if (i >= k - 1) {
            res[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    return res;
}

3. 子数组和等于 K(前缀和 + 哈希表)

问题:统计和为 k 的子数组个数。
示例

输入: nums = [1, 1, 1], k = 2
输出: 2[1,1][1,1]

Java 代码

import java.util.HashMap;
import java.util.Map;

public int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> prefixSumMap = new HashMap<>();
    prefixSumMap.put(0, 1); // 初始前缀和为 0 出现 1 次
    int prefixSum = 0, count = 0;
    for (int num : nums) {
        prefixSum += num;
        if (prefixSumMap.containsKey(prefixSum - k)) {
            count += prefixSumMap.get(prefixSum - k);
        }
        prefixSumMap.put(prefixSum, prefixSumMap.getOrDefault(prefixSum, 0) + 1);
    }
    return count;
}

4. 最长无重复字符子串(滑动窗口)

问题:求字符串中最长无重复字符的子串长度。
示例

输入: "abcabcbb"
输出: 3"abc"

Java 代码

import java.util.HashMap;
import java.util.Map;

public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> charIndexMap = new HashMap<>();
    int left = 0, maxLen = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (charIndexMap.containsKey(c) && charIndexMap.get(c) >= left) {
            left = charIndexMap.get(c) + 1; // 移动左指针
        }
        charIndexMap.put(c, right);
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

5. 乘积小于 K 的子数组(滑动窗口)

问题:统计乘积小于 k 的连续子数组个数。
示例

输入: nums = [10, 5, 2, 6], k = 100
输出: 8[10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]

Java 代码

public int numSubarrayProductLessThanK(int[] nums, int k) {
    if (k <= 1) return 0;
    int left = 0, product = 1, count = 0;
    for (int right = 0; right < nums.length; right++) {
        product *= nums[right];
        while (product >= k) {
            product /= nums[left++]; // 收缩窗口
        }
        count += right - left + 1; // 新增的子数组数量
    }
    return count;
}

总结

问题类型Java 解法时间复杂度
最大子数组和Kadane 算法O(n)
滑动窗口最大值单调队列(DequeO(n)
子数组和等于 K前缀和 + HashMapO(n)
最长无重复子串滑动窗口 + HashMapO(n)
乘积小于 K 的子数组滑动窗口 + 累积乘积O(n)

关键点

  • 滑动窗口:适用于连续子数组问题(如最大值、无重复字符)。
  • 前缀和:用于快速计算子数组和。
  • 哈希表:存储中间结果(如前缀和、字符索引)。

看完理解可以尝试刷题巩固!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木梓辛铭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值