一、栈的一个实际需求
例如,请输入一个表达式计算式:[7*2*2-5+1-5+3-3] 点击计算【如下图】
请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。--------->栈
二、栈的基本介绍
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
- 图解方式说明入栈(push)和出栈(pop)的原理
三、栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth一first)搜索法。
四、数组模拟栈
用数组模拟栈的使用,由于栈是一种有序列表, 当然可以使用数组的结构来储存栈的数据内容, 下面我们就用数组模拟栈的出栈,入栈等操作。
代码实现:
package com.marden.demo2;
public class ArrayStackDemo {
public static void main(String[] args) {
ArrayStack arrayStack=new ArrayStack(5);
arrayStack.show();
System.out.println("*******************");
arrayStack.push(3);
arrayStack.push(5);
arrayStack.push(10);
arrayStack.show();
System.out.println("*******************");
arrayStack.pop();
arrayStack.show();
System.out.println("*******************");
}
}
class ArrayStack{
private int maxSize;
private int [] stack;
private int top;
public ArrayStack(int maxSize){
this.maxSize=maxSize;
stack=new int [this.maxSize];
top=-1;
}
//判断栈满
public boolean isFull(){
return top==maxSize-1;
}
//判断栈空
public boolean isEmpty(){
return top==-1;
}
//入栈
public void push(int value){
//首先判断是否栈满,若栈满则无法入栈
if(isFull()){
System.out.println("栈满,无法入栈");
}else{
top++;
stack[top]=value;
}
}
//出栈
public int pop(){
//首先判断是否栈空,若栈空则无法出栈
if(isEmpty()){
System.out.println("栈空,无法出栈");
return -999;
}else{
int temp=stack[top];
top--;
return temp;
}
}
public void show(){
//首先判断是否栈空,若栈空则无法显示
if(isEmpty()){
System.out.println("栈空,无法显示数据");
}else{
for(int i=top;i>=0;i--){
System.out.println(stack[i]);
}
}
}
}
五、链表模拟栈
代码实现:
package com.marden.demo2;
public class ListStackDemo {
public static void main(String[] args) {
ListStack listStack=new ListStack();
listStack.show();
System.out.println("***************");
listStack.push(2);
listStack.push(5);
listStack.show();
System.out.println("***************");
listStack.pop();
listStack.show();
System.out.println("***************");
listStack.pop();
listStack.show();
System.out.println("***************");
}
}
class ListStack{
ListNode top;
public ListStack(){
ListNode listNode=new ListNode();
top=listNode;
}
//判断栈空
public boolean isEmpty(){
return top.next==null;
}
//入栈
public void push(int value){
ListNode temp =new ListNode(value);
temp.next=top;
top=temp;
}
//出栈
public int pop(){
if(isEmpty()){
System.out.println("栈空,无法出栈");
return -999;
}else{
int temp=top.data;
ListNode p=top.next;
top.next=null;
top=p;
return temp;
}
}
public void show(){
if(isEmpty()){
System.out.println("栈空,无法显示数据");
}else{
ListNode temp=top;
while(temp.next!=null){
System.out.println(temp.data);
temp=temp.next;
}
}
}
}
class ListNode{
int data;
ListNode next;
public ListNode(){
}
public ListNode(int data){
this.data=data;
}
}
六、栈实现综合计算器(中缀表达式)
需求:
思路分析:
代码实现:
package com.marden.demo3;
//单位数表达式
public class Calculator {
public static boolean isOper(char value){
return value=='+'||value=='-'||value=='*'||value=='/';
}
public static int priority(int i){
if(i=='*'|| i=='/'){
return 1;
}else if(i=='+'|| i=='-'){
return 0;
}else{
return -1;
}
}
public static int cal(int num1,int num2,int oper){
int temp = 0;
switch (oper) {
case '+':
temp= num1+num2;
break;
case '-':
temp= num2-num1;
break;
case '*':
temp= num1*num2;
break;
case '/':
temp= num2/num1;
break;
default:
break;
}
return temp;
}
public static void main(String[] args) {
String expression="3*6/2-1+3*3";
ArrayStack numStack=new ArrayStack(10); //数字栈
ArrayStack operStack=new ArrayStack(10); //符号栈
int index=0;
int num1=0;
int num2=0;
int oper=0;
int rs=0;
//循环遍历字符串的每个位置
while(true){
char temp=expression.substring(index, index+1).charAt(0);
//判断字符是否为数字,如果是数字则直接压入数字栈
if(!isOper(temp)){
numStack.push(temp-48); //次数获取到的是字符,根据ASCII表,转成对应的数字
}else{
//如果字符为符号,则分两种情况
//1.如果符号栈为空,则直接入栈
if(operStack.isEmpty()){
operStack.push(temp);
}else{
//2.如果符号栈不为空,则分两种情况
//2.1 如果当前符号的优先级比符号栈中栈顶的符号优先级高,则直接入栈
if(priority(temp)>priority(operStack.topElement())){
operStack.push(temp);
}else{
//2.2 如果当前符号的优先级比符号栈中栈顶的符号优先级低或者相同,则从数字栈中出栈两个数字,从符号栈中出栈一个符号进行计算,并将计算结果压入数字栈
num1=numStack.pop();
num2=numStack.pop();
oper=operStack.pop();
rs=cal(num1, num2, oper);
numStack.push(rs);
//符号直接入栈
operStack.push(temp);
}
}
}
index++;
if(index==expression.length()){
break;
}
}
//遍历完字符串,对数字栈和符号栈进行计算,直到符号栈为空,此时数字栈中的最后一个元素即为最后的计算结果
while(!operStack.isEmpty()){
num1=numStack.pop();
num2=numStack.pop();
oper=operStack.pop();
rs=cal(num1, num2, oper);
numStack.push(rs);
}
int sqc=numStack.pop();
System.out.println(expression+"="+sqc);
}
}
//使用数组存储形式的栈
class ArrayStack{
private int maxSize;
private int [] stack;
private int top;
public ArrayStack(int maxSize){
this.maxSize=maxSize;
stack=new int [this.maxSize];
top=-1;
}
//查看栈顶元素的内容
public int topElement(){
return stack[top];
}
//判断栈满
public boolean isFull(){
return top==maxSize-1;
}
//判断栈空
public boolean isEmpty(){
return top==-1;
}
//入栈
public void push(int value){
//首先判断是否栈满,若栈满则无法入栈
if(isFull()){
System.out.println("栈满,无法入栈");
}else{
top++;
stack[top]=value;
}
}
//出栈
public int pop(){
//首先判断是否栈空,若栈空则无法出栈
if(isEmpty()){
System.out.println("栈空,无法出栈");
return -999;
}else{
int temp=stack[top];
top--;
return temp;
}
}
//打印栈中的内容
public void show(){
//首先判断是否栈空,若栈空则无法显示
if(isEmpty()){
System.out.println("栈空,无法显示数据");
}else{
for(int i=top;i>=0;i--){
System.out.println(stack[i]);
}
}
}
}
效果演示:(计算3*6/2-1+3*3)
分析:
上述代码实现了中缀表达式的逻辑运算,但是只适用于个位数的计算,未考虑到多位数的计算。
优化代码:(多位数的中缀表达式计算)
package com.marden.demo3;
//多位数表达式
public class Calculator1 {
//判断字符是否为操作符
public static boolean isOper(char value){
return value=='+'||value=='-'||value=='*'||value=='/';
}
//根据操作符,返回其优先级,数值越大,优先级越高
public static int priority(int i){
if(i=='*'|| i=='/'){
return 1;
}else if(i=='+'|| i=='-'){
return 0;
}else{
return -1;
}
}
//根据两个数值和操作符进行逻辑运算
public static int cal(int num1,int num2,int oper){
int temp = 0;
switch (oper) {
case '+':
temp= num1+num2;
break;
case '-':
temp= num2-num1;
break;
case '*':
temp= num1*num2;
break;
case '/':
temp= num2/num1;
break;
default:
break;
}
return temp;
}
public static void main(String[] args) {
String expression="32+68-2*11";
ArrayStack numStack=new ArrayStack(10); //数字栈
ArrayStack operStack=new ArrayStack(10); //符号栈
int index=0;
int num1=0;
int num2=0;
int oper=0;
int rs=0;
char [] charTemp=expression.toCharArray();
while(true){
char temp=expression.substring(index, index+1).charAt(0);
//判断字符是否为数字,如果是数字则直接压入数字栈
if(!isOper(temp)){
//numStack.push(temp-48); //获取到的是字符,根据ASCII表,转成对应的数字
//如果是多位数,则需找到完整的数字,首先确定完整数字的边界
int end=charTemp.length-1;
for(int i=index;i<charTemp.length;i++){
if(isOper(charTemp[i])){
end=i-1;
break;
}
}
//将字符类型的数据转换成真实数字
int multiNum=0;
int count=0;
for(int i=end;i>=index;i--){
multiNum+=(charTemp[i]-48)*Math.pow(10, count); //根据ASCII表,将获取的字符转成对应的数字
count++;
}
//System.out.println(multiNum);
numStack.push(multiNum);
index=end;
}else{
//如果字符为符号,则分两种情况
//1.如果符号栈为空,则直接入栈
if(operStack.isEmpty()){
operStack.push(temp);
}else{
//2.如果符号栈不为空,则分两种情况
//2.1 如果当前符号的优先级比符号栈中栈顶的符号优先级高,则直接入栈
if(priority(temp)>priority(operStack.topElement())){
operStack.push(temp);
}else{
//2.2 如果当前符号的优先级比符号栈中栈顶的符号优先级低或者相同,则从数字栈中出栈两个数字,从符号栈中出栈一个符号进行计算,并将计算结果压入数字栈
num1=numStack.pop();
num2=numStack.pop();
oper=operStack.pop();
rs=cal(num1, num2, oper);
numStack.push(rs);
//符号直接入栈
operStack.push(temp);
}
}
}
index++;
if(index==expression.length()){
break;
}
}
while(!operStack.isEmpty()){
num1=numStack.pop();
num2=numStack.pop();
oper=operStack.pop();
rs=cal(num1, num2, oper);
numStack.push(rs);
}
int sqc=numStack.pop();
System.out.println(expression+"="+sqc);
}
}
class ArrayStack1{
private int maxSize;
private int [] stack;
private int top;
public ArrayStack1(int maxSize){
this.maxSize=maxSize;
stack=new int [this.maxSize];
top=-1;
}
//查看栈顶元素的内容
public int topElement(){
return stack[top];
}
//判断栈满
public boolean isFull(){
return top==maxSize-1;
}
//判断栈空
public boolean isEmpty(){
return top==-1;
}
//入栈
public void push(int value){
//首先判断是否栈满,若栈满则无法入栈
if(isFull()){
System.out.println("栈满,无法入栈");
}else{
top++;
stack[top]=value;
}
}
//出栈
public int pop(){
//首先判断是否栈空,若栈空则无法出栈
if(isEmpty()){
System.out.println("栈空,无法出栈");
return -999;
}else{
int temp=stack[top];
top--;
return temp;
}
}
public void show(){
//首先判断是否栈空,若栈空则无法显示
if(isEmpty()){
System.out.println("栈空,无法显示数据");
}else{
for(int i=top;i>=0;i--){
System.out.println(stack[i]);
}
}
}
}
效果演示:(计算32+68-2*11)
七、表达式计算
- 前缀表达式(波兰式):前缀表达式的运算符位于操作数之前,举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6
- 中缀表达式:中缀表达式就是常见的运算表达式,如(3+4)×5-6
- 后缀表达式(逆波兰式):与前缀表达式相似,只是运算符位于操作数之后,举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
前缀表达式的计算机求值:
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
- 从右至左扫描,将6、5、4、3压入堆栈
- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
- 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
中缀表达式的计算机求值:
中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)
后缀表达式的计算机求值:
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
- 从左至右扫描,将3和4压入堆栈;
- 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
- 将5入栈;
- 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
- 将6入栈;
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
逆波兰计算机的实现:
我们完成一个逆波兰计算器,要求完成如下任务:
- 输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
- 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
package com.marden.demo4;
import java.util.ArrayList;
import java.util.Stack;
//逆波兰表达式的计算
public class PolandNotation {
public static void main(String[] args) {
String expression="3 4 + 5 * 6 -";
//1.将给定的字符串分割,并存入到ArrayList中
ArrayList<String> splitList=getStringList(expression);
//2.将分割好的字符串ArrayList进行计算,并返回最后计算结果
int result=cal(splitList);
System.out.println("逆波兰表达式:"+expression+"的计算结果为:"+result);
}
//将给定的字符串,分割成为字符串数组,并存入到ArrayList
public static ArrayList<String> getStringList(String expression){
String [] splitArray=expression.split(" "); //根据空格,将字符串分割为字符串数组
ArrayList<String> list=new ArrayList<String>();
for(String str:splitArray){
list.add(str);
}
return list;
}
//遍历ArrayList,并利用栈数据结构,完成逆波兰表达式的计算
public static int cal(ArrayList<String> list){
Stack<String> stack=new Stack<>();
for(String str:list){
//判断list中的元素是否为数字,如果为数字,则直接入栈
if(str.matches("\\d+")){ //根据正则表达式,判断是否为数字(单位数字或者多位数字)
stack.push(str);
}else{
//如果为操作符,则出栈两次,并进行计算
int num2=Integer.parseInt(stack.pop());
int num1=Integer.parseInt(stack.pop());
int rs=0;
if(str.equals("+")){
rs=num1+num2;
}else if(str.equals("-")){
rs=num1-num2;
}else if(str.equals("*")){
rs=num1*num2;
}else if(str.equals("/")){
rs=num1/num2;
}else{
throw new RuntimeException("操作符有误!");
}
stack.push(""+rs);
}
}
//遍历完成后,栈中剩余的最后一个数值即为最后的计算结果
return Integer.parseInt(stack.pop());
}
}
效果展示:
中缀表达式转后缀表达式
计算机使用中缀表达式进行计算,需要在计算的过程中考虑符号的优先级,操作繁琐。而计算机使用后缀表达式进行计算的时候,逻辑清晰,易于实现。因此计算机在进行逻辑表达式的计算时,需要先将中缀表达式转为后缀表达式。
具体操作步骤:
- 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
- 从左至右扫描中缀表达式;
-
遇到操作数时,将其压s2;
-
遇到运算符时,比较其与s1栈顶运算符的优先级: (1)如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;(2)如果s1不为空,且当前运算符的优先级比栈顶运算符的优先级高,则将运算符压入s1;(3)如果s1不为空,且当前运算符的优先级比栈顶元素的优先级低或者相等,则将s1栈顶的运算符压入s2中,再次转到当前步骤开始,与s1中新的栈顶运算符相比较。
-
遇到括号时: (1) 如果是左括号“(”,则直接压入s1;(2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
- 重复步骤2至5,直到表达式的最右边 将s1中剩余的运算符依次弹出并压入s2
- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
举例说明:
将中缀表达 式“1+((2+3)×4)-5”转 换为后缀表达式的过程如下,因此结果为 "1 2 3 + 4 × + 5 –"
扫描到的元素 |
s2(栈底->栈顶) |
s1 (栈底->栈顶) |
说明 |
1 |
1 |
空 |
数字,直接入栈 |
+ |
1 |
+ |
s1为空,运算符直接入栈 |
( |
1 |
+ ( |
左括号,直接入栈 |
( |
1 |
+ ( ( |
同上 |
2 |
1 2 |
+ ( ( |
数字 |
+ |
1 2 |
+ ( ( + |
s1栈顶为左括号,运算符直接入栈 |
3 |
1 2 3 |
+ ( ( + |
数字 |
) |
1 2 3 + |
+ ( |
右括号,弹出运算符直至遇到左括号 |
× |
1 2 3 + |
+ ( × |
s1栈顶为左括号,运算符直接入栈 |
4 |
1 2 3 + 4 |
+ ( × |
数字 |
) |
1 2 3 + 4 × |
+ |
右括号,弹出运算符直至遇到左括号 |
- |
1 2 3 + 4 × + |
- |
-与+优先级相同,因此弹出+,再压入- |
5 |
1 2 3 + 4 × + 5 |
- |
数字 |
到达最右端 |
1 2 3 + 4 × + 5 - |
空 |
s1中剩余的运算符 |
代码实现:
package com.marden.demo4;
import java.util.ArrayList;
import java.util.Stack;
public class ToReversePoland {
public static void main(String[] args) {
String expression="1+((2+3)*4)-5";
char[] charArray=expression.toCharArray();
String [] stringArray=new String [charArray.length];
for(int i=0;i<charArray.length;i++){
stringArray[i]=""+charArray[i];
}
Stack<String> stack1=new Stack<String>();
Stack<String> stack2=new Stack<String>();
for(int i=0;i<stringArray.length;i++){
//如果当前字符是数字,则直接压入stack2
if(stringArray[i].matches("\\d")){
stack2.push(stringArray[i]);
}
//如果当前字符是操作符
else if(stringArray[i].equals("+") || stringArray[i].equals("-") || stringArray[i].equals("*") || stringArray[i].equals("/")){
while(true){
//1.如果stack1为空,则直接将当前字符压入stack1
if(stack1.isEmpty()){
stack1.push(stringArray[i]);
break;
}
//2.如果stack1不为空
else{
//判断stack1的栈顶元素是否为操作符(括号不算操作符)
//如果stack1的栈顶元素是操作符,则根据操作符的优先级,执行后续操作
if(isOper(stack1.peek())){
//若当前字符的优先级比stack1的栈顶元素优先级高,则直接将当前字符压入stack1
if(priority(stringArray[i])>priority(stack1.peek())){
stack1.push(stringArray[i]);
break;
}else{
String temp=stack1.pop();
stack2.push(temp);
//break;
continue;
}
}
//如果stack1的栈顶元素是括号,则直接入栈
else{
stack1.push(stringArray[i]);
break;
}
}
}
}
//如果当前字符为左括号,则直接压入stack1
else if(stringArray[i].equals("(")){
stack1.push(stringArray[i]);
}
//如果当前字符为右括号,则将stack1中的栈顶弹出,压入stack2中,直到stack1的栈顶元素为左括号。此时将一对括号去掉
else if(stringArray[i].equals(")")){
while(!stack1.peek().equals("(")){
String temp=stack1.pop();
stack2.push(temp);
}
stack1.pop();
continue;
}else{
throw new RuntimeException("表达式有误");
}
}
stack2.push(stack1.pop());
String last="";
//ArrayList<String> list=new ArrayList<String>();
while(!stack2.isEmpty()){
//list.add(stack2.pop());
last+=stack2.pop();
}
//System.out.println(last);
char [] tempChar=last.toCharArray();
String reservePoland="";
for(int i=tempChar.length-1;i>=0;i--){
reservePoland+=tempChar[i];
}
System.out.println(reservePoland);
}
public static boolean isOper(String str){
if(str.equals("+") || str.equals("-") || str.equals("*") || str.equals("/")){
return true;
}else{
return false;
}
}
//判断运算符的优先级,数值越大,优先级越高
public static int priority(String str){
if(str.equals("*") || str.equals("/")){
return 1;
}else if(str.equals("+") || str.equals("-")){
return 0;
}else{
return -1;
}
}
}
注意:
- break:跳出循环
- continue:跳出当前循环,执行下次循环