硅基计划3.0 学习总结 叁 栈和队列 初版
文章目录
- 一、栈
- 1. 模拟实现栈
- 2. 小试牛刀
- 1. 判断一个栈的出栈顺序是否为题目给定情况
- 2. 括号匹配
- 3. 逆波兰表达式求值
- 4. 求最小栈元素
- 3. 单链表实现栈
一、栈
栈说白了就是一个盒子,你先放进去的东西后拿出来,实现栈除了可以使用栈的类Stack
外,还可以使用链表
遵循先进先出,后进后出原则进行内容变化,重点是可以把递归转换成栈
我们查看其源码我们看到,它实质上是一个顺序表即数组,有压栈和出栈操作
1. 模拟实现栈
这次我们就不定义接口了,就直接写MyStack
类吧,然后实现一些方法
我们重点就来讲压栈和出栈,压栈说白了就是先检测你的数组有没有满,满了就扩容,否则我们就直接让有效元素个数对应的下标的数字等于我们压入的数字(因为数组下标和实际位置差1的特性)
出栈就是先判断数字是不是空的,如果不是我们直接让有效数字个数减少,这样我们再次压入其他数字的时候就可以把原有数字覆盖
其他方法操作下来,我们的时间复杂度都是O(1)
//MyStack类
public class MyStack {public int [] elem;public int usedSize;public MyStack() {this.elem = new int[2];}public void push(int val) {if (isFull()) {this.elem = Arrays.copyOf(elem, 2 * elem.length);}elem[usedSize] = val;usedSize++;}public boolean isFull() {return usedSize == elem.length;}public int pop(){if(isEmpty()){throw new IsEmptyException("空数组异常");}int ret = peek();usedSize --;return ret;}public boolean isEmpty(){return usedSize == 0;}public int peek(){if(isEmpty()){throw new IsEmptyException("空数组异常");}return elem[usedSize-1];}public int size(){if(isEmpty()){throw new IsEmptyException("空数组异常");}return usedSize;}@Overridepublic String toString() {return "MyStack{" +"elem=" + Arrays.toString(elem) +", usedSize=" + usedSize +'}';}
}//测试类
public static void main(String[] args) {MyStack myStack = new MyStack();System.out.println("压栈");myStack.push(10);myStack.push(12);myStack.push(15);System.out.println(myStack);System.out.println("查看栈顶元素:"+myStack.peek());System.out.print("出栈:");System.out.print(myStack.pop()+" ");System.out.print(myStack.pop()+" ");System.out.print(myStack.pop());System.out.println();System.out.println("栈顺序"+myStack);Stack<Integer> stack = new Stack<>();}
打印结果就是这样,看起来一切正常
2. 小试牛刀
1. 判断一个栈的出栈顺序是否为题目给定情况
题目链接
我们首先来讲讲这道题的核心思想是什么,根据题目要求
我们让i
下标等于pushV,j
下标等于popV
我们就以:pushV:1 2 3 4 5 和popV:4 3 5 1 2来讲解
我们先定义一个栈,叫stack
,把右侧看做栈顶
好,我们定义好了i
下标和j
下标,此时它们分别代表数字1和数字4
我们比较i
下标的数字和j
下标数字是否匹配,发现不匹配,将i
下标当前指向的元素放入栈中,此时我们得栈数字有[1]
,此时我们得i
下标++,发现还是和j
下标数字不匹配,继续放元素到栈中,继续++
直到[1,2,3,4]
,发现栈中的栈顶元素为4,和j
下标元素匹配,那好,我们就弹出栈顶元素4,此时栈中元素为[1,2,3]
,此时j
下标++向后走,走到数字3位置,此时我们再判断,栈中元素是空的吗,不是,我们还要继续判断相等,此时我们我们发现栈顶元素3和j
下标当前的数字3匹配,我们栈顶元素继续弹出,k++一下向后走,走到数字5的位置,此时栈中元素为[1,2]
此时再判断,栈中元素是空的吗,不是,那我们还要继续判断相等,此时这一轮循环走完了,我们的i
要++一下,走到数字5的位置,此时栈顶元素是2,和当前j
下标的数字不一样,那我们是不是继续要从pushV中放数字,好,我们放入数字5,此时栈中元素为[1,2,5]
,栈顶元素和此时j
下标的元素相等,我们此时弹出栈顶元素,此时j
++向后走一位,走到1的位置,此时栈中元素为[1,2]
,此时栈顶元素和j
下标并不匹配,这一轮循环走完,i
++了一下,此时i
就超过了原来的数组范围,大的循环走了出来,此时我们判断栈中是否还存有元素,发现还是有元素,那我们就可以知道“4 3 5 1 2”这个出栈顺序就不是原有合理顺序
如果出栈顺序是这样popV:4 5 3 2 1 ,一样的逐一的进行判断,发现最后栈是空的,那它就是和题目要求成立
但是有个特殊情况,比如这样:如果每个都匹配的情况 5 4 3 2 1 和 1 2 3 4 5
那内部进行判断的时候我们就要加上限制条件栈不可以为空并且j
位置不能超过原数组范围,因为此时下标出了数组范围再取元素就空数组异常了,i
不用判断是因为i
有循环进行范围约束而j
却没有
public class Solution {/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param pushV int整型一维数组 * @param popV int整型一维数组 * @return bool布尔型*/public boolean IsPopOrder (int[] pushV, int[] popV) {Stack <Integer> stack = new Stack<>();int i = 0;int j = 0;for(i = 0;i<pushV.length;i++){//大循环stack.push(pushV[i]);//还要考虑如果每个都匹配的情况 5 4 3 2 1 和 1 2 3 4 5while(!stack.empty() && j<= popV.length && stack.peek() == popV[j]){//内部循环stack.pop();j++;}}//看看stack空不空就行了return stack.empty();}
}
2. 括号匹配
题目链接
这个题目核心问题就是如何去直到括号在哪以及括号的个数的问题
我们就拿( [ ] )
举例,我们定义一个栈,把左括号存到里面去,栈中元素和右括号进行匹配,如果相等则弹出栈顶元素,以此类推,如果在弹出过程中第一次遇到不匹配的情况,不要犹豫直接返回false,肯定不是匹配了的
我们再拿(( )
情况举例,如果右括号你都匹配完了左括号还有剩余,那也是不匹配
还有( ))
情况,左括号遍历完了右括号还有剩余,也是不匹配
class Solution {public boolean isValid(String s) {Stack <Character> stack = new Stack<>();for(int i = 0 ;i<s.length();i++){//取出字符char ch = s.charAt(i);if(ch == '(' || ch == '{' || ch == '['){stack.push(ch);}else{//判空if(stack.empty()){return false;}//比较char chs = stack.peek();if(chs == '(' && ch == ')' || chs == '{' && ch == '}' || chs == '[' && ch == ']'){stack.pop();}else{//说明剩下的右边的元素根本就不是括号,可能是其他字符//比如((??这样return false;}}}//如果此时for循环走完了你的栈中还有元素,也就是说没有匹配完if(!stack.empty()){return false;}//当上述所有条件头部满足,说明就删完了,匹配成功return true;}
}
3. 逆波兰表达式求值
我们首先明白什么是逆波兰表达式,我们正常的表达式是这样的,比如2*(3+5)
但是计算机的计算器在计算的时候是不知道我们人类的运算符优先规则去算的,那是怎么搞的呢
你看,逆波兰表达式核心就是把符号往后移,看我演示
设M=(3+5)M = (3+5)M=(3+5),则M的逆波兰表达式为35+35+35+,之后整体式子的逆波兰表达式是$ 2M* $,带入M后就是$2 35+ * ,那此时计算就是,那此时计算就是,那此时计算就是 3+5=8 $,再2∗8=162*8=162∗8=16
画图就是这样的
那我们直接来写题吧
class Solution {public int evalRPN(String[] tokens) {Stack<Integer> stack = new Stack();for(int i = 0;i<tokens.length;i++){if(!isOperator(tokens[i])){stack.push(Integer.parseInt(tokens[i]));}else{//注意两个数的出栈顺序int num1 = stack.pop();int num2 = stack.pop();switch(tokens[i]){case "+"://算出的结果要压入栈中stack.push(num2+num1);break;case "-":stack.push(num2-num1);break;case "*":stack.push(num2*num1);break;case "/":stack.push(num2/num1);break;}}}return stack.pop();}private boolean isOperator(String operator){if(operator.equals("+")||operator.equals("-")||operator.equals("*")||operator.equals("/")){return true;}return false;}
}
4. 求最小栈元素
题目链接
就是要求栈中最小的元素,因为栈不可以倒着遍历,因此我们只能用两个栈,一个普通栈存数字,另一个最小值栈存放当前栈中最小数字
当我不断的压普通栈,如果压的数字有比当前存放最小值的栈的栈顶元素小的话,最小值的栈顶元素就变成当前压入普通栈大的元素
这样当我普通栈出栈的时候,如果比我最小栈栈顶元素一样,那就一起出,否则就只是普通栈出
这样当我后续删除普通栈元素时,每一次都能在最小栈中找到当前普通栈中的最小值
class MinStack {private Stack<Integer> stack;private Stack<Integer> minStack;;public MinStack() {stack = new Stack<>();minStack = new Stack<>();}public void push(int val) {//先压入普通栈再判断stack.push(val);//判空if(minStack.empty()){//最小值栈首次压入元素minStack.push(val);}else{//只有当压入普通栈的元素比最小值栈的栈顶元素小的时候//此时压入普通栈的数字才要同时压入最小值栈if( val <= minStack.peek()){minStack.push(val);}}}public void pop() {//判空if(stack.empty()){return;}//如果普通栈栈顶元素和最小值栈栈顶元素相等,要一起弹出int val = stack.pop();if(val == minStack.peek()){minStack.pop();}}public int top() {//判空if(stack.empty()){return -1;}return stack.peek();}public int getMin() {return minStack.peek();}
}
3. 单链表实现栈
入栈我们采用头插法,出栈我们采用删除头节点方法,此时它们时间复杂度都是O(1)
假如你入栈采用尾插法,还要遍历去找尾巴,麻烦,时间复杂度是O(n)
假如你对单链表的尾节点加上last
引用,入栈没问题了可是你出栈还不是要删除尾节点,还不是要找其前一个节点,时间复杂度还是O(n)
但是,如果你采用双向链表,就可以实现,而且双向链表本身就有实现Deque接口,我们说过这是跟栈和队列有关的接口
public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable
使用双向列表,我们就能保证出栈和入栈的时间复杂度都是O(1)