单调队列与单调栈(总结及leetcode例题)

单调队列

单调队列是满足单调性的队列,在插入某个元素前,将队尾不满足单调性的元素出队。维护单调队列的单调性,来解决问题。
单调队列和滑动窗口一样,不属于什么特殊的数据结构或编程思想,可以理解成一个解题技巧。

滑动窗口最大值

只有满足在当前窗口下,其右边没有比这个元素更大的值这个条件时,元素才在队列中。
因此在某一个滑动窗口下,若某个元素不在队列中则意味着其右边有比它更大的值,而滑动窗口是向右滑动的,因此这个不在队列中的元素一定比其右边更大元素先出窗口。也就是说,当某个元素不在队列中时,意味着它永远也不可能成为滑动窗口中的最大值了。这就是为什么当入队一个元素时,可以将队尾所有小于它的元素出队(因为提前删除这些元素不会影响结果)

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();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]提供了一个朴素的解法,使用两个栈来存储字符串,一个栈用来存储普通字符,另一个栈用来存储特殊字符。遍历字符串,如果是普通字符则压入第一个栈,如果是特殊字符则弹出第一个栈的栈顶元素。最后比较两个栈是否相同即可判断字符串是否有效。这个解法的时间复杂度是O(M + N),空间复杂度也是O(M + N)。\[1\] 引用\[2\]提供了另一个栈的应用场景,即判断括号是否有效。遍历字符串,如果是左括号则压入栈,如果是右括号则判断和栈顶元素是否匹配,不匹配则返回false,匹配则弹出栈顶元素。最后判断栈是否为空即可判断括号是否有效。\[2\] 引用\[3\]也提供了一个判断括号是否有效的解法,使用栈来操作。遍历字符串,如果是左括号则压入栈,如果是右括号则判断和栈顶元素是否匹配,不匹配则返回false,匹配则弹出栈顶元素。最后判断栈是否为空即可判断括号是否有效。这个解法使用了HashMap来存储括号的对应关系。\[3\] 综上所述,栈在解决字符串相关问题中有着广泛的应用,包括判断字符串是否有效、逆波兰表达式等。在解决这些问题时,栈可以帮助我们保存和处理字符的顺序,从而简化问题的处理过程。 #### 引用[.reference_title] - *1* *3* [Leetcode刷题03-栈](https://blog.csdn.net/weixin_47802917/article/details/123007699)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] - *2* [leetCode-栈类型详解](https://blog.csdn.net/zhiyikeji/article/details/125508011)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值