汇总和整理自己碰到的运用栈和队列的相关问题,以便日后复习。
文章目录
相关知识
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[i−1]的符号有关,需要从前往后进行递推。参考笨猪爆破组的题解.
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)
在广度优先遍历和二叉树的层序遍历中,经常会出现队列的操作,可以参考我的另外两篇文章算法学习-广度优先遍历、算法学习-二叉树。