前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
这篇博客来解析栈的使用,栈也是Java里面非常重要的一个数据结构,在算法题中会经常用到,通常在学习完顺序表和链表来学习栈
栈的功能很单一,主要就三个操作,所以学起来会比链表简单一些;当然,对于一些算法题目也需要画图进行分析
这个专栏的数据结构是代码都是用Java来写的,JavaSE专栏现在已经全部更新完成,铁汁们复习基础知识时非常推荐使用,可以试一下💪💪💪
🎇个人主页:洋不写bug的博客
🎇所属专栏:数据结构专栏
🎇复习Java基础知识:Java学习之旅,从入门到进阶
🎇铁汁们对于数据结构基础的各种核心知识(不太常用的也有😆),都可以在上面的数据结构专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,栈的简介
栈是顺序表和链表来实现的,只不过加了一些限制
栈是一种特殊的线性表,只允许在固定的一段插入和删除元素,就好像一个书箱,书一本一本放进去,最后放的书在最上面,要想拿到底部的书,就要把压在其上面的书都拿出来

栈禁止了顺序表/链表的各种增删改查操作,只支持三个操作:入栈,出栈,取栈顶元素,可以把栈看作只支持尾删、尾插,获取尾部元素的顺序表/链表
那既然已经有了链表/顺序表,功能那么全面,为什么还要发明栈呢?
在编程中,“灵活”是贬义词,”呆板“是褒义词,一个东西越灵活,就越容易出错(例如C语言中的指针),顺序表和链表的功能很丰富,就比较灵活
栈就是针对特定的场景,对顺序表的功能做出限制,从而大大降低了出错的概率
2,栈的使用
标准库中栈的集合类是Stack,入栈就使用push方法,出栈就使用pop方法,pop方法会返回出栈的元素(最后入栈的元素在栈顶,出栈时先出)
import java.util.Stack;
public class Test {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
stack.push("aaa");
stack.push("bbb");
stack.push("ccc");
String s = stack.pop();
System.out.println(s);
s = stack.pop();
System.out.println(s);
s = stack.pop();
System.out.println(s);
}
}

如果栈为空,使用pop进行出栈,就会报出“EmptyStackException”(空栈异常)
import java.util.Stack;
public class Test {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
String s = stack.pop();
System.out.println(s);
}
}

peek方法是获取栈顶元素,元素不出栈,如果对于空栈使用peek方法,同样会报出“空栈异常”
import java.util.Stack;
public class Test {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
String s = stack.peek();
System.out.println(s);
}
}

栈还有个size()方法,统计栈中元素的长度,还有个empty()方法,判断栈是否为空
import java.util.Stack;
public class Test {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
stack.push("aaa");
stack.push("bbb");
stack.push("ccc");
System.out.println(stack.size());
System.out.println(stack.empty());
}
}

栈可以改变元素的序列,下面这个题目,让判断不可能的出栈序列,可以画个图分析下(熟练就不用画了)

答案就是C,如下图,把3取出来之后,因为之前没有取2,那2一定是压在1的上面的,是不可能在不取2的情况下把1取出来的

3,栈的模拟实现
栈的模拟实现很简单,用数组来保存元素,搞个size变量,统计当前栈中的元素个数,这里我们就模拟实现个存储元素类型为String的栈
使用无参构造方法默认数组的长度就是1000,也可以使用有参来指定数组长度,再写个resize方法,进行扩容操作
public class MyStack {
private String[] arr;
private int size;
public MyStack() {
arr = new String[1000];
size = 0;
}
public MyStack(int capacity) {
arr = new String[capacity];
size = 0;
}
public void resize(){
String[] newArr = new String[size * 2];
for(int i = 0;i < size;i++){
newArr[i] = arr[i];
}
arr = newArr;
}
}
进行入栈操作时判断下数组有没有满,如果满了就进行扩容操作,接着把元素存到数组中即可
public void push(String val){
if(size == arr.length){
resize();
}
arr[size] = val;
size++;
}
重写个toString方法,接着在main方法中测试一下
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
for(int i = 0;i < size;i++){
stringBuilder.append(arr[i]);
if(i < size - 1){
stringBuilder.append(",");
}
}
stringBuilder.append("]");
return stringBuilder.toString();
}
public class Test {
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push("aaa");
myStack.push("bbb");
myStack.push("ccc");
System.out.println(myStack);
}
}

pop方法和peek方法要先判断下数组中有没有元素(也就是栈是否为空),不为空就取出元素,pop方法多加了个size–
public String pop(){
if(size == 0){
throw new RuntimeException("Stack is empty!");
}
String elem = arr[size - 1];
size--;
return elem;
}
public String peek(){
if(size == 0){
throw new RuntimeException("Stack is empty!");
}
String elem = arr[size - 1];
return elem;
}
empty方法就判断个size是否等于0即可
public boolean empty(){
return size == 0;
}
这里聊个题外话,入栈4个元素,接着pop五次,最后一次就会报错,但是铁汁们在IDEA中运行几次就会发现,报错信息有时候在上面,有时候在下面,这是怎么回事呢?
public class Test {
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push("aaa");
myStack.push("bbb");
myStack.push("ccc");
myStack.push("ddd");
System.out.println(myStack.pop());
System.out.println(myStack.pop());
System.out.println(myStack.pop());
System.out.println(myStack.pop());
System.out.println(myStack.pop());
}
}


打印元素是System.out.println,打印错误是System.error.println,这两个东西都是在控制台上打印的,但并不在一个通道中,不同通道代码的执行速度是不一样的,所以不一定先执行System.out.println了,就一定先打印元素🐵
4,逆序打印链表
如果想要逆序的打印一个链表,之前的解法是把链表反转一下,学习栈之后,就可以直接顺序遍历链表,把链表存储到栈中,再一 一出栈,就能得到逆序的打印
public class Node {
public String val;
public Node next;
public Node(String val) {
this.val = val;
this.next = null;
}
}
import java.util.Stack;
public class Test {
public static void reversePrint(Node head){
Node cur = head;
Stack<String> stack = new Stack<>();
while(cur!= null){
stack.push(cur.val);
cur = cur.next;
}
while (!stack.empty()){
System.out.println(stack.pop());
}
}
public static void main(String[] args) {
Node a = new Node("a");
Node b = new Node("b");
Node c = new Node("c");
Node d = new Node("d");
a.next = b;
b.next = c;
c.next = d;
d.next = null;
reversePrint(a);
}
}
链表逆序打印,还可以使用递归,递归写法特别简单,但是很不好理解,出错了也不好分析,因此还是尽量用非递归的写法
public static void reversePrint(Node head) {
if (head == null) {
return;
}
reversePrint(head.next);
System.out.println(head.val);
}
递归调用图如下

5,有效的括号
有效字符串中的左括号必须与右括号相匹配,示例5,s = “([)]”,字符串是无效的;括号是可以嵌套的,示例4,s = “([])”,字符串是有效的
这个题目使用栈就很好解决
- 创建一个栈,遍历字符串中的每个字符,如果是左括号,就放到栈中,如果是右括号,就取出栈顶的括号进行配对(如果这时候栈为空,说明是左括号是少的,就返回false)
- 配对不上就返回false,能配对的上,就继续遍历字符串,直到字符串遍历完,栈为空了(左括号和右括号的数量不一定相同),就说明字符串是有效的
class Solution {
public boolean isValid(String ss) {
Stack<Character> stack = new Stack<>();
char[] s = ss.toCharArray();
for(char ch : s){
if(ch == '[' || ch == '{' || ch == '('){
stack.push(ch);
continue;
}
if(ch == ']' || ch == '}' || ch == ')'){
if(stack.empty()){
return false;
}
char top = stack.pop();
if(top == '{' && ch == '}'){
continue;
}
if(top == '[' && ch == ']'){
continue;
}
if(top == '(' && ch == ')'){
continue;
}
return false;
}
}
if(stack.empty()){
return true;
}else{
return false;
}
}
6,逆波兰表达式求值
逆波兰表达式就是一种后缀表达式(运算符放在后面)
我们平常使用的算式就是中缀表达式,例如(1 + 2) * (3 + 4)
用后缀表达式来写,就是((1 2 +)(3 4 +) * )
逆波兰表达式就是把括号去掉,1 2 + 3 4 + *,也可以根据次序算出正确的结果,逆波兰适合用栈进行运算
创建一个栈,从前往后遍历表达式,如果是数字,就放入栈中,如果是运算符,就从栈中出栈两个元素,进行计算,计算后再放回栈中,这样最后就能按次序算出正确的结果,流程图如下所示





写代码时有一些点需要注意:
- 输入是一个字符串数组,计算时要把字符串通过Integer.parseInt转为数字,计算出的结果是int类型,还需要跟空字符串拼接,转为String类型
- 因为是字符串,所以不能用ASCII码值来判断读取到的是数字还是运算符,需要额外写个isNumber方法,来进行判断
- 读取到运算符后,需要一 一比对是哪个运算符,接着从栈中取出两个元素,如果是减法和除法,注意是后取出的元素减去/除以先取出的元素,顺序不要搞反了
class Solution {
public int evalRPN(String[] tokens) {
Stack<String> stack = new Stack<>();
for(String s : tokens){
if(isNumber(s)){
stack.push(s);
}else{
int num1 = Integer.parseInt(stack.pop());
int num2 = Integer.parseInt(stack.pop());
int result = 0;
if(s.equals("+")){
result = num2 + num1;
stack.push("" + result);
}
if(s.equals("-")){
result = num2 - num1;
stack.push("" + result);
}
if(s.equals("*")){
result = num2 * num1;
stack.push("" + result);
}
if(s.equals("/")){
result = num2 / num1;
stack.push("" + result);
}
}
}
return Integer.parseInt(stack.pop());
}
public boolean isNumber(String s){
if(!s.equals("-") && !s.equals("+") && !s.equals("*") && !s.equals("/")){
return true;
}
return false;
}
}
7,栈的压入,弹出序列
这个题目要判断按照第一个序列的顺序压入,第二个序列是否可能是弹出序列,题目中的pushV和popV是C++中的写法,这里改为pushA和popA

可以这样来搞,创建一个栈,每个序列再搞一个引用

遍历pushA,把每个元素依次入栈,每次入栈一个元素,都进行如下判定:
- 首先判断链表是否为空
- 如果不为空,再判定栈顶元素是否等与popA的当前元素,如果相等,元素出栈,popIndex右移一步,再重新回到第一步这里继续循环处理
- 如果判定链表为空或者栈顶元素不等于popA的当前元素,那就pushIndex继续向右移动,pushA中的元素继续入栈
铁汁们可以画图模拟下这个过程(单靠描述可能有点抽象),最后pushA遍历完毕后,如果栈为空,就说明都能匹配的上,返回true,否则就返回false

分析清楚后,代码写起来还是比较简单的
import java.util.*;
public class Solution {
public boolean IsPopOrder (int[] pushA, int[] popA) {
Stack<Integer> stack = new Stack<>();
int pushIndex = 0;
int popIndex = 0;
for(int i = 0;i < pushA.length;i++){
stack.push(pushA[i]);
while(!stack.empty()){
if(popA[popIndex] == stack.peek()){
stack.pop();
popIndex++;
}else{
break;
}
}
}
return stack.empty();
}
}
8,最小栈

这个题目是可以在MinStack类中使用栈的,如果用数组进行底层模拟的话,就比较麻烦了😅
重点是getMin()方法,因为元素pop后,最小元素是可能会改变的,所以搞一个min变量来记录当前元素的最小值是不可行的
创建一个stack1,再额外创建一个stack2,在stack2的栈顶中保存stack1中的最小值,如下图,先把9放入stack1中,这时候stack1中的最小值就是9,在stack2中加入9;接着把5放到stack1中,stack1的最小值就是5,把5放到stack2中


接下来把2放在stack1中,这时候stack1的最小值为2,把2放到stack2中,后续添加元素时,stack1中的最小值都是2,每次都把2加入到stack2中

这样后续stack1元素出栈时,stack2的元素也出栈,就能保证stack2栈顶的元素一定是stack1中的最小值,getMin方法直接取出stack2的栈顶元素即可
class MinStack {
private Stack<Integer> stack1 = new Stack<>();
private Stack<Integer> stack2 = new Stack<>();
public MinStack() {
}
public void push(int value) {
stack1.push(value);
if(stack2.empty()){
stack2.push(value);
return;
}
int min = stack2.peek();
if(value < min){
stack2.push(value);
}else{
stack2.push(min);
}
}
public void pop() {
stack1.pop();
stack2.pop();
}
public int top() {
return stack1.peek();
}
public int getMin() {
return stack2.peek();
}
}
结语
- 栈是数据结构学习路上非常基础却至关重要的一环,栈的使用挺简单的,主要就三个操作:出栈、入栈、获取栈顶元素,后续很多复杂算法,例如深度优先搜索、函数调用栈等底层原理都离不开它
- 下篇博客会解析队列,队列和栈一样,也是基于顺序表和链表来实现的,也是加了一些限制
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉





733



被折叠的 条评论
为什么被折叠?



