1. 题目解析:区间子数组个数问题
这道题目要求我们统计数组中所有连续子数组的最大值落在给定区间内的数量。乍一看可能觉得需要枚举所有子数组然后检查最大值,但对于长度为10^5的数组来说,O(n^2)的暴力解法显然不可行。我们需要找到更聪明的解法。
1.1 问题重述与理解
给定一个整数数组nums和两个整数left、right,我们需要找出所有连续非空子数组,这些子数组的最大元素值在[left, right]范围内。例如:
对于nums = [2,1,4,3],left=2,right=3:
- [2]:最大值2 ∈ [2,3] → 符合
- [2,1]:最大值2 ∈ [2,3] → 符合
- [1]:最大值1 ∉ [2,3] → 不符合
- [4]:最大值4 ∉ [2,3] → 不符合
- [3]:最大值3 ∈ [2,3] → 符合
所以总共有3个符合条件的子数组。
1.2 暴力解法的局限性
最直观的解法是枚举所有可能的子数组,然后检查每个子数组的最大值是否在指定范围内。对于一个长度为n的数组,子数组数量为n*(n+1)/2,时间复杂度为O(n^2)。当n=10^5时,这样的解法显然会超时。
2. 双指针解法思路
2.1 核心观察
关键观察点是:我们可以将问题转化为计算"所有子数组的最大值不超过right"的数量减去"所有子数组的最大值小于left"的数量。这样就将原问题分解为两个更简单的子问题。
2.2 双指针策略
我们使用两个指针minStart和maxStart来跟踪当前子数组的起始范围:
- minStart:当前子数组可以开始的最小下标
- maxStart:当前子数组可以开始的最大下标(即最后一个满足条件的元素位置)
对于每个元素nums[i],我们分三种情况处理:
- nums[i]在[left, right]范围内:可以扩展有效子数组范围
- nums[i] < left:可以包含在子数组中,但不影响最大值
- nums[i] > right:必须排除,重置指针位置
2.3 算法流程详解
初始化:
- count = 0(记录符合条件的子数组数量)
- minStart = 0
- maxStart = -1(初始无效值)
遍历数组:
- 如果nums[i] > right:
- 重置minStart到i+1
- 重置maxStart到i(相当于无效位置)
- 如果nums[i] ∈ [left, right]:
- 更新maxStart为i
- 有效子数组数量增加maxStart - minStart + 1
- 如果nums[i] < left:
- 不更新maxStart
- 有效子数组数量增加maxStart - minStart + 1(如果maxStart有效)
3. 代码实现与逐行解析
java复制class Solution {
public int numSubarrayBoundedMax(int[] nums, int left, int right) {
int count = 0;
int minStart = 0, maxStart = -1;
for (int i = 0; i < nums.length; i++) {
if (nums[i] > right) {
minStart = i + 1;
maxStart = i;
} else if (nums[i] >= left) {
maxStart = i;
}
if (nums[i] <= right) {
count += maxStart - minStart + 1;
}
}
return count;
}
}
代码解析:
- 初始化count为0,minStart为0,maxStart为-1
- 遍历数组中的每个元素:
- 如果当前元素大于right:
- 重置minStart到i+1(因为包含当前元素的子数组都不符合条件)
- 设置maxStart为i(使maxStart - minStart + 1 = 0)
- 如果当前元素在[left, right]范围内:
- 更新maxStart为当前索引i
- 如果当前元素小于left:
- 不更新maxStart(保持最后一个有效位置)
- 如果当前元素大于right:
- 对于每个元素,只要它不大于right:
- 计算以当前元素结尾的有效子数组数量:maxStart - minStart + 1
- 累加到总count中
4. 复杂度分析与优化空间
4.1 时间复杂度
该算法只对数组进行一次遍历,每个元素处理时间是常数时间,因此总时间复杂度为O(n),完全能够处理n=10^5的规模。
4.2 空间复杂度
算法只使用了固定数量的额外变量(count、minStart、maxStart等),空间复杂度为O(1)。
4.3 可能的优化
虽然当前解法已经是最优,但可以考虑以下变体:
- 将条件判断顺序调整以提高分支预测效率
- 使用更简洁的变量名(在不影响可读性的前提下)
- 对于特定数据分布(如大量元素>right),可以提前分段处理
5. 常见问题与调试技巧
5.1 边界情况处理
- 空数组:题目保证nums非空
- 所有元素都小于left:应返回0
- 所有元素都在[left, right]范围内:应返回n*(n+1)/2
- left = right:相当于统计最大值等于特定值的子数组数量
5.2 调试技巧
- 打印minStart和maxStart的变化:
java复制System.out.println("i=" + i + ", num=" + nums[i] + ", minStart=" + minStart + ", maxStart=" + maxStart + ", count=" + count); - 使用小测试用例手动验证:
- nums = [1], left=1, right=1 → 应返回1
- nums = [1,2,3], left=2, right=3 → 应返回3
- 检查指针更新逻辑是否正确:
- 确保maxStart不会小于minStart
- 确保count不会在无效情况下增加
5.3 易错点
- 初始值设置:
- maxStart初始为-1,这样第一次计算时count += 0
- 条件判断顺序:
- 必须先处理>right的情况,因为它会重置指针
- 更新count的条件:
- 只有在nums[i] <= right时才更新count
6. 扩展思考与类似题目
6.1 类似题目推荐
- 最大子数组和(Kadane算法)
- 满足条件的子数组最小长度(滑动窗口)
- 子数组乘积小于K的数量(双指针)
6.2 算法变体
- 统计最大值在[L,R]范围内的子数组数量(本题)
- 统计最大值恰好等于K的子数组数量(设L=R=K)
- 统计最大值至少为K的子数组数量(计算总数减去最大值小于K的数量)
6.3 实际应用场景
这类算法可以应用于:
- 时间序列分析中寻找特定波动范围的子序列
- 信号处理中检测特定强度的信号段
- 金融分析中识别特定价格区间的交易时段
7. 个人解题心得
在实际解决这个问题时,我最初尝试了暴力解法,很快就意识到其不可行性。然后我开始思考如何将问题分解,发现可以将原问题转化为两个子问题的差。但实现过程中,双指针的更新逻辑让我调试了好几次。
关键突破点是意识到可以用maxStart来跟踪最后一个有效位置,而minStart来跟踪当前允许的最早起始位置。这种"滑动窗口"的变体非常巧妙,它避免了重复计算,使得算法能够在线性时间内解决问题。
对于类似的区间统计问题,我现在会首先考虑:
- 能否将问题转化为更简单的子问题
- 能否使用滑动窗口或双指针技术
- 如何维护必要的状态信息来避免重复计算
这种思维方式不仅适用于这道题,对于许多其他算法问题也同样有效。