算法学习-栈和队列(持续更新中)

这篇博客汇总了栈和队列在编程问题中的应用,包括行星碰撞、有效括号、字符串解码、函数独占时间、基本计算器、验证栈序列、后缀表达式、文件夹操作日志搜集、使括号有效、括号的分数、布尔表达式解析、括号子串反转等。通过具体代码实现展示了如何利用Java中Stack和Deque API解决问题。
摘要由CSDN通过智能技术生成

汇总和整理自己碰到的运用栈和队列的相关问题,以便日后复习。

相关知识

1. Java中栈和队列常用api

队列:

常规操作:
Deque<> que= new ArrayDeque<>(); 
que.offer(); //插入
que.poll();  //删除
que.peek(); //首元素

双端队列:
添加 boolean offerFirst() boolean offerLast()
删除 E pollFirst() E pollLast()
​查询 peekFirst() peekLast()

栈:

Stack<> st= new Stack<>(); 
st.push(); //插入 
st.pop(); //删除
st.peek(); //首元素

相关题目

735.行星碰撞
class Solution {
    public int[] asteroidCollision(int[] asteroids) {
        Stack<Integer> st=new Stack<>();
        for(int t:asteroids){
            //主要控制t元素的加入
            boolean flag=true;
            //只有栈顶元素>0,待进入元素<0才需要进行判断
            while(flag&&!st.isEmpty()&&st.peek()>0&&t<0){
                int a=st.peek(),b=-t;
                //判断是否删除栈顶元素,相等或者小
                if(a<=b) st.pop();
                //判断是否不加入负数,相等或者大于
                if(a>=b) flag=false;
            }
            //栈内没有元素为true
            //有负数不加入为false
            if(flag) st.push(t);
        }
        int size=st.size();
        int[] ans=new int[size];
        while(!st.isEmpty()){
            ans[--size]=st.pop();
        }
        return ans;
    }
}
20.有效的括号

根据左括号,将右括号压入栈中,如果出现待压入右括号且和栈顶元素相同,则弹出栈顶元素。在这个过程中,如果出现栈空或者栈顶元素和待压入元素不同,则返回false.

class Solution {
    public boolean isValid(String s) {
        Stack<Character> st=new Stack<>();
        for(int i=0;i<s.length();i++){
            char c=s.charAt(i);
            if(c=='(') st.push(')');
            else if(c=='[') st.push(']');
            else if(c=='{') st.push('}');
            //当前匹配不上,或者右括号多出来要匹配但栈已经为空
            else if(st.isEmpty()||c!=st.peek()) return false;
            else st.pop();
        }
        return st.isEmpty();
    }
}
678.有效的括号字符串

用左括号栈和星号栈两个栈去匹配,栈中存储的都是字符下标,优先匹配左括号栈,参考@微扰理论大神的题解

class Solution {
    public boolean checkValidString(String s) {
        Stack<Integer> left=new Stack<>();
        Stack<Integer> star = new Stack<>();
        for(int i=0;i<s.length();i++){
            char c=s.charAt(i);
            if(c=='(') left.push(i);
            else if(c=='*') star.push(i);
            else if(c==')'){
                if(!left.isEmpty()) left.pop();
                else if(!star.isEmpty()) star.pop();
                else return false;
            } 
        } 
        //一个个字符进栈处理完之后,最后处理左括号栈
        while(!left.isEmpty()){
            if(!star.isEmpty()){
                int i=left.pop();
                int j=star.pop();
                if(i>j)return false;
            }else return false;

        }
        return true;
    }
}
394.字符串解码

这题一眼用栈做,但是没想到要用辅助栈两个栈一起,并且面对字符串中不同数字字母类型的处理,以及进栈出栈的时机,都需要想得很通透。参考笨猪爆破组的题解。这题的字符栈存的是之前待拼接或者已拼接的字符,而不是括号间的字符,此外,数字可以直接用在括号间字符上,核心在于result= strStack.pop()+result.repeat(numStack.pop());

class Solution {
    public String decodeString(String s) {
        Stack<Integer> numStack=new Stack<>();
        Stack<String> strStack=new Stack<>();
        String result="";
        int num=0;
        char[] sc=s.toCharArray();
        for(char c:sc){
            if(c>='0'&&c<='9'){
                num=num*10+c-'0';
            }else if(c=='['){
                //需要入栈的东西理解清楚
                numStack.push(num);
                num=0;
                strStack.push(result);
                result="";
            }else if(c==']'){
                //核心在于拼接
                result= strStack.pop()+result.repeat(numStack.pop());
            }else {
                //字符的情况
                result+=c;
            }   
        }
        return result;
    }
}
636.函数的独占时间

函数递归调用,栈的运用题,我原先在栈中存入的是所有函数的start,碰到end就直接在ans里面更新,但是忽略了递归调用的话,最外层函数会重复算入递归调用的时间。在看了彤哥的题解二,可以在栈中存入已经start的函数序号,同时用一个时间戳记录当前时(开始时刻),在递归调用(栈不为空但是又有start),在res中累加前面函数已经独占的时间。start将函数入栈,end将函数出栈。

class Solution {
    public int[] exclusiveTime(int n, List<String> logs) {
        int[]res=new int[n];
        Stack<Integer> st=new Stack<>();
        //记录每个时间点的开始
        int cur=0;
        for(String s:logs){
            String[] ss=s.split(":");
            int p=Integer.parseInt(ss[0]);
            int time=Integer.parseInt(ss[2]);
            if(ss[1].equals("start")){
                if(!st.isEmpty()){
                    res[st.peek()]+=time-cur;
                }
                cur=time;
                st.push(p);
            }
            if(ss[1].equals("end")){
                res[st.pop()]+=time-cur+1;
                cur=time+1;
            } 
        }
        return res;
    }
}
224.基本计算器

参考labuladong的题解,综合利用递归和栈解题,从头开始遍历字符串,其中遍历的过程中区分数字和符号,先预存一个char sigh='+'然后用+、-、*、/以及字符串结束分割压栈的技巧很巧妙。碰到(开始递归,)结束递归,但是需要有一个全局变量记录递归内字符索引的移动。+、-数压栈,当遇到*、/的时候直接和栈顶元素算出结果再压栈,最后把栈中的结果累加求和。

class Solution {
    //全部变量,递归使用
    int i=0;
    public int calculate(String s) {
        return cal(s);
    }
    public int cal(String s){
        Stack<Integer> st=new Stack<>();
        int len=s.length();
        int num=0;
        char sign='+';
        for(;i<len;i++){
            //是数字
            char c=s.charAt(i);
            if(Character.isDigit(c)){
                num=num*10+(c-'0');
            } 
            if(c=='('){
                i++;
                //将(以后的字符串全部放入递归函数中
                num=cal(s);
                //在cal里面碰到)退出来以后,当前的c仍然为(,sign为+,所以执行到下面需要压一次栈
            }      
            //不是数字并且跳过空格,即遇到+-*/
            if((!Character.isDigit(c)&&c!=' ')||i==len-1){
                //之前存的sign
                switch(sign){
                    case '+':
                        st.push(num);
                        break;
                    case '-':
                        st.push(-num);
                        break;
                    case '*':
                        st.push(num*st.pop());
                        break;
                    case '/':
                        st.push(st.pop()/num);
                        break;
                }
                sign=c;
                num=0;
                //如果为)则退出循环,执行下面的res
                if(sign==')'){
                    break;
                }
            }     
        }
        
        int res=0;
        while(!st.isEmpty()){
            res+=st.pop();
        }
        return res;
    }
}
946.验证栈序列

用一个栈模拟进栈出栈,首先不断进栈,如果当前栈顶元素和出栈数组中的元素相同,则尽可能地将相同元素进行出栈。最终判断是出栈元素是否和出栈数组长度相同。

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        int poplen=popped.length;
        Stack<Integer> st=new Stack<>();
        int j=0;
        for(int i:pushed){
            //先入栈,和单调栈不一样
            st.push(i);
            while(!st.isEmpty()&&st.peek()==popped[j]){
                st.pop();
                j++;
            }
        }
        return j==poplen;
    }
}
36.后缀表达式

经典栈

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer>st=new Stack<>();
        for(String t:tokens){
            if(t.equals("+")){
                st.push(st.pop()+st.pop());
            }else if(t.equals("-")){
                st.push(-st.pop()+st.pop());
            }else if(t.equals("*")){
                st.push(st.pop()*st.pop());
            }else if(t.equals("/")){
                int a=st.pop();
                int b=st.pop();
                st.push(b/a);
            }else{
                st.push(Integer.valueOf(t));
            }
        }
        return st.pop();
    }
}
1598.文件夹操作日志搜集器

栈的思想,但是结果只要求最小步数,因此只需要模拟就好,不需要建栈存储。

class Solution {
    public int minOperations(String[] logs) {
        int ans=0;
        for(String s:logs){
            if(Objects.equals(s,"../")){
            	//当已经是主文件夹了不能回退了
                ans=Math.max(0,ans-1);
            }else if(Objects.equals(s,"./")) continue;
            else ans++;
        }
        return ans;
    }
}
921.使括号有效的最少添加

将有效括号转变为分值有效性问题,在入栈的过程中,需要避免右括号’)'不匹配的情况,然而左括号是可以多的,多多少最后补多少右括号就行了。因此可以遇到左括号+1,右括号-1,遍历过程中随时检查分数left是否小于0(也即右括号不能多).

class Solution:
    def minAddToMakeValid(self, s: str) -> int:
        ans, left=0,0
        for c in s:
            left +=1 if c=='(' else -1
            if left<0:
                left=0
                ans +=1
        return ans+left
856.括号的分数

参考题解,通过在栈中放入数字或者括号的字符进行计算,"(“一直加入,“)”不加入将栈中元素进行弹出,若弹出的是”(“则压入‘1’,否则一直弹出数字求和,直到把最近的”("弹出,将求和数字乘以2再压入。最后将栈中所有数字求和。

class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        st=[]
        for c in s:
            # (加入
            if c=='(':
                st.append('(')
            # )弹出并判断弹出的数字
            else:
                top=st.pop()
                # 弹出的数字为数字可以直接加入
                if top=='(':
                    st.append('1')
                # 弹出的数字为(则继续弹出,直至把最后一个(弹出
                else:
                    sum=0
                    ch=top
                    while ch != '(':
                        sum += int(ch)
                        ch=st.pop()
                    st.append(str(sum<<1))
        res=0
        while len(st)!=0:
            res += int(st.pop())
        return res    
1106.解析布尔表达式

字符栈。将非“)”的符号都压入栈中,碰到")“开始处理栈顶的表达式,即”( )“两者之间的结果,同时”(“之前的符号就是为操作符op,将运算结果继续压入栈中。在处理结果的时候,需要先将”( )"两者之间的bool值收集起来,根据其性质进行求值。

class Solution:
    def parseBoolExpr(self, expression: str) -> bool:
        st=[]
        for c in expression:
            if c != ')':
                st.append(c)
            else:
                s=""
                while st[-1] != '(':
                    s += st.pop()
                st.pop()
                op= st.pop()
                setbool=set(s.split(','))
                if op == '!':
                    st.append('t' if 'f' in setbool else 'f')
                elif op == '&':
                    st.append('f' if 'f' in setbool else 't')
                else:
                    st.append('t' if 't' in setbool else 'f')
        return st[-1]=='t'
1190.反转每对括号间的子串

纯粹栈模拟,不停入栈,碰到’)'弹栈,然后继续将结果入栈,最后将栈中结果弹出再反转输出。

class Solution {
    public String reverseParentheses(String s) {
        Stack<Character> st=new Stack<>();
        char[] s1=s.toCharArray();
        for(int i=0;i<s1.length;i++){
            if(s1[i]==')'){
                ArrayList<Character> temp=new ArrayList<>();
                while(!st.isEmpty()&&st.peek()!='('){
                    temp.add(st.pop());
                }
                st.pop();
                for(Character c:temp){
                    st.push(c);
                }
            }else{
                st.push(s1[i]);
            }
            
        }
        ArrayList<Character> res=new ArrayList<>();
        while(!st.isEmpty()){
            res.add(st.pop());
        }
        StringBuilder sb=new StringBuilder();
        for(Character c:res){
            sb.append(c);
        }
        return sb.reverse().toString();
    }
}
32.最长有效括号

栈的做法是括号问题最常见的解决办法,参考笨猪爆破组的题解,其中设置参照点的思想值得学习。两种索引会入栈: 1.等待被匹配的左括号索引。2.充当「参照物」的右括号索引,当左括号匹配光时,栈需要留一个垫底的参照物,用于计算一段连续的有效长度。

class Solution {
    public int longestValidParentheses(String s) {
        char[] s1=s.toCharArray();
        int len=s1.length;
        Stack<Integer> st=new Stack<>();
        st.push(-1);
        int ans=0;
        for(int i=0;i<len;i++){
            if(s1[i]=='('){
                st.push(i);
            }else{
                st.pop(); // 先弹栈再计算
                if(st.isEmpty()){
                    st.push(i);
                }else{
                    // 当前下标减去栈顶下标
                    ans=Math.max(ans,i-st.peek());
                }
            }
        }
        return ans;
    }
}

d p [ i ] dp[i] dp[i]代表以s1[i]结尾(严格包括)的最长有效括号长度, d p [ i ] dp[i] dp[i]和前一个子问题的末尾 s [ i − 1 ] s[i-1] s[i1]的符号有关,需要从前往后进行递推。参考笨猪爆破组的题解.

class Solution {
    public int longestValidParentheses(String s) {
        char[] s1=s.toCharArray();
        int len=s1.length;
        // dp[i]代表以s1[i]结尾(严格包括)的最长有效括号长度
        int []dp=new int[len];
        int ans=0;
        for(int i=0;i<len;i++){
            if(s1[i]=='('){
                dp[i]=0;
            }else{
                // 当只有两个元素时
                if(i==1&&s1[i-1]=='('){
                    dp[i]=2;
                } // 当元素大于2时
                else if(i>1&&s1[i-1]=='('){
                    dp[i]=dp[i-2]+2;
                }
                else if(i>=1&&s1[i-1]==')'){
                    // 当远处存在一个有效括号且前面没有别的有效括号时 "(()())"
                    if(i-1-dp[i-1]==0&&s1[i-1-dp[i-1]]=='('){
                        dp[i]=dp[i-1]+2;
                    } //当远处存在一个有效括号且前面还有别的有效括号时  "()(())"
                    if(i-1-dp[i-1]>0&&s1[i-1-dp[i-1]]=='('){
                        dp[i]=dp[i-2-dp[i-1]]+dp[i-1]+2;
                    }
                }
            }
            ans=Math.max(ans,dp[i]);
        }
        return ans;
    }
}

队列

41.滑动窗口的平均值

双端队列,用数组模拟。

class MovingAverage {
    int[]que=new int[10010];
    int start;
    int end;
    int n;
    int sum=0;
    /** Initialize your data structure here. */
    public MovingAverage(int size) {
        n=size;
    }
    
    public double next(int val) {
        que[end++]=val;
        sum+=val;
        if(end-start>n) sum-=que[start++];
        return 1.0*sum/(end-start);
    }
}

/**
 * Your MovingAverage object will be instantiated and called as such:
 * MovingAverage obj = new MovingAverage(size);
 * double param_1 = obj.next(val);
 */
42.最近请求次数

注意到调用时间严格递增,因此可以用双端队列先进先出的性质,将时间小于t-3000的出队列,最后返回队列长度。

class RecentCounter {
    int slot;
    Deque<Integer> que;
    public RecentCounter() {
        slot=3000;
        que=new ArrayDeque<>();
    }
    
    public int ping(int t) {
        que.offer(t);
        int start=t-3000;
        while(!que.isEmpty()&&que.peek()<start) que.poll();
        return que.size();
    }
}

/**
 * Your RecentCounter object will be instantiated and called as such:
 * RecentCounter obj = new RecentCounter();
 * int param_1 = obj.ping(t);
 */
1700.无法吃午餐的学生数量
class Solution:
    def countStudents(self, students: List[int], sandwiches: List[int]) -> int:
        que=collections.deque(students)
        # 遍历所有三明治
        for i,c in enumerate(sandwiches):
            size = len(que)
            # 表示没有一个学生匹配
            flag=True
            # 对于每个三明治,遍历当前的学生队列
            for j in range(size):
                top=que.popleft()
                if(top==c):
                    # 有一个学生匹配就False
                    flag=False
                    break
                else:
                    que.append(top)
            # 最后还没有学生匹配
            if flag:
                break
        # 返回最后没有pop出去的学生
        return len(que)

在广度优先遍历和二叉树的层序遍历中,经常会出现队列的操作,可以参考我的另外两篇文章算法学习-广度优先遍历算法学习-二叉树

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网民工蒋大钊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值