1. 问题背景与定义
区间子数组个数问题是一类经典的算法题型,通常给定一个数组和特定条件,要求统计满足条件的连续子数组的数量。这类问题在实际工程中有广泛应用,比如数据分析中的滑动窗口统计、时序数据处理等场景。
双指针技巧(Two Pointers)是解决这类问题的利器。不同于暴力解法O(n²)的时间复杂度,双指针通常能在O(n)时间内解决问题,尤其适合处理"连续子数组/子串"类问题。其核心思想是通过维护两个指针的动态移动,高效地缩小搜索范围。
2. 问题具体描述与示例
给定一个整数数组nums和两个整数left、right,要求统计满足以下条件的连续子数组的个数:
- 子数组中的最大值在[left, right]区间内
- 子数组必须连续
示例:
输入:nums = [2,1,4,3], left = 2, right = 3
输出:3
解释:满足条件的子数组为[2], [2,1], [3]
3. 解题思路分析
3.1 暴力解法及其缺陷
最直观的解法是枚举所有可能的子数组,然后检查每个子数组的最大值是否在给定范围内。这种方法需要O(n²)的时间复杂度,对于大规模数据显然不够高效。
python复制# 暴力解法示例
def countSubarrays(nums, left, right):
count = 0
n = len(nums)
for i in range(n):
current_max = -float('inf')
for j in range(i, n):
current_max = max(current_max, nums[j])
if left <= current_max <= right:
count += 1
return count
3.2 双指针优化思路
我们可以观察到以下关键性质:
- 如果一个子数组的最大值大于right,那么包含它的任何更大子数组都不满足条件
- 如果一个子数组的最大值小于left,它本身不满足条件,但可能被包含在更大的满足条件的子数组中
基于此,我们可以维护三个指针:
- i:当前处理的子数组起始位置
- j:当前处理的子数组结束位置
- k:最后一个最大值超过right的位置
4. 双指针解法实现
4.1 算法步骤详解
- 初始化count=0, last_overflow=-1, valid_start=0
- 遍历数组,对于每个元素nums[i]:
a. 如果nums[i] > right:- 更新last_overflow = i
- 重置valid_start = i + 1
b. 如果nums[i] >= left: - 更新valid_start = i + 1
c. 计算当前有效子数组数:count += (i - last_overflow) - (i - valid_start + 1)
4.2 完整代码实现
python复制def countSubarrays(nums, left, right):
count = 0
last_overflow = -1 # 记录最近一个超过right的元素位置
valid_start = 0 # 记录最近一个在[left,right]范围内的元素位置
for i in range(len(nums)):
if nums[i] > right:
last_overflow = i
valid_start = i + 1
elif nums[i] >= left:
valid_start = i + 1
count += (i - last_overflow) - (i - valid_start + 1)
return count
4.3 复杂度分析
时间复杂度:O(n),只需一次遍历数组
空间复杂度:O(1),只使用了常数个额外变量
5. 算法正确性证明
我们可以通过数学归纳法证明该算法的正确性:
- 基本情况:当数组为空时,返回0,正确
- 归纳假设:假设对于前k个元素的处理是正确的
- 归纳步骤:
- 当nums[k] > right时,所有包含它的子数组都不合法,更新last_overflow
- 当nums[k] < left时,它可能被包含在更大的合法子数组中
- 当nums[k]在[left,right]时,它本身就是一个合法子数组
每次迭代中,(i - last_overflow)计算所有可能的子数组数,(i - valid_start + 1)减去那些最大值小于left的子数组数。
6. 边界条件与特殊测试用例
6.1 边界情况处理
- 空数组:应返回0
- 所有元素都小于left:应返回0
- 所有元素都在[left,right]之间:应返回n*(n+1)/2
- 所有元素都大于right:应返回0
6.2 测试用例设计
python复制test_cases = [
([], 2, 3, 0), # 空数组
([1,1,1], 2, 3, 0), # 全部小于left
([2,2,2], 2, 3, 6), # 全部在范围内
([4,4,4], 2, 3, 0), # 全部大于right
([2,1,4,3], 2, 3, 3), # 示例用例
([1,2,3,4,3,2,1], 2, 3, 12), # 混合情况
([5,1,2,5,3,5], 2, 3, 6) # 包含多个超过right的元素
]
7. 算法优化与变种
7.1 类似问题扩展
- 统计最大值刚好等于k的子数组数量
- 统计最小值在给定范围内的子数组数量
- 同时考虑最大值和最小值范围的子数组统计
7.2 性能优化技巧
- 提前终止:当剩余元素数量不足以形成满足条件的子数组时,可以提前结束
- 并行处理:对于超大数组,可以考虑分段处理
- 预处理:对于多次查询的情况,可以预处理最大值/最小值信息
8. 实际应用场景
- 金融数据分析:统计股票价格在某个区间内的连续时间段
- 网络流量监控:识别流量在正常范围内的连续时间段
- 传感器数据处理:找出传感器读数在安全范围内的连续区间
9. 常见错误与调试技巧
9.1 常见错误类型
- 指针更新逻辑错误:特别是在处理边界条件时
- 计数公式错误:容易混淆包含和不包含的条件
- 初始化值错误:last_overflow和valid_start的初始值设置不当
9.2 调试建议
- 使用小规模测试用例手动验证
- 打印指针位置和中间计算结果
- 对比暴力解法的结果进行验证
调试技巧:对于复杂的指针移动问题,可以在每次迭代时打印指针位置和关键变量值,这能帮助快速定位逻辑错误。
10. 扩展思考
- 如果将问题改为统计所有元素都在[left,right]范围内的子数组数量,该如何修改算法?
- 如果要求同时满足最大值和最小值的范围限制,算法该如何调整?
- 如果数组非常大,无法一次性装入内存,该如何处理?
这类问题体现了双指针技巧在处理连续子数组问题上的强大能力。掌握这种技巧不仅能解决特定问题,更能培养对数组遍历和区间统计的深刻理解。在实际编码面试中,类似的变种问题经常出现,建议通过大量练习来熟练掌握。