目录
一.栈
1 概念
栈:一种特殊的线性表,一种数据结构.其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
2.虚拟机栈
JVM stack 只是JVM只是JVM当中的一小块内存,该内存一般用来存放数据,如:局部变量
调用函数的时候,我们会为这个函数开辟一块内存,叫做栈帧,在JVM虚拟栈开辟
二.栈的实现(数组-顺序表)
public class MyStack {
public int[] elem;
public int usedSize;
public MyStack(){//实现构造方法
this.elem=new int[10];
}
public void push(int val){//入栈
if(isFull()){//判读是否满了.没满扩容
this.elem= Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[usedSize]=val;
this.usedSize++;
}
public boolean isFull(){
return this.usedSize==this.elem.length;
}
public int pop() {
if(isEmpty()) {
throw new RuntimeException("栈为空!");
}
int oldVal = this.elem[usedSize-1];
this.usedSize--;
return oldVal;
}
public boolean isEmpty(){
return this.usedSize==0;
}
public int peek(){//查看第一个
if(isEmpty()){//判断是否是空的
throw new RuntimeException("栈为空!");
}
return this.elem[usedSize-1];
}
}
为简便先用int
java底层也是数组,只要把加上泛型即可,他继承与Vector.无参的构造方法是空的,
追根溯源发现,他给数组的初始化也是10,因为'
我们改变int变成泛型发现泛型是不能直接实例化的,我们可以仿造源码的做法
public class MyStack<E> {
public E[] elem;
public int usedSize;
public MyStack(){//实现构造方法
this.elem=new E[10];
}
public void push(E val){//入栈
if(isFull()){//判读是否满了.没满扩容
this.elem= Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[usedSize]=val;
this.usedSize++;
}
public boolean isFull(){
return this.usedSize==this.elem.length;
}
public E pop() {
if(isEmpty()) {
throw new RuntimeException("栈为空!");
}
E oldVal = this.elem[usedSize-1];
this.usedSize--;
return oldVal;
}
public boolean isEmpty(){
return this.usedSize==0;
}
public E peek(){//查看第一个
if(isEmpty()){//判断是否是空的
throw new RuntimeException("栈为空!");
}
return this.elem[usedSize-1];
}
}
三.栈的实现(单向链表)
源码里的栈要求是先进后出,并且入栈和出栈的时间复杂度是O(1)
因为链表可以头插也可以尾插,那么我们用头插法来实现入栈出栈还是尾插法呢
假设入栈是尾插法,那么时间复杂度是O(n),因为尾插法每次都要找尾巴.
这时候我们可以试着在链表尾部定义一个尾结点.插入的时候直接找尾结点即可
这样时间复杂度就是O(1)
但是单向链表的增删查改都需要找到前驱结点.所以需要遍历找这个节点的前驱.这样时间复杂度又是
o(1)
所以我们的解决方案就是双向链表或者是头插法.出栈入栈删除和添加头结点即可.
class MyNode{
public int val;
public MyNode next;
public MyNode(int val) {
this.val = val;
}
}
public class MyStackLisk {
public MyNode head;
public MyStackLisk(){
this.head=null;
}
public void push(int val){
MyNode node=new MyNode(val);
if(isEmpty()){
this.head=node;
}
node.next=this.head;
this.head=node;
}
public boolean isEmpty(){
return this.head==null;
}
public int pop(){
if(isEmpty()){
throw new RuntimeException("kong");
}
MyNode node=this.head;
this.head=node.next;
return node.val;
}
public int peep(){
if(isEmpty()){
throw new RuntimeException("kong");
}
return this.head.val;
}
}
四.栈的实现(双向链表)
class Node{
public int val;
public Node next;
public Node pre;
public Node(int val) {
this.val = val;
}
}
public class MyStacklisks {
public Node head;
public MyStacklisks(){
this.head=null;
}
public void push(int val){
Node node=new Node(val);
if(isEmpty()){
this.head=node;
}
node.next=this.head;
this.head.pre=node;
this.head=node;
this.head.pre=null;
}
public boolean isEmpty(){
return this.head==null;
}
public int pop(){
if(isEmpty()){
throw new RuntimeException("kong");
}
Node node=this.head;
this.head=node.next;
this.head.pre=null;
return node.val;
}
public int peep(){
if(isEmpty()){
throw new RuntimeException("kong");
}
return this.head.val;
}
}
五.栈的不可能输出序列
1.选择题
画图可知,不再赘述
2.力扣习题
思路:构建两个循环,先让第一个序列按数组顺序压入,再进入第二个序列,看栈顶与第二个序列的第一个元素是否相同,如果相同,直接pop.让第二个序列往后走一个位置,继续比较,如果不同,或者栈是空的,直接跳出循环,进入大循环,继续.循环结束的情况就是第一个序列全部PUSH了
最后看栈是否是空的,如果不是,就说明不是,
因为如果不是空的,就说明第二个序列遍历完了,但是第一个还有元素push在里面,没有与他相同的
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack=new Stack<>();
int j=0;
for(int i =0;i<pushed.length;i++){
stack.push(pushed[i]);
while(stack.peek()==popped[j]){
stack.pop();
j++;
if(stack.empty()){
break;
}
}
}
if(!stack.empty()){
return false;
}
return true;
}
}
六.中缀表达式和后缀表达式
中缀表达式、前缀表达式、后缀表达式
1)中缀表达式:操作运算符在操作数中间
(3+2)4-62)前缀表达式:波兰式,操作运算符在操作数之前,如-+3246。
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 op 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果
1.中缀表达式 ----> 前缀表达式:
- 初始化两个栈,符号栈s1、存放中间结果栈s2 - **从右至左扫描**中缀表达式 - 遇到操作数,将其压入栈s2 - 遇到操作运算符,则需要比较其与s1栈顶操作符的优先级 1.若s1为空,或者栈顶运算符为**右括号)**,则直接入栈 2.优先级高于栈顶运算符,也直接入栈 3.否则,将s1栈顶运算符弹出压入s2,再转到步骤1进行判断 - 遇到括号: 1.如果遇到**右括号**,直接压入栈 2.如果遇到**左括号**,则依次弹出s1栈顶运算符,压入s2,直到遇到右括号为止,并将这一对括号丢弃 - 重复2-5,直至表达式左边 - 将s1中剩余运算符依次弹出并压入s2 - 依次弹出s2的元素,即为中缀表达式
2.后缀表达式(逆波兰式):操作运算符在操作数之前,
如3 4 + 5 × 6 -。与前缀表达式类似,区别是从左至右扫描。
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 op 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
中缀表达式 ----> 后缀表达式:
- 初始化两个栈,符号栈s1、存放中间结果栈s2
- **从左至右**扫描中缀表达式
- 遇到操作数,将其压入栈s2
- 遇到操作运算符,则需要比较其与s1栈顶操作符的优先级
1.若s1为空,或者栈顶运算符为**左括号(**,则直接入栈
2.优先级高于栈顶运算符,也直接入栈
3.否则,将s1栈顶运算符弹出压入s2,再转到步骤1进行判断
- 遇到括号:
1.如果遇到**左括号**,直接压入栈
2.如果遇到**右括号**,则依次弹出s1栈顶运算符,压入s2,直到遇到右括号为止,并将这一对括号丢弃
- 重复2-5,直至表达式右边
- 将s1中剩余运算符依次弹出并压入s2
- 依次弹出s2的元素,即为中缀表达式
表达式:3 4 + 5 × 6 -
- 从左至右扫描后缀表达式,依次将3、4依次压入堆栈
- 遇到运算符 + ,弹出栈顶和次栈顶的两个数4、3,计算4+3的值为7,压入栈
- 遇到5,入栈
- 遇到x,弹出栈顶和次栈顶的两个数5、7,计算7*5=35,入栈
- 遇到6,入栈
- 遇到-,弹出栈顶和次栈顶的两个数6、36,计算35-6=29
3.力扣习题
思路:怎样判断是否是运算符还是数字
遍历数组
就建立一个栈把数字放在栈里
如果是数字就先压入栈,碰到运算符就弹出两个元素,先弹出的是运算符右边的操作数
因为按照中缀表达式的压入顺序
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack=new Stack();
int num1=0;int num2=0;
for(int i=0;i<tokens.length;i++){
String s=tokens[i];
if(!isOperation(s)){
stack.push(Integer.parseInt(s));
}else{
num2=stack.pop();
num1=stack.pop();
switch(s){
case "+":
stack.push(num1+num2);
break;
case "-":
stack.push(num1-num2);
break;
case "*":
stack.push(num1*num2);
break;
case "/":
stack.push(num1/num2);
break;
}
}
}
return stack.pop();
}
public boolean isOperation(String s){
if(s.equals("+")||s.equals("-")||s.equals("*")||s.equals("/")){
return true;
}
return false;
}
}
这里要注意,就是避免内存冗余,可以在进入Switch语句的时候之前就把pop值放在num里不要忘记break
还有还要把字符转为整数包装类:Integer.parseInt(s)
七.有效括号
首先要判断字符串长度是否是偶数,不是偶数一定不匹配
有效字符串
1.左括号必须有相同类型的右括号闭合
2.左括号必须以正确的顺序闭合
不匹配的情况
如果遇到左括号,就入栈
如果遇到有括号,就跟栈顶匹配
如果匹配.就pop
如果不匹配就直接输出false
当字符串遍历完
栈还有元素,就说明又不匹配的元素.就说明左括号多
如果遇到右括号多的情况,也就是在循环的时候.栈已经空了.这种情况直接false
class Solution {
public boolean isValid(String s) {
if(s.length()%2==1) return false;//如果是奇数直接返回假
Stack<Character> stack=new Stack();
for(int i = 0 ;i<s.length();i++){
char ch = s.charAt(i);
if(isLeft(ch)){
stack.push(ch);//如果是左括号就入栈
}else{//这是右括号的情况
if(stack.empty()){
return false;//如果是右括号的时候,但是栈为空,就说明右括号多,
}
char top=stack.peek();
if(top == '{' && ch == '}' || top == '[' && ch == ']' || top == '(' && ch == ')'){//如果匹配
stack.pop();就排出
}else{
return false;//不匹配就不对
}
}
}
if(stack.empty()){
return true;
}else{
return false;
}
}
public boolean isLeft(char top){
if(top == '{' || top == '[' || top == '(' ){
return true;
}
return false;
}
八.设计最小栈
设计两个栈,一个是正常放入,一个只放最小元素的栈,
第一次两个栈都正常放
第二次的时候放入普通栈,并且两个栈顶元素比较,如果最小元素大.那么就把那个元素也同样放入
这里有个问题,如果相同,放不放
答案是放的
因为出栈的时候,如果不是最小元素就只要拿走普通栈的
但是如果是最小元素那就两个都要拿走
但是如果两个相同的最小,第一次拿走了,最小元素也拿走了,第二次就没得拿了
class MinStack {
private Stack<Integer> nomalStack;
private Stack<Integer> miniStack;//要放在外面不然其他位置永不了/
public MinStack() {
nomalStack=new Stack<>();
miniStack=new Stack<>();
}
public void push(int val) {
nomalStack.push(val);
if(miniStack.empty()){
miniStack.push(val);
}else{
if(val<=getMin()){//等于也要成立
miniStack.push(val);
}
}
}
public void pop() {
if(top()==getMin()){
miniStack.pop();
nomalStack.pop();
}else{
nomalStack.pop();
}
}
public int top() {
return nomalStack.peek();
}
public int getMin() {
return miniStack.peek();
}
}
我这里出现的问题就是我在构造方
我这里出现的问题就是我在构造方法里定义stack
这样导致其他的方法都用不了,因为在方法里,那他的使用范围就只在方法里.