单调栈和窗口及其更新结构
滑动窗口
- 滑动窗口是一种想象出来的数据结构
- 有左边界L和有边界R
- 在数组或字符串或者一个序列上,记为S,窗口就是S[L...R]这一部分
- L往右滑以为着一个样本出了窗口,R往右滑意味着一个样本进了窗口
- L和R都是只能往右滑
作用
滑动窗口首位指针等技巧,是一种求解问题的流程设计
滑动窗口内最大值和最小值的更新结构
- 窗口不管L还是R滑动之后,都会让窗口呈现新状况
- 利用单调双端队列(LinkedList)
- 窗口内是从左到右变小的,头部位置的数代表整个窗口的最大值
- 双端队列不让R动只让L往右动表示哪些数会依次成为最大值的优先级
栗子1:
假设一个固定大小为W的窗口,依次划过arr。返回每一次滑出状况的最大值。arr={4,3,5,4,3,3,6,7},w=3,返回[5,5,5,4,6,7]
public static int[] maxSlidingWindow(int[] nums, int k) { if(nums.length < k || nums == null) { return null; } //nums.length - k + 1表示要返回的长度 int[] res = new int[nums.length - k + 1]; int index = 0; //双端队列,存放窗口的下标 LinkedList<Integer> qmax = new LinkedList<>(); for(int R = 0; R < nums.length ; R ++) { //R从右边弹出与添加的条件。弹出:双端队列中的值小于即将要添加的值 while(!qmax.isEmpty() && nums[qmax.peekLast()] <= nums[R]) { qmax.pollLast(); } qmax.addLast(R); //双端队列左边的值弹出的条件:R - k表示要弹出的下标,例如R=4,k=3,则要弹出下标为1,只剩下2,3,4三个数 if(qmax.peekFirst() == R - k) { qmax.pollFirst(); } //以上窗口更新做完了 //窗口形成w之后收集答案,没形成w之前不收集答案 if(R >= k-1) { res[index++] = nums[qmax.peekFirst()]; } } return res; }
栗子2:
给定一个整型数组arr,和一个整数num。某个arr中的子数组sub,如果想达标,必须满足:sub中最大值-sub中最小值 <=num,返回arr中达标子数组的数量
public static int num(int[] arr, int sum) { if (arr == null || arr.length == 0 || sum < 0) { return 0; } int N = arr.length; int count = 0; LinkedList<Integer> maxWindow = new LinkedList<>();//最大值窗口 LinkedList<Integer> minWindow = new LinkedList<>();//最小值窗口 int R = 0; //L是开头位置,尝试每一个开头,0位置达标的多少个,1位置达标的多少个,R滑到违规停 for (int L = 0; L < N; L++) { //如果窗口此时的开头为L,下面的while工作:R向右扩到违规为止 while (R < N) {//R是最后一个达标位置的再下一个 while (!maxWindow.isEmpty() && arr[maxWindow.peekLast()] <= arr[R]) { maxWindow.pollLast(); } maxWindow.addLast(R); while (!minWindow.isEmpty() && arr[minWindow.peekLast()] >= arr[R]) { minWindow.pollLast(); } minWindow.addLast(R); //一旦不达标break if (arr[maxWindow.peekFirst()] - arr[minWindow.peekFirst()] > sum) { break; } else { R++; } } //R是最后一个达标位置的下一个位置,第一个违规的位置,以L开头,到R位置一共有多少达标的。例如R=5,L=0,0~0,0~1,0~2,0~3,0~4共5个达标 count += R - L; //L更新 if (maxWindow.peekFirst() == L) { maxWindow.pollFirst(); } if (minWindow.peekFirst() == L) { minWindow.pollFirst(); } } return count; }
单调栈
一种特别设计的栈结构,解决如下问题:
- arr[i]的左侧离i最近并且小于(或者大于)arr[i]的位置在哪
- arr[i]的右侧离i最近并且小于(或者大于)arr[i]的位置在哪
栗子1:
数组{3,2,1,4,5},返回[ [-1,1],[-1,2],[-1,-1],[2,-1],[3,-1] ]
//无重复值 public static int[][] getNearLessNoRepeat(int[] arr) { int[][] res = new int[arr.length][2]; // 单调栈,只存位置! Stack<Integer> stack = new Stack<>(); for (int i = 0; i < arr.length; i++) { // 当遍历到i位置的数,arr[i] while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) { int j = stack.pop(); int leftLessIndex = stack.isEmpty() ? -1 : stack.peek(); res[j][0] = leftLessIndex; res[j][1] = i; } stack.push(i); } while (!stack.isEmpty()) { int j = stack.pop(); int leftLessIndex = stack.isEmpty() ? -1 : stack.peek(); res[j][0] = leftLessIndex; res[j][1] = -1; } return res; } //有重复值 public static int[][] getNearLess(int[] arr) { int[][] res = new int[arr.length][2]; //List<Integer>:放的是位置,同样值的东西,位置压在一起 //代表值: 底->顶 小->大 Stack<List<Integer>> stack = new Stack<>(); for (int i = 0; i < arr.length; i++) { // i -> arr[i] 进栈 //下面的值大于当前值 while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) { List<Integer> popIs = stack.pop(); //取位于下面位置的列表中,最晚加入的那个.get(stack.peek().size() - 1):最晚加入的 int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1); for (Integer popi : popIs) { res[popi][0] = leftLessIndex; res[popi][1] = i; } } //等于的,比你小的 if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) { stack.peek().add(Integer.valueOf(i)); } else { ArrayList<Integer> list = new ArrayList<>(); list.add(i); stack.push(list); } } while (!stack.isEmpty()) { List<Integer> popIs = stack.pop(); //取位于下面位置的列表中,最晚加入的那个 int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1); for (Integer popi : popIs) { res[popi][0] = leftLessIndex; res[popi][1] = -1; } } return res; }
栗子2:
给定一个只包含正数的数组arr,arr中任何一个子数组sub,一定都可以算出(sub累加和)*(sub中的最小值)是什么,所有子数组中,这个值最大是多少?
滑动窗口,单调栈怎么用
想用滑动窗口,要想办法把具体的问题转化为滑动窗口的处理流程
想用滑动窗口最值的更新结构,就看处理流程下,是否需要最值这个信息
想用单调栈,要想办法把具体的问题转化为单调栈所解决的原问题

浙公网安备 33010602011771号