Leetcode记录库数据结构篇之三:栈和队列

本文介绍了如何用栈和队列数据结构解决LeetCode中的经典问题,如队列模拟栈、栈实现队列、最小栈、括号匹配等,通过实例演示和代码实现,帮助理解这两种基本数据结构在实际问题中的灵活运用。

简介

三、栈和队列

结论

一些很主观的东西,归根到底可能还是自己的实力不够:

1 232. 用栈实现队列

题目

思路描述

  1. 栈后进先出,队列先进先出
  2. 倒腾呗。进队操作,就是进栈操作。出队操作时,先把一个栈一个一个pop()出来,push()到另一个栈中,然后出栈当出队,最后还得换回来。
  3. 其他操作类似
  4. 没了。

代码实现
java代码:

class MyQueue {
    private Stack<Integer> s1 = new Stack<>();
    private Stack<Integer> s2 = new Stack<>();

    /** Initialize your data structure here. */
    public MyQueue() {

    }
    
    /** Push element x to the back of queue. */
    public void push(int x) {
        s1.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        while(!s1.isEmpty()){
            s2.push(s1.pop());
        }
        int tmp = s2.pop();
        while(!s2.isEmpty()){
            s1.push(s2.pop());
        }

        return tmp;
    }
    
    /** Get the front element. */
    public int peek() {
        while(!s1.isEmpty()){
            s2.push(s1.pop());
        }
        int tmp = s2.peek();
        while(!s2.isEmpty()){
            s1.push(s2.pop());
        }

        return tmp;
    }
    
    /** Returns whether the queue is empty. */
    public boolean empty() {
        return s1.isEmpty();
    }
}

注意事项

  1. 注意事项

拓展延伸

  1. 拓展延伸

2 225. 用队列实现栈

题目

思路描述

  1. 和上一题一样,倒腾呗。
  2. 进栈就进队,出栈就先记录队列长度,然后一直出队到队列长度减一,再出队就是我们要的值。
  3. 其他操作类似。
  4. 完了。

代码实现
java代码:

class MyStack {
    private Queue<Integer> q1 = new LinkedList<>();
    private Queue<Integer> q2 = new LinkedList<>();

    /** Initialize your data structure here. */
    public MyStack() {

    }
    
    /** Push element x onto stack. */
    public void push(int x) {
        if(q1.isEmpty() && q2.isEmpty() || q2.isEmpty()){
            q1.offer(x);
        }else{
            q2.offer(x);
        }
    }
    
    /** Removes the element on top of the stack and returns that element. */
    public int pop() {
        int tmp;
        if(!q1.isEmpty()){
            int size = q1.size();
            for(int i = 0; i < size - 1; i++){
                q2.offer(q1.poll());
            }
            tmp = q1.poll();
        }else{
            int size = q2.size();
            for(int i = 0; i < size - 1; i++){
                q1.offer(q2.poll());
            }
            tmp = q2.poll();
        }

        return tmp;
    }
    
    /** Get the top element. */
    public int top() {
        int tmp;
        if(!q1.isEmpty()){
            int size = q1.size();
            for(int i = 0; i < size - 1; i++){
                q2.offer(q1.poll());
            }
            tmp = q1.peek();
            q2.offer(q1.poll());
        }else{
            int size = q2.size();
            for(int i = 0; i < size - 1; i++){
                q1.offer(q2.poll());
            }
            tmp = q2.peek();
            q1.offer(q2.poll());
        }

        return tmp;
    }
    
    /** Returns whether the stack is empty. */
    public boolean empty() {
        if(q1.isEmpty() && q2.isEmpty()){
            return true;
        }else{
            return false;
        }
    }
}

注意事项

  1. 注意事项

拓展延伸

  1. 拓展延伸

3 155. 最小栈

题目

思路描述

  1. 两个栈,一个栈就用来存储数值。另一个一直进栈当前栈中数值最小的数字。

代码实现
java代码:

class MinStack {
    private Stack<Integer> s = new Stack<>();
    private Stack<Integer> minS = new Stack<>();
    private int min = Integer.MAX_VALUE;

    /** initialize your data structure here. */
    public MinStack() {

    }
    
    public void push(int val) {
        s.push(val);
        min = min < val ? min : val;
        minS.push(min);
    }
    
    public void pop() {
        s.pop();
        minS.pop();
        min = minS.isEmpty() ? Integer.MAX_VALUE : minS.peek();
    }
    
    public int top() {
        return s.peek();
    }
    
    public int getMin() {
        return minS.peek();
    }
}

注意事项

  1. 注意事项

拓展延伸

  1. 拓展延伸

4 20. 有效的括号

题目

思路描述

  1. 将字符串从左向右扫描,存到栈中,每一次扫描判断当前括号是否能与栈顶相匹配。

代码实现
java代码:

class Solution {
    public boolean isValid(String s) {
        if(s.length() % 2 == 1) return false;
        Stack<Character> s1 = new Stack<>();
        for(int i = 0; i<s.length(); i++){
            if(!s1.isEmpty()){
                switch (s.charAt(i)){
                    case ')': 
                    if(s1.peek().equals('(')) {
                        s1.pop();
                        }else{
                            s1.push(s.charAt(i));
                            }
                    break;

                    case ']': if(s1.peek().equals('[')) {
                        s1.pop();
                        }else{
                            s1.push(s.charAt(i));
                            }
                    break;

                    case '}': if(s1.peek().equals('{')) {
                        s1.pop();
                        }else{
                            s1.push(s.charAt(i));
                            }
                    break;

                    default: 
                    s1.push(s.charAt(i));
                }
            }else{
                s1.push(s.charAt(i));
            }
        }

        return s1.isEmpty();
    }
}

注意事项

  1. 注意事项

拓展延伸

  1. 拓展延伸

5 503. 下一个更大元素 II

题目

思路描述

  1. 没太看懂题意,这不就一个个找不就行了?

代码实现
java代码:

class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int len = nums.length; // 数组长度新建数组
        int[] ret = new int[len];
        int i = 0, j; // 初始化
        while(i < len){ // 遍历nums内所有元素
            boolean flag = false; // ret数据赋值标记
            int first = nums[i]; // 第一个
            j = (i + 1 == len) ? 0 : i + 1; // 后续元素索引 
            while(i != j){ // 是否查找了一圈?
                if(first < nums[j]){ // 当前元素是否大于first
                    ret[i] = nums[j];
                    flag = true;
                    break;
                }
                j = ++j == len ? 0 : j; // j后移并循环
            }
            if(!flag){ // 如果ret[i]没有被修改,则它的值应该为-1
                ret[i] = -1;
            }
            i++;
        }

        return ret;
    }
}

注意事项

  1. 注意事项

拓展延伸

  1. 单调栈的题目,很巧妙。走通了,但是以我目前的实力并不能做到化为自己的东西。
    解法如下:
class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[] next = new int[n];
        Arrays.fill(next, -1);
        Stack<Integer> pre = new Stack<>();
        for (int i = 0; i < n * 2; i++) {
            int num = nums[i % n];
            while (!pre.isEmpty() && nums[pre.peek()] < num) {
                next[pre.pop()] = num;
            }
            if (i < n){
                pre.push(i);
            }
        }
        return next;
    }
}

6 剑指 Offer 58 - I. 翻转单词顺序

题目

思路描述

  1. 这道题很思想挺简单的,就是实现起来有好多需要注意的地方。
  2. 我首先想到的是类似于循环左右移数组的方法,首先翻转一下整个数组,然后再一个一个的翻转各个单词,但是我们还要把多余的空格删掉,这就要求我们使用一个能够删除空格的字符串存储方式,字符数组可以但不太好,因为每删除一个字符,还要移动其他的字符,时间复杂度不好,虽然可以优化方法做到时间复杂度为n,但是一想做完这个操作后,还需要继续一个一个单词的翻转,就想着看看有没有更好的方法。虽然,StringBuilder类的类型可以方便的删除某个索引的字符,但是在又不能方便的交换字符,这个方法在实现时可以做到零API使用。
  3. 还想着说使用双端队列,从前往后将前面的单词一个个移到队列后面,但是一想这个String一开始没在Deque中存着,还要自己转换,这样时间复杂度就又上去了,这个多少还是会使用一点API的。
  4. 结果一看题解,他们也没有什么好方法,也都是我这两种想法。但是有一个全程调用API的方法,确实好用,但是说了面试时不推荐使用。但是也有好处,让我知道好多以前做题时没用过的API。
  5. 时空复杂度O(n) O(n)

代码实现
java代码:

// 纯手写连续翻转StringBuilder和charArray实现
class Solution {
    public String reverseWords(String s) {
        char[] sarr = trimSpaces(s);
        // 翻转字符串
        reverse(sarr, 0, sarr.length - 1);
        // 翻转每个单词
        reverseEachWord(sarr);

        return new String(sarr);
    }

    public char[] trimSpaces(String s) {
        int l = 0, r = s.length() - 1;
        char[] sarr = s.toCharArray();
        // 去掉字符串开头的空白字符
        while (l <= r && sarr[l] == ' ') {
            l++;
        }
        // 去掉字符串末尾的空白字符
        while (l <= r && sarr[r] == ' ') {
            r--;
        }

        // 将字符串间多余的空白字符去除
        StringBuilder sb = new StringBuilder();
        char tmp;
        while (l <= r) {
            tmp = sarr[l];
            if (tmp != ' ') {
                sb.append(tmp);
            } else if (sb.charAt(sb.length() - 1) != ' ') {
                sb.append(tmp);
            }
            l++;
        }

        return sb.toString().toCharArray();
    }

    public void reverse(char[] sarr, int l, int r) {
        char tmp;
        while (l < r) {
            tmp = sarr[l];
            sarr[l] = sarr[r];
            sarr[r] = tmp;
            l++;
            r--;
        }
    }

    public void reverseEachWord(char[] sarr) {
        int n = sarr.length;
        int start = 0, end = 0;

        while (start < n) {
            // 遍历到当前单词的末尾
            while (end < n && sarr[end] != ' ') {
                end++;
            }
            // 翻转单词
            reverse(sarr, start, end - 1);
            // 更新start,去找下一个单词
            end++;
            start = end;
        }
    }
}
// 连续翻转StringBuilder实现
class Solution {
    public String reverseWords(String s) {
        StringBuilder sb = trimSpaces(s);

        // 翻转字符串
        reverse(sb, 0, sb.length() - 1);

        // 翻转每个单词
        reverseEachWord(sb);

        return sb.toString();
    }

    public StringBuilder trimSpaces(String s) {
        int left = 0, right = s.length() - 1;
        // 去掉字符串开头的空白字符
        while (left <= right && s.charAt(left) == ' ') {
            ++left;
        }

        // 去掉字符串末尾的空白字符
        while (left <= right && s.charAt(right) == ' ') {
            --right;
        }

        // 将字符串间多余的空白字符去除
        StringBuilder sb = new StringBuilder();
        while (left <= right) {
            char c = s.charAt(left);

            if (c != ' ') {
                sb.append(c);
            } else if (sb.charAt(sb.length() - 1) != ' ') {
                sb.append(c);
            }

            ++left;
        }
        return sb;
    }

    public void reverse(StringBuilder sb, int left, int right) {
        while (left < right) {
            char tmp = sb.charAt(left);
            sb.setCharAt(left++, sb.charAt(right));
            sb.setCharAt(right--, tmp);
        }
    }

    public void reverseEachWord(StringBuilder sb) {
        int n = sb.length();
        int start = 0, end = 0;

        while (start < n) {
            // 循环至单词的末尾
            while (end < n && sb.charAt(end) != ' ') {
                ++end;
            }
            // 翻转单词
            reverse(sb, start, end - 1);
            // 更新start,去找下一个单词
            start = end + 1;
            ++end;
        }
    }
}
// 双端队列
class Solution {
    public String reverseWords(String s) {
        s = s.trim();
        int n = s.length();
        char[] sarr = s.toCharArray();
        int l = 0;
        char c;
        Deque<String> dq = new LinkedList<String>();
        StringBuilder sb = new StringBuilder();
        while (l < n) {
            c = sarr[l++];
            if (c == ' ' && sb.length() != 0) {
                dq.addFirst(sb.toString());
                sb.setLength(0);
            } else if (c != ' '){
                sb.append(c);
            }
        }
        dq.addFirst(sb.toString());

        return String.join(" ", dq);
    }
}

注意事项

  1. 注意事项

拓展延伸

  1. 全API写法
class Solution {
    public String reverseWords(String s) {
        String[] strs = s.trim().split(" "); // 删除首尾空格,以空格为界分割字符串
        StringBuilder res = new StringBuilder();
        for(int i = strs.length - 1; i >= 0; i--) { // 倒序遍历单词列表
            if(strs[i].equals("")) continue; // 遇到空单词则跳过
            res.append(strs[i] + " "); // 将单词拼接至 StringBuilder
        }
        return res.toString().trim(); // 转化为字符串,删除尾部空格,并返回
    }
}

7 剑指 Offer 59 - II. 队列的最大值

题目

思路描述

  1. 这道题重点在于构造一个单调队列,类似于155. 最小栈这道题。
  2. 只要没有比当前队列中的最大的值进队,当前队列中的最大值也没有出队,那么其他任何操作都不会改变最大值。
  3. 所以重点是构造一个单调不增的队列来时刻保存队列中的最大值。
  4. 时空复杂度最高是O(n),平摊是O(1), O(n)

代码实现
java代码:

class MaxQueue {
    Deque<Integer> q; // 普通队列
    Deque<Integer> maxQ; // 单调不增队列,用于保存队列中的最大值

    public MaxQueue() {
        q = new LinkedList<>();
        maxQ = new LinkedList<>();
    }
    
    public int max_value() {
        if (maxQ.isEmpty()) {
            return -1;
        }

        return maxQ.peekFirst();
    }
    
    public void push_back(int value) {
        while (!maxQ.isEmpty() && maxQ.peekLast() < value) { // 构造单调队列
            maxQ.pollLast(); // 新的最大值入队,将比这个最大值小的值都出队
        }
        maxQ.offer(value); // 当前值进队
        q.offer(value);
    }
    
    public int pop_front() {
        if (q.isEmpty()) {
            return -1;
        }
        int ans = q.poll();
        if (ans == maxQ.peekFirst()) { // 如果当前出队的数字刚好是最大队列中的队头数字
            maxQ.pollFirst(); // 最大队列队头出队,这会改变最大队列中的最大值
        }

        return ans;
    }
}

注意事项

  1. 注意事项

拓展延伸

8 剑指 Offer 59 - I. 滑动窗口的最大值

题目

思路描述

  1. 窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题,所以这也是一道利用单调队列的题目。
  2. 我们使用单调队列来单调不增的保存每一个对应窗口中的数据。具体操作如下:
    • 每次窗口的滑动,队列的两端会发生变化:[i, j] => [i + 1, j + 1],也就是说 nums[i] 出队,nums[j + 1] 进队
    • 如果当前窗口滑出的值 nums[i] 刚好是队列的头,那么把队列的头出队,更新当前单调队列的队头(最大值)。
    • 将队列中所有小于滑动后新进入窗口 nums[j + 1] 的数值从队列后方出队,直到找到这个元素在单调不增队列中的位置。
    • peek队列中的第一个元素,也就是经过调整后当前窗口中最大的元素作为当前窗口的最大值。
  3. 时空复杂度O(n) O(k)

代码实现
java代码:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0) return new int[0];

        int n = nums.length;
        int[] ans = new int[n - k + 1];
        Deque<Integer> q = new LinkedList<>(); // 单调不增队列,记录每个窗口中的最大值
        int tmp;
        for (int i = 0; i < k; i++) { // 构造窗口
            tmp = nums[i];
            while (!q.isEmpty() && q.peekLast() < tmp) { // 构造单调队列
                q.pollLast();
            }
            q.offer(tmp);
        }
        ans[0] = q.peekFirst(); // 当前窗口最大值

        for (int i = k; i < n; i++) { // 窗口移滑动
            if(q.peekFirst() == nums[i - k]) q.poll(); // 如果刚好把单调队列中最大的元素滑出
            tmp = nums[i];
            while (!q.isEmpty() && q.peekLast() < tmp) { // 维护构造单调队列
                q.pollLast();
            }
            q.offer(tmp);
            ans[i - k + 1] = q.peekFirst(); // 获取当前的最大值
        }

        return ans;
    }
}

注意事项

  1. 注意事项

拓展延伸

9 剑指 Offer 31. 栈的压入、弹出序列

题目

思路描述

  • 时空复杂度O(n) O(1)

代码实现
java代码:

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        int pushLen = pushed.length, popLen = popped.length;
        if (pushLen != popLen) return false;

        int top = -1, i = 0, j = 0;
        while (i < pushLen && j < popLen) { // 遍历元素
            pushed[++top] = pushed[i++]; // 压栈
            while (top > -1 && pushed[top] == popped[j]) { // 如果出栈元素和栈顶元素相同
                top--; // 一直出栈,直到栈空,或者栈顶和预定的出栈序列不同
                j++;
            }
            if (top > -1 && i == pushLen) { // 如果栈不为空,但是 i 已经等于pushLen了,也就是说下次不会再进入外层的while循环了,也就没有办法再进行出栈操作,而栈还没有空
                return false; // 错了
            }
        }

        return true;
    }
}

注意事项

  1. 注意事项

拓展延伸

10 32. 最长有效括号

题目

思路描述

  • 时空复杂度O(n) O(n)在这里插入图片描述

代码实现
java代码:

class Solution {
    public int longestValidParentheses(String s) {
        int n = s.length();
        if (n < 2) return 0;

        Deque<Integer> st = new LinkedList<>(); // 栈底始终存放的是当前遍历过的所有符号中最后一个未被匹配的')'的索引值
        int i = 0;
        st.push(-1); // 模拟第一个未被匹配的')'
        int maxLen = 0; // 记录最长的连续的有效括号长度
        while (i < n) {
            if (s.charAt(i) == '(') { // 如果当前的字符是'('
                st.push(i); // 直接压栈
            } else { // 如果当前扫到的字符是')'
                st.pop(); // 我们希望栈顶是一个'(',假设是的,直接出栈,即便不是,那它一定是一个')'
                if (st.isEmpty()) { // 而且如果上面一步出栈的是')',那么此时栈已经是空了,说明我们连续遇到了两个')'
                    st.push(i); // 更新中最后一个未被匹配的')'的索引值
                } else { // 否则说明栈顶确实是一个左括号,成功匹配
                    maxLen = Math.max(maxLen, i - st.peek()); // 计算当前成功匹配的括号长度
                }
            }

            i++; // 不要忘了i++;
        }

        return maxLen;
    }
}

思路描述

  • 时空复杂度O(n) O(1)
  • 题解
    在这里插入图片描述
// 贪心
class Solution {
    public int longestValidParentheses(String s) {
        int n = s.length();
        if (n < 2) return 0;

        int ans = 0;
        int l = 0, r = 0, i = 0;
        while (i < n) {
            if (s.charAt(i) == '(') {
                l++;
            } else {
                r++;
            }

            if (l == r) {
                ans = Math.max(ans, (l << 1));
            } else if (r > l) {
                l = 0;
                r = 0;
            }

            i++;
        }

        l = 0;
        r = 0;
        i = n - 1;
        while (i > -1) {
            if (s.charAt(i) == '(') {
                l++;
            } else {
                r++;
            }

            if (l == r) {
                ans = Math.max(ans, (l << 1));
            } else if (l > r) {
                l = 0;
                r = 0;
            }

            i--;
        }

        return ans;
    }
}

注意事项

  1. 注意事项

拓展延伸

10 32. 最长有效括号

题目

思路描述

  • 时间复杂度:O(nlogk),其中 k 为服务器的数目,n 为请求的数目。开始时我们先初始化 k 个任务给 k 台机器,作为runningServer 时间复杂为O(klogk);之后在处理请求时,最多再执行(n - k)次移除、查询和放入操作,时间复杂度为O((n - k)logk),获取最繁忙的服务器列表时间复杂度是O(k)。
  • 空间复杂度:O(k),runningServer 和 freeServer 两者总和最多放入 k 个元素。

代码实现
java代码:

// 这道题一开始用暴力n方超时了
// 后来使用优先队列和有序集合进行了优化,降低了时间复杂度

class Solution {
    public List<Integer> busiestServers(int k, int[] arrival, int[] load) {
        if(k >= arrival.length){ // 如果服务器数量大于等于arrival的数量
            List<Integer> ret = new ArrayList();
            for(int i = 0; i < arrival.length; i++){
                ret.add(i);
            }
            return ret;
        }

        int taskNums = load.length;
        int[] handledTaskNum = new int[k]; // 第k号服务器处理了的任务数量
        int maxHandledTasksNum = 0; // 单台服务器处理过的最大任务数
        PriorityQueue<int[]> runningServerQueue = new PriorityQueue<int[]>((a, b) -> (a[1] - b[1])); // 优先队列,int[]{服务器编号,服务器结束上一次任务的时间}
        TreeSet<Integer> freeServerSet = new TreeSet<Integer>(); // <当前空闲的服务器编号>

        for (int i = 0; i < k; i++) {
            runningServerQueue.offer(new int[]{i, arrival[i] + load[i]}); // 初始化运行服务器集合
        }
        maxHandledTasksNum = 1;
        int arrivalTime, runningTime;
        for (int i = k; i < taskNums; i++) {
            arrivalTime = arrival[i]; // 当前任务到达时间
            runningTime = load[i]; // 当前任务完成所需时间
            while (!runningServerQueue.isEmpty() && runningServerQueue.peek()[1] <= arrivalTime) {
                freeServerSet.add(runningServerQueue.poll()[0]); // 从运行任务的服务器中选出在当前任务到达时上一次任务已经结束的那些,放入freeServerSet中
            }
            if(freeServerSet.isEmpty()) continue; // 如果当前没有空闲的服务器

            Integer serverId = freeServerSet.ceiling(i % k); // 选出空闲服务器中大于等于当前任务第一次尝试提交的服务器编号
            if (serverId == null) { // 如果为空,那么我们这里重新从0再次选一次服务器,这里是模拟服务器是一个环,首尾相连
                serverId = freeServerSet.ceiling(0);
            }
            freeServerSet.remove(serverId); // 将选中运行任务的服务器从空闲服务器中移除
            runningServerQueue.offer(new int[]{serverId, arrivalTime + runningTime}); // 将相应的服务器编号和任务终止时间放入运行服务器队列中
            maxHandledTasksNum = Math.max(maxHandledTasksNum, ++handledTaskNum[serverId]); // 记录当前但服务器最大的任务处理数量
        }
        
        List<Integer> serverIds = new ArrayList<Integer>();
        for (int i = 0; i < k; i++) {
            if (handledTaskNum[i] == maxHandledTasksNum) {
                serverIds.add(i);
            }
        } 

        return serverIds;
    }
}

注意事项

  1. 注意事项

拓展延伸

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值