单调栈

  1. 316.去除重复字母
    (https://leetcode-cn.com/problems/remove-duplicate-letters)
    image
String removeDuplicateLetters(String s) {
    Stack<Character> stk = new Stack<>();

    // 维护一个计数器记录字符串中字符的数量
    // 因为输入为 ASCII 字符,大小 256 够用了
    int[] count = new int[256];
    for (int i = 0; i < s.length(); i++) {
        count[s.charAt(i)]++;
    }

    boolean[] inStack = new boolean[256];
    for (char c : s.toCharArray()) {
        // 每遍历过一个字符,都将对应的计数减一
        count[c]--;

        if (inStack[c]) continue;

        while (!stk.isEmpty() && stk.peek() > c) {
            // 若之后不存在栈顶元素了,则停止 pop
            if (count[stk.peek()] == 0) {
                break;
            }
            // 若之后还有,则可以 pop
            inStack[stk.pop()] = false;
        }
        stk.push(c);
        inStack[c] = true;
    }

    StringBuilder sb = new StringBuilder();
    while (!stk.empty()) {
        sb.append(stk.pop());
    }
    return sb.reverse().toString();
}

这里也可以想到为什么要用「栈」这种数据结构,因为先进后出的结构允许我们立即操作刚插入的字符,如果用「队列」的话肯定是做不到的。

  1. 移掉K位数字


    截屏2021-11-24 下午7.03.05.png

我们可以用一个栈维护当前的答案序列,栈中的元素代表截止到当前位置,删除不超过 kk 次个数字后,所能得到的最小整数。

class Solution {
    public String removeKdigits(String num, int k) {
        if(k == 0) return num;
        if(k == num.length()) return "0";
        Stack<Character> s = new Stack<>();
        int index = 0;
        while(index < num.length()){
            while(k > 0 && s.size() > 0 && num.charAt(index) < s.peek()){
                s.pop();
                k--;
            }
            //前导0情况的处理
            if(s.size() == 0 && num.charAt(index) == '0') {
                index++;
                continue;
            }
            s.push(num.charAt(index));
            index++;
        }
        String ans = "";
        while(k > 0){
            s.pop();
            k--;
        }
        if(s.size() == 0) return "0";
        while(!s.isEmpty()) ans += s.pop();
        return new StringBuffer(ans).reverse().toString();
    }
}
  1. 拼接最大数
    和第一道题类似,只不过这一次是两个数组,而不是一个,并且是求最大数。

最大最小是无关紧要的,关键在于是两个数组,并且要求从两个数组选取的元素个数加起来一共是 k。

然而在一个数组中取 k 个数字,并保持其最小(或者最大),我们已经会了(利用单调栈)。但是如果问题扩展到两个,会有什么变化呢?

实际上,问题本质并没有发生变化。 假设我们从 nums1 中取了 k1 个,从 num2 中取了 k2 个,其中 k1 + k2 = k。而 k1 和 k2 这 两个子问题我们是会解决的。由于这两个子问题是相互独立的,因此我们只需要分别求解,然后将结果合并即可。

假如 k1 和 k2 个数字,已经取出来了。那么剩下要做的就是将这个长度分别为 k1 和 k2 的数字,合并成一个长度为 k 的数组合并成一个最大的数组。

以题目的 nums1 = [3, 4, 6, 5] nums2 = [9, 1, 2, 5, 8, 3] k = 5 为例。 假如我们从 num1 中取出 1 个数字,那么就要从 nums2 中取出 4 个数字。

运用第一题的方法,我们计算出应该取 nums1 的 [6],并取 nums2 的 [9,5,8,3]。 如何将 [6] 和 [9,5,8,3],使得数字尽可能大,并且保持相对位置不变呢?

截屏2021-11-24 下午7.20.21.png
class Solution {
    public int[] maxNumber(int[] nums1, int[] nums2, int k) {
        int m = nums1.length;
        int n = nums2.length;
        int[] ans = new int[k];
        int len = Math.min(k, m);
        for (int i=Math.max(0, k-n); i<=len; i++) {
            int[] sub1 = maxKArray(nums1, i);
            int[] sub2 = maxKArray(nums2, k-i);
            int[] array = combineArray(sub1, sub2, k);
            for (int j=0; j<k; j++) {
                if (array[j] == ans[j]) continue;
                if (array[j] > ans[j])  ans = array;
                break;
            }
        }
        return ans;
    }
    
    public int[] maxKArray(int[] nums, int k) {
        if (k == 0) return new int[0];
        
        int[] res = new int[k];
        int cursor = -1;
        for (int i=0; i<nums.length; i++) {
            while (cursor>=0 && nums[i]>res[cursor] && nums.length-i>k-cursor-1) {
                cursor--;
            }
            if (cursor < k-1)
                res[++cursor] = nums[i];
        }
        return res;
    }
    
    public int[] combineArray(int[] nums1, int[] nums2, int k) {
        int[] res = new int[k];
        int i = 0;
        int i1 = 0;
        int i2 = 0;
        while (i1 < nums1.length && i2 < nums2.length)
            res[i++] = deepCompare(nums1, nums2, i1, i2)? nums1[i1++] : nums2[i2++];
        while (i1 < nums1.length)
            res[i++] = nums1[i1++];
        while (i2 < nums2.length)
            res[i++] = nums2[i2++];
        return res;
    }

    public boolean deepCompare(int[] nums1, int[] nums2, int i1, int i2) {
        while (i1 < nums1.length && i2 < nums2.length) {
            if (nums1[i1] == nums2[i2]) {
                i1++;
                i2++;
                continue;
            }
            return nums1[i1] > nums2[i2];
        }
        return i1 < nums1.length;
    }
}
  1. Largest Rectangle in Histogram

我们归纳一下枚举「高」的方法:

首先我们枚举某一根柱子 i 作为高h=heights[i];

随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 h。换句话说,我们需要找到左右两侧最近的高度小于 h 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 ii 能够扩展到的最远范围。
这样我们在枚举到第 i 根柱子的时候,就可以先把所有高度大于等于 height[i] 的 j 值全部移除,剩下的 j 值中高度最高的即为答案。在这之后,我们将 i 放入数据结构中,开始接下来的枚举。此时,我们需要使用的数据结构也就呼之欲出了,它就是栈。

栈中存放了 j 值。从栈底到栈顶,j 的值严格单调递增,同时对应的高度值也严格单调递增;

当我们枚举到第 ii 根柱子时,我们从栈顶不断地移除 height[j]≥height[i] 的 j 值。在移除完毕后,栈顶的 j 值就一定满足 height[j]<height[i],此时 j 就是 i 左侧且最近的小于其高度的柱子。

这里会有一种特殊情况。如果我们移除了栈中所有的 j 值,那就说明 i 左侧所有柱子的高度都大于 height[i],那么我们可以认为 i 左侧且最近的小于其高度的柱子在位置 j=−1,它是一根「虚拟」的、高度无限低的柱子。这样的定义不会对我们的答案产生任何的影响,我们也称这根「虚拟」的柱子为「哨兵」。
我们再将 i 放入栈顶。

栈中存放的元素具有单调性,这就是经典的数据结构「单调栈」了。

复杂度分析:
时间复杂度:O(N)。
空间复杂度:O(N)。

class Solution {
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];
        int[] right = new int[n];
        
        Deque<Integer> mono_stack = new ArrayDeque<Integer>();
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                mono_stack.pop();
            }
            left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
            mono_stack.push(i);
        }

        mono_stack.clear();
        for (int i = n - 1; i >= 0; --i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                mono_stack.pop();
            }
            right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
            mono_stack.push(i);
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
}

  1. Maximal Rectangle
    我们首先计算出矩阵的每个元素的左边连续 1的数量,使用二维数组 left 记录,其中left[i][j] 为矩阵第 i行第 j 列元素的左边连续 1的数量。
class Solution {
   public int maximalRectangle(char[][] matrix) {
       int m = matrix.length;
       if (m == 0) {
           return 0;
       }
       int n = matrix[0].length;
       int[][] left = new int[m][n];

       for (int i = 0; i < m; i++) {
           for (int j = 0; j < n; j++) {
               if (matrix[i][j] == '1') {
                   left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
               }
           }
       }

       int ret = 0;
       for (int j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
           int[] up = new int[m];
           int[] down = new int[m];

           Deque<Integer> stack = new LinkedList<Integer>();
           for (int i = 0; i < m; i++) {
               while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
                   stack.pop();
               }
               up[i] = stack.isEmpty() ? -1 : stack.peek();
               stack.push(i);
           }
           stack.clear();
           for (int i = m - 1; i >= 0; i--) {
               while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
                   stack.pop();
               }
               down[i] = stack.isEmpty() ? m : stack.peek();
               stack.push(i);
           }

           for (int i = 0; i < m; i++) {
               int height = down[i] - up[i] - 1;
               int area = height * left[i][j];
               ret = Math.max(ret, area);
           }
       }
       return ret;
   }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容