滑动窗口
滑动窗口是一种想象出来的数据结构:
滑动窗口有左边界L和有边界R
在数组或者字符串或者一个序列上,记为S,窗口就是S[L..R]这一部分
L往右滑意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口
L和R都只能往右滑
在给定一个整数数组和滑动窗口大小的情况下,需要在每次窗口滑动时更新窗口内的最大值。双端队列可以用来存储窗口内的最大值元素的索引,从而在 O(1) 时间内更新最大值
保持双端队列从头到尾是以大到小,数字均从尾进出,当新进来的数比双端队列的数大时,就把队列里面小的数依次从尾部弹出。窗口往右划的过程中,左边的数过期就从头部弹出。这样就能从双端队列头部数字得到窗口内最大的数
题目1:假设一个固定大小为W的窗口,依次划过arr,返回每一次滑出状况的最大值。例如,arr=[4,3,5,4,3,3,6,7],W=3,返回:[5,5,5,4,6,7]
public static int[] getMaxWidow(int[] arr,int w){
if(arr=null || w<1 || arr.length<w){
return null;
}
//其中放的是位置,arr[位置]
//双端队列
LinkedList<Integer> qmax=new LinkedList<Integer>();
int[] res=new int[arr.length-w+1];
int index=0;
for(int i=0;i<arr.length;i++){
while(!qmax.isEmpty()&&arr[qmax.peekLast()]<=arr[i]){
qmax.pollLast();
}
qmax.addLast(i);
if(qmax.peekFirst()==i-w){
qmax.pollFirst();
}
if(i>=w-1){
res[index++]=arr[qmax.peekFirst()];
}
}
return res;
}
题目二:
给定一个整型数组arr,和一个整数num,某个arr中的子数组sub,如果想达标,必须满足:sub中最大值-sub最小值<=num,返回arr中达标子数组的数量
public static int getNum(int[] arr,int num){
if(arr==null || arr.length==0){
return 0;
}
//存储当前窗口最小值的索引。
LinkedList<Integer> qmin=new LinkedList<Integer>();
//存储当前窗口最大值的索引。
LinkedList<Integer> qmax=new LinkedList<Integer>();
int L=0; int R=0;
int res=0;
while(L<arr.length){
while(R<arr.length){
while(!qmin.isEmpty()&&arr[qmin.peekLast()]>=arr[R]){
qmin.pollLast();
}
qmin.addLast(R);
while(!qmax.isEmpty()&&arr[qmax.peekLast()]<=arr[R]){
qmax.pollLast();
}
qmax.addLast(R);
//如果当前窗口的最大值和最小值之差大于 num,则停止扩展窗口。
if(arr[qmax.getFirst()]-arr[qmin.getFirst()]>num){
break;
}
R++;
}
res+=R-L;
if(qmin.peekFirst()==L){
qmin.pollFirst();
}
if(qmax.peekFirst()==L){
qmax.pollFirst();
}
L++;
}
return res;
}
单调栈
找某个数左右两边离他最近的比它小的数字在哪
准备一个栈,从栈底到栈顶遵循从小到大的,碰到一个不遵循的,就把数从栈顶弹出,该位置的数的右边离你最近最小的就是那个让你弹出去的数,你所压在栈底下的那个数则是你左边离你最近最小的数
//arr=[3,2,1,4,5]
//[0:[-1,1]
// 1:[-1,2]
//]
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();
//取位于下面位置的列表中,最晚加入的那个
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;
}
题目:给定一个只包含正整数的数组arr,arr中任何一个子数组sub,一定都可以算出(sub累加和)*(sub中最小值)是什么,那么所有子数组中,这个值最大是多少
以i位置做为最小值的子数组,使其子数组范围尽量最大
public class Solution {
public int maxSumMinProduct(int[] arr) {
int n = arr.length;
int maxSum = 0;
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
int index = stack.pop();
int left = stack.isEmpty() ? -1 : stack.peek();
int sum = (index - left - 1) * arr[index]; // 计算以 arr[index] 为最小值的子数组的和
if (left != -1) {
sum += (left + n - index) * arr[index]; // 如果左边有元素,加上左边和右边的和
}
maxSum = Math.max(maxSum, sum); // 更新最大值
}
stack.push(i);
}
//处理栈中剩余的元素。
//这些索引代表了以它们为最小值的子数组,其左侧没有更小的元素。
//计算这些剩余子数组的和,并更新最大值 maxSum
while (!stack.isEmpty()) {
int index = stack.pop();
int left = stack.isEmpty() ? -1 : stack.peek();
int sum = (index - left - 1) * arr[index];
if (left != -1) {
sum += (left + n - index) * arr[index];
}
maxSum = Math.max(maxSum, sum);
}
return maxSum;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {1, 2, 3, 4, 5}; // 示例数组
int result = solution.maxSumMinProduct(arr);
System.out.println("最大值是: " + result);
}
}
总结:
滑动窗口
滑动窗口通常用于解决与数组或字符串中的连续子序列相关的问题,特别是当问题可以归结为“找到满足特定条件的子序列”时。具体来说,当你需要:
- 找到子序列的最大或最小和/乘积:例如,找到数组中和最大的连续子数组。
- 满足特定条件的子序列:例如,找到所有字符都是奇数的最长子字符串。
- 维护一个窗口内的状态:例如,找到窗口内所有元素都是正数的最长子数组。
- 计数问题:例如,找到数组中不同整数对的最大数量,使得一对数的和为特定值。
单调栈
单调栈通常用于解决与下标相关的问题,特别是当问题涉及到维护一个递增或递减的序列时。具体来说,当你需要:
- 找到下一个/上一个大/小的元素:例如,找到数组中每个元素右侧/左侧的第一个比它大/小的元素。
- 维护一个单调递增或递减的序列:例如,使用单调栈可以有效地找到数组中每个元素的下一个最小值。
- 处理与栈有关的括号匹配问题:例如,验证括号是否正确闭合或找到匹配的括号。
- 优化空间复杂度:相比于滑动窗口,单调栈可以在 O(n) 的空间复杂度内解决问题,因为它只存储与当前元素相关的元素。