子数组系列问题
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 类似,但语法和部分数据结构(如 Deque
、HashMap
)的使用方式有所不同。
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) |
滑动窗口最大值 | 单调队列(Deque ) | O(n) |
子数组和等于 K | 前缀和 + HashMap | O(n) |
最长无重复子串 | 滑动窗口 + HashMap | O(n) |
乘积小于 K 的子数组 | 滑动窗口 + 累积乘积 | O(n) |
关键点:
- 滑动窗口:适用于连续子数组问题(如最大值、无重复字符)。
- 前缀和:用于快速计算子数组和。
- 哈希表:存储中间结果(如前缀和、字符索引)。
看完理解可以尝试刷题巩固!