import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.PriorityQueue;

public class SlidingWindowMaximum {
    //方法一 :暴力法 遍历每一个窗口 对每个窗口中的每个元素求最大值
    public int[] maxSlidingWindow(int[] nums,int k){
        //定义一个结果数组  总共有n-k+1个数组
        int[] result = new int[nums.length-k+1];

        //遍历数组,从0到n-k,作为滑动窗口的起始位置
        for (int i = 0; i <=nums.length-k ; i++) {
            //找窗口内的最大值 定义一个变量来保存
            int max = nums[i];
            //遍历窗口中的每一个元素,比较大小
            for (int j = i+1; j <i+k ; j++) {
                if(max<nums[j]){
                    max = nums[j];
                }
            }
            result[i] = max;
        }
        return result;
    }
    //方法二:使用大顶堆
    public int[] maxSlidingWindow2(int[] nums,int k){
        //定义一个结果数组  总共有n-k+1个数组
        int[] result = new int[nums.length-k+1];

        //用优先队列实现一个大顶堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });

        //准备工作 构建大顶堆 将第一个窗口元素(前k个)放入堆中
        for (int i = 0; i <k ; i++) {
            maxHeap.add(nums[i]);
        }

        //当前大顶堆的堆顶元素就是第一个窗口的最大值
        result[0] = maxHeap.peek();

        //遍历所有的窗口
        for (int i = 1; i <=nums.length-k; i++) {
            maxHeap.remove(nums[i-1]);//删除堆中
            maxHeap.add(nums[i+k-1]);//添加当前窗口中最后一个元素进堆
            result[i] = maxHeap.peek();
        }

        return result;
    }

    // 方法三:使用双向队列
    public int[] maxSlidingWindow3(int[] nums, int k){
        // 定义一个结果数组
        int[] result = new int[nums.length - k + 1];

        // 定义双向队列,保存元素的索引
        ArrayDeque<Integer> deque = new ArrayDeque<>();

        // 初始化双向队列,处理第一个窗口的数据
        for (int i = 0; i < k; i++){
            // 如果队尾元素小于当前元素,直接删除
            while (!deque.isEmpty() && nums[i] > nums[deque.getLast()])
                deque.removeLast();
            deque.addLast(i);
        }

        result[0] = nums[deque.getFirst()];    // 第一个窗口的最大值

        // 遍历数组所有元素,作为窗口的结束位置
        for (int i = k; i < nums.length; i++){
            // 判断如果上一个窗口删掉的就是窗口最大值,那么需要将队列中的最大值删掉
            if (!deque.isEmpty() && deque.getFirst() == i - k)
                deque.removeFirst();

            // 判断新增元素是否可以删除队尾元素
            // 如果队尾元素小于当前元素,直接删除
            while (!deque.isEmpty() && nums[i] > nums[deque.getLast()])
                deque.removeLast();
            deque.addLast(i);

            // 保存结果
            result[i - k + 1] = nums[deque.getFirst()];
        }

        return result;
    }

    //方法四:左右扫描
    public int[] maxSlidingWindow4(int[] nums, int k){
        int n = nums.length;
        // 定义一个结果数组
        int[] result = new int[n - k + 1];

        //定义存放块内最大值的left和right数组
        int[] left =  new int[n];
        int[] right = new int[n];

        //遍历数组左右扫描
        for (int i = 0; i <n ; i++) {
            //1.从左到右扫描
            if(i%k==0){
                //如果能整除k,就是块的起始位置
                left[i] = nums[i];
            }else {
                //如果不是起始位置,就直接跟left[i-1]取最大值即可
                left[i] = Math.max(left[i-1],nums[i]);
            }
            //2.从右到左扫描
            int j = n-1-i;//j就是倒数的i
            if(j%k==k-1||j==n-1){
                //就是块的起始位置
                right[j] = nums[j];
            }else {
                //如果不是起始位置,就直接跟left[i-1]取最大值即可
                right[j] = Math.max(right[j+1],nums[j]);
            }
        }
        //对每个窗口计算最大值
        for (int i = 0; i < n-k+1; i++) {
            result[i] = Math.max(right[i],left[i+k-1]);
        }
        return result;
    }

    public static void main(String[] args) {
        int[] input = {1,3,-1,-3,5,3,6,7};
        int k = 3;
        SlidingWindowMaximum slidingWindowMaximum = new SlidingWindowMaximum();
        int[] output = slidingWindowMaximum.maxSlidingWindow4(input, k);
        for (int max:output) {
            System.out.println(max);
        }
    }
}