每日算法 — 使用java实现队列:循环队列与滑动窗口最大值

队列(Queue)是一种先进先出(FIFO)的线性数据结构,与栈的 LIFO 特性形成鲜明对比。队列广泛应用于 BFS 广度优先搜索、任务调度、消息传递等场景。本文将从零实现一个基于数组的循环队列,解决普通顺序队列的”假溢出”问题,并重点讲解单调队列技巧——在 O(n) 时间内求解滑动窗口最大值这一经典问题。

数据结构原理

普通顺序队列使用头尾指针管理元素,但当元素出队后,前面的空间无法复用,导致”假溢出”。循环队列通过取模运算将数组逻辑上首尾相连,充分利用每一寸空间。

单调队列是双端队列(Deque)的进阶应用,维护队列内元素的单调递减(或递增)性。通过从队尾移除破坏单调性的元素、从队头移除滑出窗口的元素,保证队首始终是当前窗口的最大值。

Java 实现

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;

/**
 * 队列完整实现:循环队列与单调队列高级应用
 */
public class QueueDemo {

    /**
     * 基于数组的循环队列实现
     * 解决顺序队列的假溢出问题
     */
    static class CircularQueue {
        private int[] data;
        private int head; // 队首下标
        private int tail; // 队尾下一个位置下标
        private int size; // 当前元素个数
        private int capacity;

        public CircularQueue(int capacity) {
            this.capacity = capacity;
            this.data = new int[capacity];
            this.head = 0;
            this.tail = 0;
            this.size = 0;
        }

        /**
         * 入队操作
         *
         * @param val 待入队元素
         * @return true 若入队成功
         */
        public boolean enqueue(int val) {
            if (isFull()) {
                System.out.println("队列已满");
                return false;
            }
            data[tail] = val;
            tail = (tail + 1) % capacity; // 循环移动
            size++;
            return true;
        }

        /**
         * 出队操作
         *
         * @return 队首元素
         */
        public int dequeue() {
            if (isEmpty()) {
                throw new RuntimeException("队列为空");
            }
            int val = data[head];
            head = (head + 1) % capacity; // 循环移动
            size--;
            return val;
        }

        /**
         * 查看队首元素
         */
        public int front() {
            if (isEmpty()) {
                throw new RuntimeException("队列为空");
            }
            return data[head];
        }

        /**
         * 查看队尾元素
         */
        public int rear() {
            if (isEmpty()) {
                throw new RuntimeException("队列为空");
            }
            // tail 指向下一个位置,所以队尾在 tail 前一个位置
            return data[(tail - 1 + capacity) % capacity];
        }

        public boolean isEmpty() {
            return size == 0;
        }

        public boolean isFull() {
            return size == capacity;
        }

        public int size() {
            return size;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < size; i++) {
                sb.append(data[(head + i) % capacity]);
                if (i < size - 1) sb.append(", ");
            }
            sb.append("]");
            return sb.toString();
        }
    }

    /**
     * 单调队列应用:滑动窗口最大值
     * 给定数组 nums 和窗口大小 k,返回每个滑动窗口中的最大值
     *
     * 核心思想:维护一个双端队列,队列中保存下标,对应值单调递减
     * 1. 入队时,从队尾移除所有值小于等于新元素的旧元素(它们不可能成为最大值)
     * 2. 队首元素若已滑出窗口,则移除
     * 3. 队首即为当前窗口最大值
     *
     * @param nums 输入数组
     * @param k    窗口大小
     * @return 每个窗口的最大值
     */
    public static int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return new int[0];
        }

        int n = nums.length;
        int[] result = new int[n - k + 1];
        Deque<Integer> deque = new ArrayDeque<>(); // 存放下标,对应值单调递减

        for (int i = 0; i < n; i++) {
            // 移除已滑出窗口的队首元素
            if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.pollFirst();
            }

            // 从队尾移除所有小于等于当前元素的值,维护单调递减
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            }

            deque.offerLast(i); // 当前元素入队

            // 窗口已形成,记录最大值(队首)
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }

        return result;
    }

    /**
     * 单调队列应用:滑动窗口最小值
     *
     * @param nums 输入数组
     * @param k    窗口大小
     * @return 每个窗口的最小值
     */
    public static int[] minSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] result = new int[n - k + 1];
        Deque<Integer> deque = new ArrayDeque<>(); // 存放下标,对应值单调递增

        for (int i = 0; i < n; i++) {
            if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.pollFirst();
            }
            while (!deque.isEmpty() && nums[deque.peekLast()] >= nums[i]) {
                deque.pollLast();
            }
            deque.offerLast(i);
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        return result;
    }

    /**
     * 单调队列应用:求数组中每个元素作为最大值时的子数组个数
     * 结合单调栈求左右边界,再用单调队列思想理解
     */
    public static long subArrayRangesSum(int[] nums) {
        int n = nums.length;
        long sum = 0;
        // 对每个元素,计算以它为最大值的所有子数组个数
        // 左边界:前一个更大元素的位置;右边界:后一个更大或等于元素的位置
        Deque<Integer> stack = new ArrayDeque<>();
        int[] left = new int[n];
        int[] right = new int[n];

        // 左边界:严格大于
        for (int i = 0; i < n; i++) {
            while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
                stack.pop();
            }
            left[i] = stack.isEmpty() ? -1 : stack.peek();
            stack.push(i);
        }

        stack.clear();
        // 右边界:大于或等于(避免重复计数)
        for (int i = n - 1; i >= 0; i--) {
            while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
                stack.pop();
            }
            right[i] = stack.isEmpty() ? n : stack.peek();
            stack.push(i);
        }

        for (int i = 0; i < n; i++) {
            long count = (long) (i - left[i]) * (right[i] - i);
            sum += nums[i] * count;
        }
        return sum;
    }

    public static void main(String[] args) {
        // 测试循环队列
        CircularQueue cq = new CircularQueue(5);
        cq.enqueue(10);
        cq.enqueue(20);
        cq.enqueue(30);
        System.out.println("循环队列: " + cq);
        System.out.println("出队: " + cq.dequeue());
        cq.enqueue(40);
        cq.enqueue(50);
        cq.enqueue(60);
        System.out.println("满队列: " + cq);
        System.out.println("队首: " + cq.front() + ", 队尾: " + cq.rear());

        // 测试滑动窗口最大值
        int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
        int k = 3;
        System.out.println("\n原始数组: " + Arrays.toString(nums));
        System.out.println("窗口大小: " + k);
        System.out.println("滑动窗口最大值: " + Arrays.toString(maxSlidingWindow(nums, k)));
        System.out.println("滑动窗口最小值: " + Arrays.toString(minSlidingWindow(nums, k)));

        // 测试子数组范围之和
        int[] arr = {1, 2, 3};
        System.out.println("\n子数组范围之和: " + subArrayRangesSum(arr));
    }
}

复杂度分析

操作/算法 时间复杂度 空间复杂度
循环队列 enqueue / dequeue O(1) O(1)
滑动窗口最大值 O(n) O(k)
滑动窗口最小值 O(n) O(k)
子数组范围之和 O(n) O(n)

单调队列的精髓在于:每个元素最多入队和出队各一次,因此整体时间复杂度为线性。这种”每个元素只被处理有限次”的思想是 amortized analysis(摊还分析)的典型应用。

适用场景

循环队列适用于固定缓冲区、生产者-消费者模型、以及任何需要 FIFO 且空间受限的场景。单调队列则广泛应用于:滑动窗口极值问题;股票价格跨度;接雨水问题的双单调栈/队列解法;以及需要维护窗口内元素单调性的各类优化问题。掌握这两种结构,是处理线性数据流问题的重要武器。