单调队列
单调队列是满足单调性的队列,在插入某个元素前,将队尾不满足单调性的元素出队。维护单调队列的单调性,来解决问题。
单调队列和滑动窗口一样,不属于什么特殊的数据结构或编程思想,可以理解成一个解题技巧。
滑动窗口最大值
只有满足在当前窗口下,其右边没有比这个元素更大的值这个条件时,元素才在队列中。
因此在某一个滑动窗口下,若某个元素不在队列中则意味着其右边有比它更大的值,而滑动窗口是向右滑动的,因此这个不在队列中的元素一定比其右边更大元素先出窗口。也就是说,当某个元素不在队列中时,意味着它永远也不可能成为滑动窗口中的最大值了。这就是为什么当入队一个元素时,可以将队尾所有小于它的元素出队(因为提前删除这些元素不会影响结果)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (k == 1) return nums;
Deque<Integer> queue = new LinkedList<>(); //单调减,队头就是窗口中的最大值。
//先将前k-1个元素入队,因为构不成一个完整的窗口
for (int i = 0; i < k - 1; i++) {
while (!queue.isEmpty() && nums[queue.getLast()] < nums[i]) queue.removeLast();
queue.addLast(i);
}
int[] ans = new int[nums.length - k + 1];
//依次将第k个元素及之后的元素入队,并确保队列的最大元素没有超过窗口范围
for (int i = 0; i < ans.length; i++) {
int index = i + k;
while (!queue.isEmpty() && nums[queue.getLast()] < nums[index]) queue.removeLast();
queue.addLast(index);
ans[i] = nums[queue.getFirst()];
if (queue.getFirst() == i) queue.removeFirst();
}
return ans;
}
}
绝对差不超过限制的最长连续子数组
class Solution {
public int longestSubarray(int[] nums, int limit) {
//队列中存放index
Deque<Integer> winMax = new LinkedList<>(); //单调递减
Deque<Integer> winMin = new LinkedList<>(); //单调递增
int ans = 0;
int left = 0; //窗口左边框
int right = 0; //窗口右边框
while (right < nums.length) {
while (!winMax.isEmpty() && nums[winMax.getLast()] <= nums[right]) winMax.removeLast();
while (!winMin.isEmpty() && nums[winMin.getLast()] >= nums[right]) winMin.removeLast();
winMax.addLast(right);
winMin.addLast(right);
while (nums[winMax.getFirst()] - nums[winMin.getFirst()] > limit) {
//此时窗口中最大最小元素之差超过limit,应该去掉一个。那么该去掉哪一个呢?
//假设此时窗口中的最大元素对应位置为maxI,最小元素对应位置为minI。
//如果将left移动到min(maxI, minI)的左边并不会影响窗口中的最大最小值
//因此希望改变窗口中的最大最小值就应当将left移动到min(maxI, minI)的右边,即min(maxI, minI) + 1的位置。
if (winMax.getFirst() > winMin.getFirst()) {
left = winMin.removeFirst() + 1;
} else {
left = winMax.removeFirst() + 1;
}
}
right++;
ans = Math.max(ans, right - left);
}
return ans;
}
}
单调栈
单调栈用于解决:寻找下/上一个更大/更小元素。
寻找下一个更大元素:从后往前遍历,维护一个单调递减的栈,当对某个元素入栈时,将比它小的元素出栈,以满足栈的单调性,此时栈顶元素就是当前元素的下一个更大元素,若栈空,则无下一个更大元素。(首先栈中元素都在当前元素的后面,另外把比当前元素小的元素都弹出了,因此此时栈顶就是比当前元素更大的下一元素)
对于寻找上一更大元素,则从前往后遍历。对于寻找更小元素,则用一个单调递增的栈。
下一个更大元素 I
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int[] index = new int[10001];
int[] stack = new int[nums2.length];
int top = -1;
int[] ans = new int[nums1.length];
//计算nums2中的下一个更大元素,并将答案记录在index数组中
for (int i = nums2.length - 1; i >= 0; i--) {
while (top >= 0 && stack[top] <= nums2[i]) {
top--;
}
index[nums2[i]] = top >= 0 ? stack[top] : -1;
stack[++top] = nums2[i];
}
for (int i = 0; i < nums1.length; i++) {
ans[i] = index[nums1[i]];
}
return ans;
}
}
去除重复字母
目的是使字典序小的字母尽可能往前,字典序大的字母尽可能往后。
题解:遍历字符串,记录每个字母出现的次数。再从前往后遍历,对于当前字母,若栈中还没有该字母,则将栈顶所有字典序更大且后面还有的字母弹出(对于字典序更大但后面不出现的字母则不必弹出,且该字母之前的所有字母也不必弹出(使字典序大的字母尽可能往后))
class Solution {
int[] chCount = new int[26];
int[] stack = new int[26];
boolean[] isInStack = new boolean[26];
public String removeDuplicateLetters(String s) {
char[] array = s.toCharArray();
Arrays.fill(isInStack, false);
int top = -1;
for (char ch : array) {
chCount[ch - 'a']++;
}
for (char ch : array) {
int chi = ch - 'a';
if (isInStack[chi]) { //该字母已在栈中,则不用管
chCount[chi]--;
continue;
}
//该字母尚未入栈,则弹出栈顶所有字典序大且后面还有的字母
while (top != -1 && chi < stack[top] && chCount[stack[top]] != 0) { //栈不空且当前小于栈顶且栈顶元素还有,删除栈顶
isInStack[stack[top--]] = false;
}
stack[++top] = chi;
isInStack[chi] = true;
chCount[chi]--;
}
//此时栈中字母就是最小字典序
StringBuilder ans = new StringBuilder();
for (int i = 0; i <= top; i++) {
ans.append((char)(stack[i] + 'a'));
}
return ans.toString();
}
}