数据结构与算法学习笔记(训练营一)---单调栈和滑动窗口

滑动窗口是什么?

  • 滑动窗口是一种想象出来的数据结构。
  • 滑动窗口有左边界L和有边界R。
  • 在数组或者字符串或者一个序列上,记为S,窗口就是S[L..R]这一部分。
  • L往右滑意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口
    L和R都只能往右滑。

滑动窗口能做什么?

  • 滑动窗口、首尾指针等技巧,说白了是一种求解问题的流程设计。

滑动内最大值和最小值的更新结构

  • 窗口不管L还是R滑动之后,都会让窗口呈现新状况,
  • 如何能够更快的得到窗口当前状况下的最大值和最小值?
  • 最好平均下来复杂度能做到O(1)
  • 利用单调双端队列!

例题

  • 假设一个固定大小为W的窗口,依次划过arr,返回每一次滑出状况的最大值。例如,arr = [4,3,5,4,3,3,6,7], W = 3,返回:[5,5,5,4,6,7]
/**
 * 求滑动窗口的最大值和最小值流程(L,R分别是窗口的边界)
 * 

* 求窗口内最大值(利用双端队列实现) * 1.滑动窗口R向右移动时,窗口增加,如果此时队列为空那么直接从尾部加入队列,如果队列不为空 * 那么比较队列尾部元素和当前元素大小,如果当前元素比队列尾部元素小那么从尾部加入当前元素, * 如果当前元素比队列尾部元素大于等于那么,那么弹出队列尾部元素,继续比较当前元素和队列尾部元素, * 知道队列尾部元素大于当前元素。此时队头元素就是此时形成的窗口的最大值。 * 2.滑动窗口L向右移动时,窗口减小。比较此时移除窗口的元素是否是队列中的元素如不不是,什么也不用做 * 如果窗口移出元素刚好等于队列头部元素,那么从队列头部移出改元素,此时队列中的新头部元素就是此时 * 窗口的最大值。 *

* 假设一个固定大小为W的窗口,依次划过arr, * 返回每一次滑出状况的最大值 * 例如,arr = [4,3,5,4,3,3,6,7], W = 3 * 返回:[5,5,5,4,6,7] */ public class SlidingWindow { //形成一个窗口为3的滑动窗口,每次求出最大值,最后返回 public static int[] slidingWindowsMax(int[] arr, int w) { if (arr == null || arr.length == 0 || w <= 0 || arr.length - w < -1) { return null; } int len = arr.length; int[] res = new int[len - w +1]; int index = 0; LinkedList queueMax = new LinkedList<>(); for (int R = 0; R < len; R++) { // 加入双端队列 while (!queueMax.isEmpty() && arr[R] >= arr[queueMax.peekLast()]) { queueMax.pollLast(); } queueMax.offerLast(R); if((R - w >=0) &&(R - w) == queueMax.peekFirst()){ queueMax.pollFirst(); } if(R - w >= -1){ // 形成了w窗口,取出最大值 res[index++] = arr[queueMax.peekFirst()]; } } return res; } public static void main(String[] args) { // [5,5,5,4,6,7] int[] arr = new int[]{4,2,3,3,5,1,1,1,1,8}; int w = 3; int[] ints = slidingWindowsMax(arr, w); for(int i : ints){ System.out.print(i+" "); } } }

  • 滑动窗口内最小值:
    1.滑动窗口R向右移动,如果队列为空,直接加入队列,否则比较队尾元素和当前元素,若当前元素大于等于队尾元素则加入,否则弹出队尾元素再次进行比较直到当前元素大于等于队尾元素,加入当前元素。
    2.滑动窗口L向右移动,若当前元素不等于队列中的队头元素那么不用管,此时队列中的队头元素就是此时窗口的最小值。若当前元素等于队列头部元素则删除队头元素,此时队列的新头就是此时滑动窗口的最小值。

  • 给定一个整型数组arr,和一个整数num
    某个arr中的子数组sub,如果想达标,必须满足:
    sub中最大值 – sub中最小值 <= num,
    返回arr中达标子数组的数量
/**
 * 给定一个整型数组arr,和一个整数num
 * 某个arr中的子数组sub,如果想达标,必须满足:
 * sub中最大值 – sub中最小值 <= num,
 * 返回arr中达标子数组的数量
 */
public class SubArray {
    // 首先如果在L -> R 上是达标子数组,那么在L -> R 内的的子数组都是达标的,因为在这个
    // 范围上最大值,只可能比L -> R上的小,最小值只可能比L -> R上的大,所以还是满足达标子数组的要求
    // 如果在L ->R上不满足,那么在大于L -> R的范围上也不会存在达标的子数组
    // 因为最大值只可可能更大,最小值只可能更小,那么本来就不满足,现在差距跟大了,一定不满足
    // 现在可以这样做以数组汇总每个位置为开头,一直滑动R形成一个窗口找出以当前位置为头的最大的
    // 达标的子数组,然后 R - L + 1就是以当前位置开头的达标子数组的所有。
    // 然后在换开头,遍历完所有的位置结果就出来了
    public static int subArray(int[] arr,int num){
        if(arr == null || arr.length == 0){
            return 0;
        }

        // 窗口最大值
        LinkedList maxQ = new LinkedList<>();
        // 窗口最小值
        LinkedList minQ = new LinkedList<>();
        int len = arr.length;
        int L = 0;
        int R = 0;
        int res = 0;
        while (L < len){
            while (!maxQ.isEmpty() && arr[R] >= arr[maxQ.peekLast()]){
                maxQ.pollLast();
            }
            maxQ.offerLast(R);

            while (!minQ.isEmpty() && arr[R] <= arr[minQ.peekLast()]){
                minQ.pollLast();
            }
            minQ.offerLast(R);
            // 如果此时最大值和最小值不达标,开始结算
            if(arr[maxQ.peekFirst()] - arr[minQ.peekFirst()]> num){
                res += R - L;
            }else{
                if(R == len - 1){
                    res += R - L + 1;
                    L ++;
                }else {
                    R ++;
                }
                continue;
            }
            // 结算完以后让L 前进一步,并且调整 最大最小队列
            if(maxQ.peekFirst() == L){
                maxQ.pollFirst();
            }
            if(minQ.peekFirst() == L){
                minQ.pollFirst();
            }

            L ++;

        }
        return res;
    }

    public static int subArray2(int[] arr,int num){
        if(arr == null || arr.length == 0){
            return 0;
        }

        // 窗口最大值
        LinkedList maxQ = new LinkedList<>();
        // 窗口最小值
        LinkedList minQ = new LinkedList<>();
        int len = arr.length;
        int L = 0;
        int R = 0;
        int res = 0;
        while (L < len){
            // 每个L位置尝试最大子数组
            while (R < len){
                // R不断扩大直到破坏子数组条件
                while (!maxQ.isEmpty() && arr[R] >= arr[maxQ.peekLast()]){
                    maxQ.pollLast();
                }
                maxQ.offerLast(R);

                while (!minQ.isEmpty() && arr[R] <= arr[minQ.peekLast()]){
                    minQ.pollLast();
                }
                minQ.offerLast(R);
                if(arr[maxQ.peekFirst()] - arr[minQ.peekFirst()]> num){
                    // 跳出R向右扩循环
                    break;
                }
                    R ++;
            }
            res += R - L;
            // 结算完以后让L 前进一步,并且调整 最大最小队列
            if(maxQ.peekFirst() == L){
                maxQ.pollFirst();
            }
            if(minQ.peekFirst() == L){
                minQ.pollFirst();
            }
            L ++;
        }
        return res;
    }
}

单调栈是什么?

一种特别设计的栈结构,为了解决如下的问题:
给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息
1)arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?
2)arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪?
如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快。
那么到底怎么设计呢?

/**
 * 单调栈
 * 一种特别设计的栈结构,为了解决如下的问题:
 * 给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息
 * 1)arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?
 * 2)arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪?
 * 如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快。
 */
public class MonotonousStack {

    // 数组中无重复元素
    public static int[][] monotonousStack(int[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }

        int len = arr.length;
        // 准备一个栈,存放元素下标
        Stack stack = new Stack<>();
        // 返回数组,每个元素占据一行,每一行有两列,第一列表示离当前元素最近的左边小的元素
        // 第二列表示离当前元素最近的右边小的元素
        int[][] res = new int[len][2];
        int index = 0;
        while (index < len) {
            while (!stack.isEmpty() && arr[stack.peek()] > arr[index]){
                // 收集答案,弹出栈顶元素 pop 是元素的下标
                int pop = stack.pop();
                int temp = stack.isEmpty() ? -1:stack.peek();
                res[pop][0] = temp;
                res[pop][1] = index;
            }
            stack.push(index++);

        }
        while (!stack.isEmpty()) {
            int cur = stack.pop();
            int temp = stack.isEmpty() ? -1:stack.peek();
            // 没有右边最近小的元素用-1代替
            res[cur][1] = -1;
            res[cur][0] = temp;
        }

        return res;
    }

    // 数组中有重复元素
    public static int[][] monotonousStack2(int[] arr){
        if (arr == null || arr.length == 0) {
            return null;
        }

        int len = arr.length;
        Stack> stack = new Stack<>();
        int[][] res = new int[len][2];
        int index = 0;
        while (index < len){
            while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[index]){
                List pop = stack.pop();
                for(Integer item : pop){
                    int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
                    res[item][0] = temp;
                    res[item][1] = index;
                }

            }
            if(!stack.isEmpty() && arr[stack.peek().get(0)] == arr[index]){
                stack.peek().add(index);
            }else {
                ArrayList list = new ArrayList<>();
                list.add(index);
                stack.push(list);
            }
            index ++;
        }

        while (!stack.isEmpty()) {
            List list = stack.pop();
            for(Integer item : list){
                int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
                res[item][0] = temp;
                res[item][1] = -1;
            }
        }

        return res;

    }
 
}
  • 给定一个只包含正数的数组arr,arr中任何一个子数组sub,
    一定都可以算出(sub累加和 )* (sub中的最小值)是什么,
    那么所有子数组中,这个值最大是多少?

滑动窗口、单调栈怎么用?

想用滑动窗口,要想办法把具体的问题转化为滑动窗口的处理流程
想用滑动窗口最值的更新结构,就看看处理流程下,是否需要最值这个信息
想用单调栈,要想办法把具体的问题转化为单调栈所解决的原问题
滑动窗口及其最大值和最小值的更新结构、单调栈,都是重要算法原型

  • 给定一个只包含正数的数组arr,arr中任何一个子数组sub,
    一定都可以算出(sub累加和 )* (sub中的最小值)是什么,
    那么所有子数组中,这个值最大是多少?
public class MaxSum {
    public static int maxSum(int[] arr){
        int[] sum = new int[arr.length];
        sum[0] = arr[0];
        for (int i = 1;i < arr.length ;i++){
            sum[i] = sum[i -1] + arr[i];
        }
        int[][] ints = monotonousStack2(arr);
        int max = Integer.MIN_VALUE;
        for (int i = 0;i < ints.length;i++){
            // 以当前元素为最小值的子数组
            int left = ints[i][0];
            int right = ints[i][1];
            // left 和 right 之间的和
            int he = 0;
            if(left == -1){
                if(right == -1){
                    he = sum[sum.length-1];
                }else{
                    he = sum[right - 1];
                }
            }else {
                if(right == -1){
                    he = sum[sum.length-1] - sum[left];
                }else {
                    he = sum[right -1 ] - sum[left];
                }
            }
            max = Math.max(max,he*arr[i]);
        }

        return max;
    }

    // 数组中有重复元素
    public static int[][] monotonousStack2(int[] arr){
        if (arr == null || arr.length == 0) {
            return null;
        }

        int len = arr.length;
        Stack> stack = new Stack<>();
        int[][] res = new int[len][2];
        int index = 0;
        int max = Integer.MIN_VALUE;
        while (index < len){
            while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[index]){
                List pop = stack.pop();
                for(Integer item : pop){
                    int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
                    // 左边最近最小元素
                    res[item][0] = temp;
                    // 右边最近最小元素
                    res[item][1] = index;
                }

            }
            if(!stack.isEmpty() && arr[stack.peek().get(0)] == arr[index]){
                stack.peek().add(index);
            }else {
                ArrayList list = new ArrayList<>();
                list.add(index);
                stack.push(list);
            }
            index ++;
        }

        while (!stack.isEmpty()) {
            List list = stack.pop();
            for(Integer item : list){
                int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
                res[item][0] = temp;
                res[item][1] = -1;
            }
        }

        return res;

    }

    public static int max2(int[] arr) {
        int size = arr.length;
        int[] sums = new int[size];
        sums[0] = arr[0];
        for (int i = 1; i < size; i++) {
            sums[i] = sums[i - 1] + arr[i];
        }
        int max = Integer.MIN_VALUE;
        Stack stack = new Stack();
        for (int i = 0; i < size; i++) {
            while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
                int j = stack.pop();
                max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
            }
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            int j = stack.pop();
            max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
        }
        return max;
    }
    
}

你可能感兴趣的:(数据结构与算法学习笔记(训练营一)---单调栈和滑动窗口)