文章目录
1、栈
- 栈的英文名为stack
- 栈是一个先入后出的有序列表
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为-栈顶,另外一端称为栈底
- 根据栈的定义可知,最先放入栈中的元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
先进后出,后进先出
栈的应用场景
- 子程序的调用:在跳往子程序之前,会先将下一个指令的地址存在堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中
- 处理递归调用:和子程序的调用类似,只是除了存储下一跳指令的地址外,也将参数、区域变量等数据存入堆栈中
- 表达式的转换([中缀表达式转后缀表达式])与求值(实际解决)
- 二叉树的遍历
- 图形的深度优先(depth-first)搜索法
2、栈的实现
2.1、数组实现栈
实现栈的思路分析
- 使用数组来模拟栈
- 定义一个top来表示栈顶,初始化为-1(top包含栈顶元素)
- 入栈的操作,当有数据加入到帐时,top++。stack[top]=data
- 出栈的操作,int value = stack[top];top–;return value;
package D栈;
import com.sun.jmx.remote.internal.ArrayQueue;
import java.util.Scanner;
/**
* @Author Zhou jian
* @Date 2019 ${month} 2019/12/30 0030 18:09
*
*/
public class ArrayStackDemo {
public static void main(String[] args) {
//创建一个队列
ArrayStack stack = new ArrayStack(3);
char key = ' ';//接受用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
//输出一个菜单
while(loop){
System.out.println("s(show):显示栈");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到栈");
System.out.println("g(get):从栈取出数据");
key = scanner.next().charAt(0);
switch (key){
case 's':
stack.list();
break;
case 'a':
System.out.println("输入一个数");
int value = scanner.nextInt();
stack.push(value);
break;
case 'g':
try{
System.out.println("取出的数据是"+stack.pop());
}catch (Exception e){
System.out.println(e);
}
break;
case 'e':
scanner.close();
loop = false;
}
}
}
}
//定义一个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[maxSize];
}
//栈满
public boolean isFull(){
return top == maxSize-1;
}
//栈空
public boolean isEmpty(){
return top == -1;
}
//入栈=push
public void push(int value){
if(isFull()){
System.out.println("栈满");
return;
}else{
top++;
stack[top]=value;
}
}
//出栈,pop
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空");
}else{
int value = stack[top];
stack[top]=0;//便于显示栈,没有存入的数据为0
top--;
return value;
}
}
//遍历栈.便利时,需要从栈顶开始显示
public void list(){
for(int i=top;i>=0;i--){
System.out.printf("stack[%d]=%d\n",i,stack[i]);
}
}
}
2.2、链表实现栈
package D栈;
import java.util.Scanner;
/**
* @Author Zhou jian
* @Date 2019 ${month} 2019/12/30 0030 18:34
* 链表实现栈
*/
public class LinkedStackDemo {
public static void main(String[] args) {
//创建一个队列
LinkedStack stack = new LinkedStack();
char key = ' ';//接受用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show):显示栈");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到栈");
System.out.println("g(get):从栈取出数据");
key = scanner.next().charAt(0);
switch (key) {
case 's':
stack.list();
break;
case 'a':
System.out.println("输入一个数");
int value = scanner.nextInt();
Node newNode = new Node(value);
stack.push(newNode);
break;
case 'g':
try {
System.out.println("取出的数据是" + stack.pop());
} catch (Exception e) {
System.out.println(e);
}
break;
case 'e':
scanner.close();
loop = false;
}
}
}
}
class LinkedStack{
private Node head;//头节点
public LinkedStack() {
this.head = new Node(-1);
}
//判断栈是否为空
public boolean isEmpty(){
return head.next==null;
}
//向栈中加入数据
public void push(Node newNode){
Node temp = head; //辅助指针用来遍历栈
while(true){
//取出栈的末尾元素
if(temp.next==null){
break;
}else{
temp=temp.next;
}
}
//将数据添加到栈的尾部
temp.next=newNode;
}
//取出栈的元素
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈为空");
}else{
Node temp = head; //辅助指针用来遍历栈
Node prev = temp;//保存当前节点的前一个节点用来删除
while(true){
//取出栈的末尾元素
if(temp.next==null){
break;
}else{
prev = temp;
temp=temp.next;
}
}
//取出末尾元素
int value = temp.data;
prev.next=null;//将末尾元素删除
return value;
}
}
//遍历栈:就是反向打印链表中的所有节点
public void list(){
printReverse1();
}
/**
* 反向打印链表节点
*/
public void printReverse1(){
int sum = size();
for(int i=1;i<=sum;i++){
System.out.println(findByReverse(i));
}
}
/**
* 反向获取节点
* @param no
* @return
*/
public Node findByReverse(int no){
//1、求链表中的节点的个数
int length = size();
//2、反向数的第no个节点也就是正向数的第 length-no节点
int place = length-no+1; //思考为什么要加1,可以考虑倒数第一个节点的情况
//3、通过遍历的方式获取节点
Node temp = head;
for(int i=0;i<place;i++){
temp=temp.next;
}
return temp;
}
/**
* 求链表中节点个数问题:遍历链表即可
*/
public int size() {
Node temp = head.next;
int length = 0;
while (true) {
if (temp != null) {
length++;
temp = temp.next;
} else {
break;
}
}
return length;
}
}
class Node{
int data;
Node next;
public Node(int data) {
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
'}';
}
}
关键难点:
-
删除链表中的节点,获取待删除节点前一个节点
-
如何反向打印链表
3、表达式(栈实现计算功能应用)
3.1、中缀表达式实现计算器
使用中缀表达式完成计算的思路:
-
通过一个index值(索引),来遍历我们的表达式
-
如果我们发现是一个数字,就直接进入数栈
-
如果我们扫描到的是一个符号,就分如下情况
3.1、如果发现当前的符号栈为空,就直接入栈
3.2、如果发现符号栈有操作符,就进行比较,如果当前的操作符的优先小于或者等于栈中的操作符,就需要从栈中pop出两个数,在从符号栈中pop出一个符号进行运算,将得到的结果,入数栈,然后将当前的操作符入符号栈;如果当前的操作符的优先级大于与栈中的操作符,就直接入符号栈 思考为什么这样???(这样会确保高优先级的先运行;低优先级别的后运行)
-
当表达式扫描完毕,就顺序的从数栈中pop出相应的数和符号,并运行
-
最后在数栈只有一个数字,就是表达式的结果
代码实现(1、先实现一位数;2、扩展到多位数)
package D栈;
/**
* @Author Zhou jian
* @Date 2019 2019/12/30 0030 21:26
*/
public class Calcuator {
public static void main(String[] args) {
String expression = "30+2*6-2";//中缀表达式:正常的计算思路
//创建两个栈,数栈和符号栈
ArrayStack numStack = new ArrayStack(10);//数栈
ArrayStack operStack = new ArrayStack(10);//符号栈
//定义需要的相关变量
int index = 0; //用于扫描表达式
int num1 = 0; //计算时从栈内弹出的值
int num2 = 0;
int oper = 0; //操作符
int res = 0; //计算的结果
char ch = ' ';//将每次扫描得到的char保存到ch
String keepNum ="";//用于拼接多位数
//开始循环扫描expression
while(true){
//依次得到expression的每一个字符
ch = expression.subSequence(index,index+1).charAt(0);
//判断ch是什么,然后做相同应的处理
if(operStack.isOpera(ch)){//如果是运算符
//判断当前的符号栈是否为空
if(!operStack.isEmpty()){//不为空
//如果发现符号栈有操作符,就进行比较,
// 如果当前的操作符的优先小于或者等于栈中的操作符,就需要从栈中pop出两个数,
// 在从符号栈中pop出一个符号进行运算,将得到的结果,入数栈,然后将当前的操作符入符号栈
if(operStack.priority(ch)<=operStack.priority(operStack.peek())){
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1,num2,oper);
numStack.push(res);//将得到的结果入数栈
operStack.push(ch);//将当前操作符入操作符展
}else{
//==如果当前的操作符的优先级大于与栈中的操作符,就直接入符号栈==
operStack.push(ch);
}
}else{//为空直接进站
operStack.push(ch);
}
}else {//如果为数字符
//分析思路
//1、当处理多位数时,不能发现一个数就立即入栈,因为它可能是多位数
//2、在处理数,需要向expression的表达式的index后再看一位,如果是数就进行扫描,如果是符号才入栈
//3、因此我们需要定义一个变量字符串,用于拼接
//
//处理多为数
keepNum += ch;
//如果ch已经是expression的最后一位,就直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
} else {
//判断下一位字符是不是数字,如果是数字就继续扫描,如果是运算符则入栈
if (operStack.isOpera(expression.substring(index + 1, index + 2).charAt(0))) {
//如果后一位是操作符则入栈
numStack.push(Integer.parseInt(keepNum));
//重要的!!!
keepNum = "";
} else {
}
}
}
//让index+1并判断是否扫描到expression的最后
index++;
if(index>=expression.length()){
break;
}
}
//当表达式扫描完毕,就顺序的从数栈中pop出相应的数和符号,并运行
while(true){
//如果为符号栈为空,则计算到最后
if(operStack.isEmpty()){
break;
}else{
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1,num2,oper);
numStack.push(res);//将得到的结果入栈
}
}
//1. 最后在数栈只有一个数字,就是表达式的结果
System.out.printf("表达式%s=%d",expression,numStack.pop());
}
}
//定义一个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[maxSize];
}
//栈满
public boolean isFull(){
return top == maxSize-1;
}
//栈空
public boolean isEmpty(){
return top == -1;
}
//入栈=push
public void push(int value){
if(isFull()){
System.out.println("栈满");
return;
}else{
top++;
stack[top]=value;
}
}
//出栈,pop
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空");
}else{
int value = stack[top];
stack[top]=0;//便于显示栈,没有存入的数据为0
top--;
return value;
}
}
//增加一个方法可以返回当前栈顶的元素
public int peek(){
if(isEmpty()){
throw new RuntimeException("栈为空");
}else{
return stack[top];
}
}
//遍历栈.便利时,需要从栈顶开始显示
public void list(){
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 isOpera(char val){
return val=='+'||val=='-'||val=='*'||val=='/';
}
//计算方法
public int cal(int num1,int num2,int oper){
int res = 0;//用于存放计算的结果
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;
}
}
3.2、前缀表达式(波兰表达式)
-
前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
-
举例说明:(3+4)x5-6对应的前缀表达式是 - x + 3 4 5 6
算法分析:
从右向左扫描表达式,
遇到数字时;将数字压入堆栈,
遇到运算符时,弹出栈顶的两个数,用运算符对他们作相应的计算(栈顶元素和次顶元素),并将结果入栈;
重复上述过程直到表达式最左端,最后运算得到的值即使表达式的结果。
例如:(3+4)x5-6对应的前缀表达狮子就是==- x + 3 4 5 6==,针对前缀表达式求值步骤如下:
- 从右到左扫描,,将6、5、4、3压入堆栈
- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4值为7,再将7入栈
- 接下来是x运算符,因此弹出7和5,计算处于7x5=35,将35入栈
- 最后是-运算符,计算出35-6的值,即29,由此得到最终的结果
3.3、中缀表达式
- 中缀表达式就是常见的运算表达式,如(3+4)x5-6
- 中缀表达式的求值使我们人最熟悉的,但是对计算机来说确不好操作,因此,在计算结果时候,。往往会将中缀表达式转换成其他表达式来操作(一般转成后缀表达式)
3.4、后缀表达式(逆波兰表达式)
- 后缀表达式又称逆波兰表达式,与前缀表达式相似。只是运算符位于操作数之后
- 中举例说明:(3+4)x 5-6对应的后缀表达式就是 3 4 + 5 x 6 -
算法分析:
从左到右扫描表达式,遇到数字时,将数字压入堆栈
遇到运算符时,弹出栈顶的两个数,用运算符对他们做相应的计算(次顶元素和栈顶元素),并将结果入栈
重复上述过程直到表达式的最右端,最后运算得到的值即为表达式的结果
例如:(3+4)x5-6对应的后缀表达式就是3 4 + 5 x 6 -,针对后缀表达式求值步骤如下:
- 从左☞右扫描,将3和4压入堆栈
- 遇到+运算符,因此弹出4和3(4位栈顶元素,3位次顶元素),计算吗3+4的值,得7.再将7入栈
- 将5入栈
- 接下来是x运算符,因此弹出5和7,计算出7x5-35,将35入栈
- 将6入栈
- 最后是-运算符,计算出35-6的值,即29
note:减法与除法都是次顶元素对顶部元素进行操作
逆波兰计算器代码实现
/**
1)支持一个逆波兰表达式(后缀表达式),使用栈(Stack):用JDK的栈,计算其结果
2)支持小括号和多为整数,因这里我们会主要
**/
package D栈;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @Author Zhou jian
* @Date 2019 ${month} 2019/12/30 0030 23:14
* 逆波兰表达式求值过程
*/
public class PolandNotation {
public static void main(String[] args) {
//定义一个逆波兰表达式
// (3+4)x5-6=>3 4 + 5 x 6 -
//为了方便,逆波兰表达式的数字和符号使用空格隔开
String suffixExpression = "30 4 + 5 x 6 -";
//思路
//1、现将“3 4 + 5 x 6 - ”=》放到ArrayList中
//2、将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
List<String> rpnList = getListString(suffixExpression);//[3, 4, +, 5, x, 6, -]
System.out.println("计算的结果是"+calculate(rpnList));
}
//将一个你博看表达式,依此将数据和运算符放入到ArrayList中
public static List<String> getListString(String suffieExpression){
//
String[] split = suffieExpression.split(" ");
List<String> list = new ArrayList<>();
for(String ele:split){
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的运算
public static int calculate(List<String> ls){
//创建栈,只需要一个栈即可
Stack<String> stack = new Stack<>();
//遍历ls
for(String str:ls){
//这里使用正则表达式来取出术
if(str.matches("\\d+")){//匹配多为数
//入栈
stack.push(str);
}else{
//弹出两个数,并运算,将运算的结果入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if(str.equals("+")){
res = num1+num2;
}else if(str.equals("-")){
res = num1-num2;
}else if(str.equals("x")){
res = num1*num2;
}else if(str.equals("/")){
res = num1/num2;
}
//把res入ZHAN
stack.push(res+"");
}
}
//最后留在栈中的数据就是哦运算结果
int returnValue =Integer.parseInt(stack.pop());
return returnValue;
}
}
3.5、中缀表达式转后缀表达式
后缀表达式适合计算机进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式
具体步骤如下:
-
初始化两个栈:运算符栈s1和存储中间结果的s2栈
-
从左到右扫描中缀表达式
-
遇到操作数时,将其压s2
-
遇到操作符时,比较其与s1栈顶运算符的优先级;
4.1、如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈
4.2、否则,若优先级比栈顶运算符的高,也将运算符压入s1
4.3、否则将s1栈顶的运算符弹出并压入到s2中,再次转到4.1与s1中新的栈顶运算符相比较
-
遇到括号时:
- 如果是左括号,直接压入s1
- 如果是右括号。则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
-
重复步骤2-5,直到表达式的最右边
-
将s1中剩余运算符依次弹出并压入s2
-
依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
package D栈;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @Author Zhou jian
* @Date 2019 ${month} 2019/12/30 0030 23:14
* 逆波兰表达式求值过程
*/
public class PolandNotation {
public static void main(String[] args) {
//方法将中缀表达式转成对应的list
String expression ="1+((20+3)x4)-5";
//1+((2+3)x4)-5=>[1, +, (, (, 20, +, 3, ), x, 4, ), -, 5]
List<String> infixExpression= toInfixExpression(expression);
//将得到的中缀表达式对应的List=>后缀表达式对应的List
//[1, +, (, (, 20, +, 3, ), x, 4, ), -, 5]=> [1,2,3,+,4,*,+,5,-]
List<String> suffixExpressionList = parseSuffixExpressionList(infixExpression);
System.out.println(suffixExpressionList);
System.out.println("计算的结果是"+calculate(suffixExpressionList));
}
//将一个你博看表达式,依此将数据和运算符放入到ArrayList中
public static List<String> getListString(String suffieExpression){
//
String[] split = suffieExpression.split(" ");
List<String> list = new ArrayList<>();
for(String ele:split){
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的运算
public static int calculate(List<String> ls){
//创建栈,只需要一个栈即可
Stack<String> stack = new Stack<>();
//遍历ls
for(String str:ls){
//这里使用正则表达式来取出术
if(str.matches("\\d+")){//匹配多为数
//入栈
stack.push(str);
}else{
//弹出两个数,并运算,将运算的结果入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if(str.equals("+")){
res = num1+num2;
}else if(str.equals("-")){
res = num1-num2;
}else if(str.equals("x")){
res = num1*num2;
}else if(str.equals("/")){
res = num1/num2;
}
//把res入ZHAN
stack.push(res+"");
}
}
//最后留在栈中的数据就是哦运算结果
int returnValue =Integer.parseInt(stack.pop());
return returnValue;
}
//完成将中缀表达式转后缀表达式
/**
* 1、1+((2+3)x4)-5=>转成 1 2 3 + 4 x + 5 -
* 2、因为直接对字符串操作不方便,因此先将 “1+((2+3)x4)-5=”转成中缀list
* 即把
*
*/
//方法将中缀表达式转成对应的list
public static List<String> toInfixExpression(String s ){
//先定义一个List,存放中缀表达式对应的内容
List<String> ls = new ArrayList<>();
int i = 0; //指针,用于遍历中缀表达式字符串
String str;//对多位数拼接
char c;//每遍历到一个字符就放入x中
do{
//如果c是一个非数字,我们就需要加入到ls中
if((c=s.charAt(i))<48||(c=s.charAt(i))>57) {
ls.add(c + "");
i++; //i需要后移
}else{//如果是一个数需要考虑多为数的问题
str = ""; //先将str支撑空串
while(i<s.length()&&(c=s.charAt(i))>=48&&(c=s.charAt(i))<=57){
str+=c;//拼接
i++;
}
ls.add(str);
}
}while(i<s.length());
return ls;
}
//将得到的中缀表达式对应的List=>后缀表达式对应的List
//[1, +, (, (, 20, +, 3, ), x, 4, ), -, 5]=> [1,2,3,+,4,*,+,5,-]
public static List<String> parseSuffixExpressionList(List<String> ls){
//定义两个栈
Stack<String> s1 = new Stack<>();//符号栈
//因为s2在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//因此比较麻烦,这里我们就不用栈,直接使用List<String>,按顺序输出即为逆波兰表达式
// Stack<String> s2 = new Stack<>();//中间结果栈
List<String> s2 = new ArrayList<>();
//遍历ls
for(String item:ls){
System.out.println(item);
//如果是一个数就加入到s2
if(item.matches("\\d+")){
s2.add(item);
}else if(item.equals("(")){//左括号加入到s1
s1.push(item);
}else if(item.equals(")")){
//右括号,。则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while(!(s1.peek().equals("("))){
s2.add(s1.pop());
}
//将s1中左括号弹出,此时将这一对括号丢弃
s1.pop();
}else{
//当item优先级小于或等于栈顶运算符优先级
//s1栈顶的运算符弹出并压入到s2中,再次转到4.1与s1中新的栈顶运算符相比较
//问题:缺少比较优先级高低的方法
while(s1.size()!=0&&Operation.getValue(item)<=Operation.getValue(s1.peek())){
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
//把s1中剩余的运算符加入到s2中
while(s1.size()!=0){
s2.add(s1.pop());
}
//因为是存放到一个List中,因此俺顺序输出
return s2;
}
}
//编写一个类 Operation 可以返回一个运算符对饮个的有限级
class Operation{
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
//写一个方法,返回对应的优先级数字
public static int getValue(String operation){
int result = 0;
switch (operation){
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "x":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
break;
}
return result;
}
}