一、栈
1、栈的介绍
定义:栈是只允许在一端插入或删除的特定线性表。
栈的应用场景:
1、子程序的调用:在跳往子程序前,会将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
2、处理递归调用:和子程序调用类似,除了存储下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
3、 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)—使用操作数栈和运算符栈来完成
4、二叉树的遍历—行遍历
5、图形的深度优先(depth—first)搜索法。
2、使用数组来模拟栈
思路分析:
① 定义一个top来表示栈顶,初始化为-1
② 入栈的操作,当有数据加入到栈时,top++;stack[top]=data;
③ 出战的操作,int value =stack[top]; top–; return value;
定义一个ArrayStack类
代码如下:
// 定义一个ArrayStack表示栈
class ArrayStack{
private int maxSize; // 栈的大小
private int[] stack; // 数组模拟栈,数据就放在该数组
private int top =-1; //top表示栈顶,初始化为-1
// 构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize]; //初始化数组,
// 并赋值给数组类型的变量stack
}
// 栈满
public boolean isFull(){
return top==maxSize-1;
}
// 栈空
public boolean isEmpty(){
return top==-1;
}
//入栈 push
public void stackPush(int value){
if (top==maxSize-1) {
System.out.println("栈满---不能再添加");
return;
}
top++;
stack[top]=value;
}
// 出栈 pop,将栈顶的数据返回
public int stackPop(){
if (top==-1){
// 抛出异常
throw new RuntimeException("栈空,没有数据---");
}
int temp=stack[top]; // 记录下要出栈的值,
top--; // 如果直接return,就不能完成top--了
return temp;
}
// 遍历栈 从栈顶开始到栈底
public void showStack(){
if (top==-1){
System.out.println("栈空,没有数据---");
return;
}
for (int i=top;i>=0;i--){
System.out.printf("stack[%d]=%d\n",i,stack[i]);
}
}
}
测试如下:
public class ArrayStackDemo {
public static void main(String[] args) {
// 先创建一个ArrayStack对象---表示栈
ArrayStack arrayStack = new ArrayStack(3);
String key=""; // 保存后续选择
boolean loop=true; // 控制是否退出菜单
Scanner scanner = new Scanner(System.in);//获取一个扫描器
while (loop){
System.out.println("show:表示显示栈");
System.out.println("exit:退出栈");
System.out.println("push:表示添加数据到栈(入栈)");
System.out.println("pop:表示从栈取出数据(出栈)");
System.out.println("请输入你的选择");
key=scanner.next();
switch (key){
case "show":
arrayStack.showStack();
break;
case "push":
System.out.println("请输入你要添加的数:");
int value=scanner.nextInt();
arrayStack.stackPush(value);
break;
case "pop": // 在里面抛出了异常,可能有异常,try-catch
try {
int res=arrayStack.stackPop();
System.out.printf("出栈的数据为%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop=false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
3、使用单向链表来模拟栈(有头结点的-头插法实现)
思路分析:
1、先创建个结点类,然后创建单向链表类,之后利用头结点,进行头插,完成进栈操作。
2、由于栈是先进后出,因此出栈也是头结点指向的那个结点。
3、遍历也是从链表头到尾完成。
创建结点类
代码如下:
// 创建一个Person结点类
class Person{
public int num;
public Person next;
@Override
public String toString() {
return "Person{" +
"num=" + num +
'}';
}
public Person(int num) {
this.num = num;
}
}
创建栈(单向链表头插)
代码如下:
class ListStack{
// 先创建一个头结点
Person head =new Person(0);
public void StackAdd(Person person){ // 添加操作 进栈
person.next = head.next;
head.next = person;
}
public Person StackDel(){ // 删除操作 出栈
if (head.next==null){
System.out.println("栈为空");
return null;
}else if (head.next.next==null){
return head.next;
}else {
Person temp=head.next; // 变量向保存要出栈的数据,以便return
head.next=head.next.next;
return temp;
}
}
public void StackShow(){ // 显示操作 由于是头插,直接头部遍历
Person temp=head; // 遍历不能破坏链表 要移动辅助遍历temp
if (temp.next==null){
return;
}
while (true){
if (temp.next==null){
break;
}
System.out.println(temp.next);
temp=temp.next;
}
}
}
测试如下:
public class ListStackDemo {
public static void main(String[] args) {
// 创建结点
Person a = new Person(1);
Person b = new Person(2);
Person c = new Person(3);
Person d = new Person(4);
// 创建栈
ListStack listStack = new ListStack();
//添加数据 进栈
listStack.StackAdd(a);
listStack.StackAdd(b);
listStack.StackAdd(c);
listStack.StackAdd(d);
System.out.println("进栈后的栈是---");
//显示
listStack.StackShow();
// 删除数据 出栈
Person deldata1=listStack.StackDel();
System.out.println("出栈的元素是"+ deldata1);
System.out.println("现在的栈是---");
listStack.StackShow();
}
}
二、栈的应用—表达式的计算
1、中缀表达式计算计算器
1.1 思路:
1、通过一个index索引,来遍历表达式
2、如果发现是个数字,就直接入数栈
3、如果发现是个符号,就分如下情况
① 如果当前符号栈为空,就直接入栈
②遇到界限符。遇到“( ”直接入符号栈;遇到“)”则依次弹出栈内运算符,并弹出数栈两个数,进行计算,在保存在数栈中。直到弹出“(”为止。
③ 如果符号栈有操作符,就进行比较,如果当前操作符的优先级小于或等于栈中的操作符,就需要从数栈中pop处两个数,再从符号栈中pop出一个符号,进行运算,将得到的结果,入数栈,然后将当前的操作符入符号栈。
4、当表达式扫描完毕,就顺序的从数栈和符号栈中pop处相应的数和符号,并计算,
5、最后在数栈只有一个数字,就是表达式的结果
注意:
期间每弹出一个运算符,就需要在弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈。
1.2 代码实现
创建栈并增加(判定优先级、判定运算符、计算)等方法
// 先创建一个栈
// 定义一个ArrayStack2表示栈
class ArrayStack2{
private int maxSize; // 栈的大小
private int[] stack; // 数组模拟栈,数据就放在该数组
private int top =-1; //top表示栈顶,初始化为-1
// 构造器
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize]; //初始化数组,
// 并赋值给数组类型的变量stack
}
// 栈满
public boolean isFull(){
return top==maxSize-1;
}
// 栈空
public boolean isEmpty(){
return top==-1;
}
//入栈 push
public void stackPush(int value){
if (top==maxSize-1) {
System.out.println("栈满---不能再添加");
return;
}
top++;
stack[top]=value;
}
// 出栈 pop,将栈顶的数据返回
public int stackPop(){
if (top==-1){
// 抛出异常
throw new RuntimeException("栈空,没有数据---");
}
int temp=stack[top]; // 记录下要出栈的值,
top--; // 如果直接return,就不能完成top--了
return temp;
}
// 观察栈顶的方法
public int showTop(){
if (top==-1) {
System.out.println("栈空,没有数据---");
return -1;
}
return stack[top];
}
// 遍历栈 从栈顶开始到栈底
public void showStack(){
if (top==-1){
System.out.println("栈空,没有数据---");
return;
}
for (int i=top;i>=0;i--){
System.out.printf("stack[%d]=%d\n",i,stack[i]);
}
}
// 返回运算符的优先级,优先级使用数字表示,数字越大,优先级越高
public int priority(int oper){
if (oper=='*'||oper=='/'){
return 1;
}else if(oper=='+'||oper=='-'){
return 0;
}else {
return -1;// 假定目前的表达式只有+、-、*、/
}
}
// 判断是否是个运算符
public boolean isOper(char val){
return val=='+'||val=='-'||val=='*'||val=='/';
}
// 计算方法
public int cal(int num1,int num2 ,int oper){
int res=0; //res 用于存放计算结果
switch (oper) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1; // 注意顺序
break;
case '*':
res = num2 * num1;
break;
case '/':
res = num2 / num1;
break;
default:
break;
}
return res;
}
}
测试代码如下:
public class Calculator {
public static void main(String[] args) {
String expression ="300+2*6-2*2";
//创建两个栈,数栈和符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
// 定义需要的相关变量 字符串不能用for直接遍历,故采用index索引扫描
int index=0; // 用于扫描
int num1=0;
int num2=0;
int oper=0;
int res=0;
char ch=' '; //将每次扫描得到的char保存到ch
String keepnum=""; // 用于拼接多位数
// 开始while循环的扫描experssion
while (true){
// 一次得到experssion的每一个字符
// substring方法获取返回一个索引间的字符串,charAt获取第一个
ch =expression.substring(index,index+1).charAt(0);
//判断ch是什么
if (operStack.isOper(ch)){ // 如果是运算符
// 判断当前的符号栈是否为空
if (!operStack.isEmpty()){ //如果不为空
if (operStack.priority(ch)<=operStack.priority(operStack.showTop())){
num1=numStack.stackPop();
num2=numStack.stackPop();
oper=operStack.stackPop();
res=numStack.cal(num1,num2,oper);
// 把运算的结果入数栈
numStack.stackPush(res);
// 然后将当前的操作符入符号栈
operStack.stackPush(ch);
}else {
// 如果当前操作符的优先级大于栈中的操作符,直接写入
operStack.stackPush(ch);
}
}else {
// 如果符号栈为空,则直接入符号栈
operStack.stackPush(ch);
}
}else { //如果是数,直接入数栈
// numStack.stackPush(ch)是错的,因为是字符串要ascll码
// numStack.stackPush(ch-48);
// 分析思路
// 1、当处理多位数时,不能发现是一个数就立即出栈,可能是个多位数
// 2、在处理数,需要向expression表达式的index再看一位,如果是数就进行扫描,如果符号才入栈
// 3、因此需要定义一个变量字符串,用于拼接
// 处理多位数
keepnum+=ch;
// 如果ch已经是expression最后一位,就直接入栈
if (index==expression.length()-1){
numStack.stackPush(Integer.parseInt(keepnum));
}else {
// 判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
// 注意只是看后一位,不是index++
if (operStack.isOper(expression.substring(index+1,index+2).charAt(0))){
//如果后一位是运算符,则入栈keepNum=”1“或”123“
numStack.stackPush(Integer.parseInt(keepnum));
//重要的 要把keeepnum清空
keepnum="";
}
}
}
// 让index+1,并判断是否扫描到expression最后
index++;
if (index>=expression.length()){
break;
}
}
// 当表达式扫描完毕,就顺序的从数栈和符号栈中弹出相应的数和符号,并允许
while (true){
// 如果符号栈为空,最计算得到最后结果
if (operStack.isEmpty()){
break;
}
num1=numStack.stackPop();
num2=numStack.stackPop();
oper=operStack.stackPop();
res=numStack.cal(num1,num2,oper);
// 把运算的结果入数栈
numStack.stackPush(res);
}
// 将最后的数,弹出
System.out.printf("表达式%s=%d",expression,numStack.stackPop());
}
}
2、中缀转后缀计算器
① 中缀表达式字符串(String)转中缀表达式集合(List)
思路分析:
由于字符串再转为后缀表达式过程中不好遍历,故把其转为Lsit中缀表达式。
代码如下:
注意:
① String.charAt(int index)方法获取当前索引位置对应的字符
② 字符与数字进行比较是拿ascii码进行的
③字符与数字计算 例如 ch+=2;或ch++; 得到的ch还是字符,而system.out.println(ch+2) 输出的是ascii码。
// 将中缀表达式转为对应的中缀List
public static List<String> toInfixExpression(String arr) {
ArrayList<String> ls = new ArrayList<>(); // 定义一个List,存放中缀对应的内容
int index = 0;// 索引取遍历中缀表达式arr
char ch; //将每次扫描得到的char保存到ch
String str = ""; // 用于拼接多位数
do {
// 如果ch是一个非数字,需要加入到ls中
// String.charAt(int index)方法获取当前索引位置对应的字符
// 字符与数字进行比较是拿ascii码进行的
// 字符与数字计算 例如 ch+=2;或ch++; 得到的ch还是字符
// 而system.out.println(ch+2) 输出的是ascii码
if ((ch = arr.charAt(index)) < 48 || (ch = arr.charAt(index)) > 57) {
ls.add("" + ch);
index++; //需要index后移
} else { //如果是一个数,需要考虑多位数
str = ""; // 先将str置成”“
while (index < arr.length() && (ch = arr.charAt(index)) >= 48 && (ch = arr.charAt(index)) <= 57) {
str += ch; // 拼接
index++;
}
ls.add(str);
}
} while (index < arr.length());
return ls; //返回
}
② 中缀表达式转换后缀表达式 (将中缀List转后缀List)
具体步骤:
1、 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
2、 从左至右扫描中缀表达式;
3、 遇到操作数时,将其压入s2;
4、遇到运算符时,比较其于s1栈顶运算符的优先级:
① 如果s1为空,或栈顶运算符为左括号”(“,则直接将此运算符入栈;
② 否则,若优先级比栈顶运算符的高,也将运算符压入s
1;
③ 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(①)与此时的新栈顶运算符比较 (实际就是—弹出栈中优先级高于或等于当前运算符的所有运算符)
5、遇到界限符,遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并压入s2,直到弹出“(”为止,此时将着一对括号丢弃。
6、重复步骤2-5,直到表达式的最右边
7、将s1中剩余的运算符依次弹出并压入s2
8、依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。
代码如下:
说明:因为中间结果栈s2,在整个过程中,没有pop过程,而且后面还需要逆序输出,比较麻烦。因此采用List s2。
// 方法:将得到的中缀表达式List转为后缀表达式List
public static List<String> parseSuffixExpressionList(List<String> ls){
// 定义两者栈
Stack<String> s1 = new Stack<>(); // 符号栈
// Stack<String> s2 = new Stack<>(); // 中间值栈
// 说明 因为s2这个栈,在整个过程中,没有pop过程,而且后面还需要逆序输出,比较麻烦。
// 这里就不用Stack<String> s2 直接用List<String> s2
ArrayList<String> s2 = new ArrayList<>();
// 遍历ls
for(String item:ls){
//如多是一个数 加入s2
if (item.matches("\\d+")){
s2.add(item);
}else if (item.equals("(")){
s1.push(item);
}else if(item.equals(")")){
// 如果是右括号”)“,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时一对括号被丢弃
while (!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop(); // 将”(“ 弹出s1栈,消除小括号
}else {
// 当item优先级小于等于s1栈顶运算符优先级,将s1栈顶运算符弹出并压入s2,
// 再次转(4.1)与s1新栈顶比较 (即 弹出所有大于等于的)
// 缺少个比较优先级的类
while (s1.size()!=0 && Operation.getValue(s1.peek())>=Operation.getValue(item)){
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
// 将s1剩余的运算符弹出压入s2
while (s1.size()!=0){
s2.add(s1.pop());
}
return s2; // 注意因为村的List,因此按顺序输出就是对应的后缀表达式List
}
优先级的比较方法如下:
// 编写一个类Operation可以返回一个运算符,对应的优先级
class Operation{
private static int ADD=1;
private static int SUB=1;
private static int MUL=1;
private static int DIV=1;
// 写一个方法,返回对应的优先级数字
public static int getValue(String operation){
int result=0;
switch (operation){
case "+":
result=ADD;
break;
case "-":
result=SUB;
break;
case "*":
result=MUL;
break;
case "/":
result=DIV;
break;
default:
break;
}
return result;
}
}
测试如下:
String expression = "1+((2+3)*4)-5";
List<String> infixExpressionList = toInfixExpression(expression);
System.out.println("中缀表达式对应的List"+infixExpressionList); // [1,+,(,(,2,+,3,),*,4,),-,5]
List<String> parseSuffixExpressionList = parseSuffixExpressionList(infixExpressionList);
System.out.println("后缀表达式对应的List"+parseSuffixExpressionList); // [1,2,3,+,4,*,+,5,-]
③ 后缀表达式的计算
代码如下:
// 完成逆波兰的计算
public static int calculate(List<String> ls){ //ls传入的集合
// 创建一个栈
Stack<String> stack = new Stack<String>();
//遍历ls
for(String item:ls){
//使用正则表达式来取数
if (item.matches("\\d+")){ // 匹配多位数
stack.push(item);
}else {
//pop两个数计算
int num2=Integer.parseInt(stack.pop());
int num1=Integer.parseInt(stack.pop());
int res=0;
if (item.equals("+")){
res=num1+num2;
}else if (item.equals("-")){
res=num1-num2;
}else if (item.equals("*")){
res=num1*num2;
}else if (item.equals("/")){
res=num1/num2;
}else {
throw new RuntimeException("运算符有误");
}
//把res入栈
stack.push(""+res);
}
}
//最后留在stack中的数据是运算结果
return Integer.parseInt(stack.pop());
}