滑动窗口进阶:统计满足条件的子数组数量

滑动窗口进阶:统计满足条件的子数组数量

在上一篇文章中,我们学习了如何使用单调队列解决滑动窗口最大值问题。今天我们要挑战一个更复杂的题目:统计满足特定条件的子数组数量。这个问题不仅考察了滑动窗口的应用,还涉及了双指针和单调队列的巧妙结合。

问题描述

给定一个整数数组 arr 和一个整数 num,找出所有满足条件的子数组数量。子数组 sub 达标的条件是:

sub中的最大值 - sub中的最小值 <= num

示例:

  • 输入:arr = [1, 2, 3, 4], num = 2
  • 输出:8
  • 解释:满足条件的子数组有 [1], [2], [3], [4], [1,2], [2,3], [3,4], [2,3,4]

暴力解法分析

最直观的方法是枚举所有可能的子数组,然后检查每个子数组是否满足条件。

// 暴力解法(时间复杂度 O(n³))
public static int right(int[] arr, int num) {
    if (arr == null || arr.length == 0 || num < 0) {
        return 0;
    }
    int count = 0;
    int n = arr.length;
    
    // 枚举所有子数组
    for (int left = 0; left < n; left++) {
        for (int right = left; right < n; right++) {
            // 找出当前子数组的最大值和最小值
            int max = arr[left];
            int min = arr[left];
            for (int i = left + 1; i <= right; i++) {
                max = Math.max(max, arr[i]);
                min = Math.min(min, arr[i]);
            }
            // 检查是否满足条件
            if (max - min <= num) {
                count++;
            }
        }
    }
    return count;
}

问题: 时间复杂度为 O(n³),对于大数组来说效率极低。

优化思路:滑动窗口 + 单调队列

观察发现,如果我们固定左边界 L,那么随着右边界 R 的增大:

  • 子数组的最大值不会减小
  • 子数组的最小值不会增大
  • 因此 (最大值 - 最小值) 不会减小

这意味着:如果某个窗口 [L, R] 不满足条件,那么所有更大的窗口 [L, R+1], [L, R+2], ... 也都不满足条件。

核心思想

对于每个左边界 L,找到最大的右边界 R,使得窗口 [L, R-1] 满足条件,而窗口 [L, R] 不满足条件。

这样,以 L 为左边界的所有满足条件的子数组数量就是 R - L

数据结构选择

我们需要高效地维护窗口内的最大值和最小值,这正是单调队列的用武之地:

  • 最大值队列:单调递减队列,队首是最大值
  • 最小值队列:单调递增队列,队首是最小值

算法步骤详解

步骤1:初始化

  • 使用两个双端队列分别维护最大值和最小值的索引
  • 使用双指针 L(左边界)和 R(右边界)

步骤2:扩展右边界

对于每个固定的 L,尽可能向右扩展 R

while (R < N) {
    // 维护最大值单调递减队列
    while (!maxQueue.isEmpty() && arr[maxQueue.peekLast()] <= arr[R]) {
        maxQueue.pollLast();
    }
    maxQueue.addLast(R);
    
    // 维护最小值单调递增队列  
    while (!minQueue.isEmpty() && arr[minQueue.peekLast()] >= arr[R]) {
        minQueue.pollLast();
    }
    minQueue.addLast(R);
    
    // 检查当前窗口是否满足条件
    if (arr[maxQueue.peekFirst()] - arr[minQueue.peekFirst()] > num) {
        break; // 不满足条件,停止扩展
    }
    R++; // 满足条件,继续扩展
}

步骤3:统计结果并收缩左边界

// 以L为左边界的所有满足条件的子数组数量
count += R - L;

// 移除L位置的元素(如果它在队列头部)
if (maxQueue.peekFirst() == L) {
    maxQueue.pollFirst();
}
if (minQueue.peekFirst() == L) {
    minQueue.pollFirst();
}

完整代码实现

package com.fjd.windows;

import java.util.LinkedList;

/**
 * 统计满足条件的子数组数量
 * 条件:子数组中最大值 - 最小值 <= num
 */
public class AllLessNumSubArray {

    /**
     * 使用滑动窗口 + 单调队列优化算法
     * 时间复杂度:O(n)
     * 空间复杂度:O(n)
     */
    public static int countValidSubarrays(int[] arr, int num) {
        // 边界条件检查
        if (arr == null || arr.length == 0 || num < 0) {
            return 0;
        }
        
        // 单调递减队列:维护窗口内最大值的索引
        LinkedList<Integer> maxQueue = new LinkedList<>();
        // 单调递增队列:维护窗口内最小值的索引  
        LinkedList<Integer> minQueue = new LinkedList<>();
        
        int validCount = 0; // 满足条件的子数组数量
        int right = 0;      // 右边界指针
        int n = arr.length; // 数组长度
        
        // 遍历每个可能的左边界
        for (int left = 0; left < n; left++) {
            
            // 尝试尽可能扩展右边界
            while (right < n) {
                // 维护最大值单调递减队列
                while (!maxQueue.isEmpty() && 
                       arr[maxQueue.peekLast()] <= arr[right]) {
                    maxQueue.pollLast();
                }
                maxQueue.addLast(right);
                
                // 维护最小值单调递增队列
                while (!minQueue.isEmpty() && 
                       arr[minQueue.peekLast()] >= arr[right]) {
                    minQueue.pollLast();
                }
                minQueue.addLast(right);
                
                // 检查当前窗口 [left, right] 是否满足条件
                int currentMax = arr[maxQueue.peekFirst()];
                int currentMin = arr[minQueue.peekFirst()];
                
                if (currentMax - currentMin > num) {
                    // 不满足条件,停止扩展右边界
                    break;
                }
                right++; // 满足条件,继续向右扩展
            }
            
            // 以left为左边界的所有满足条件的子数组数量为 (right - left)
            validCount += right - left;
            
            // 收缩左边界:移除left位置的元素(如果在队列头部)
            if (!maxQueue.isEmpty() && maxQueue.peekFirst() == left) {
                maxQueue.pollFirst();
            }
            if (!minQueue.isEmpty() && minQueue.peekFirst() == left) {
                minQueue.pollFirst();
            }
        }
        
        return validCount;
    }

    /**
     * 暴力解法(用于对数器验证)
     * 时间复杂度:O(n³)
     */
    public static int bruteForceCount(int[] arr, int num) {
        if (arr == null || arr.length == 0 || num < 0) {
            return 0;
        }
        
        int count = 0;
        int n = arr.length;
        
        // 枚举所有子数组
        for (int left = 0; left < n; left++) {
            for (int right = left; right < n; right++) {
                // 计算当前子数组的最大值和最小值
                int maxVal = arr[left];
                int minVal = arr[left];
                
                for (int i = left + 1; i <= right; i++) {
                    maxVal = Math.max(maxVal, arr[i]);
                    minVal = Math.min(minVal, arr[i]);
                }
                
                // 检查是否满足条件
                if (maxVal - minVal <= num) {
                    count++;
                }
            }
        }
        return count;
    }

    // ==================== 测试工具方法 ====================
    
    /**
     * 生成随机测试数组
     */
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        int size = (int) (Math.random() * (maxSize + 1));
        int[] arr = new int[size];
        for (int i = 0; i < size; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    /**
     * 打印数组(用于调试)
     */
    public static void printArray(int[] arr) {
        if (arr == null) {
            System.out.println("null");
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    /**
     * 主函数:运行对数器测试
     */
    public static void main(String[] args) {
        int maxLen = 100;
        int maxValue = 200;
        int testTime = 100000;
        
        System.out.println("开始对数器测试...");
        boolean success = true;
        
        for (int i = 0; i < testTime; i++) {
            int[] arr = generateRandomArray(maxLen, maxValue);
            int num = (int) (Math.random() * (maxValue + 1));
            
            int expected = bruteForceCount(arr, num);
            int actual = countValidSubarrays(arr, num);
            
            if (expected != actual) {
                System.out.println("测试失败!");
                System.out.println("数组:");
                printArray(arr);
                System.out.println("num = " + num);
                System.out.println("期望结果: " + expected);
                System.out.println("实际结果: " + actual);
                success = false;
                break;
            }
        }
        
        if (success) {
            System.out.println("所有测试通过!");
        }
    }
}

算法复杂度分析

时间复杂度:O(n)

  • 每个元素最多入队一次,出队一次
  • 左右指针各遍历数组一次
  • 总体时间复杂度为线性

空间复杂度:O(n)

  • 两个队列最多存储 n 个元素
  • 其他变量占用常数空间

相比暴力解法的 O(n³),这是一个巨大的优化!

执行过程演示

arr = [1, 2, 3, 4], num = 2 为例:

leftright范围满足条件的子数组数量
0[0, 2)[1], [1,2]2
1[1, 4)[2], [2,3], [2,3,4]3
2[2, 4)[3], [3,4]2
3[3, 4)[4]1

总计:2 + 3 + 2 + 1 = 8

关键要点总结

  1. 单调性利用:窗口扩展时,最大值不减,最小值不增
  2. 双队列维护:一个维护最大值,一个维护最小值
  3. 双指针技巧:右指针不回退,保证线性时间复杂度
  4. 边界处理:注意队列为空的情况和索引越界

扩展应用

这种思想可以应用到更多类似问题:

  • 统计满足 max - min >= k 的子数组数量
  • 统计满足其他极值条件的子数组
  • 结合其他数据结构解决更复杂的窗口问题

掌握滑动窗口和单调队列的组合使用,会让你在解决数组相关问题时更加得心应手!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值