文章目录
0.栈的一个实际需求
思考一个问题:计算机底层是如何运算得到结果的?注意这不是简单的把算式列出运算,因为我们看这个算式就是一个表达式,但是对于计算机而言,他接受到的就是一个字符串,我们讨论的是这个问题- - - >栈。
1.栈的介绍
[1].栈的英文为stack。
[2].栈是一个先入后出的有序列表。
[3].栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底。
[4].根据栈的定义可知,最先放入栈中的元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
[5].出栈(pop)和入栈(push)的概念如图所示:
2.栈的应用场景
[1].子程序的调用:在眺往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
[2].处理递归调用:和子程序的调用类似,只是出了存储下一个指令的地址外,也将参数,区域变量等数据存入堆栈中。
[3].表达式的转换(中缀转后缀)与求值(表达式求值)。
[4].二叉树的遍历。
[5].图的深度优先搜索法。
3.栈的快速入门
3.1数组模拟栈
由于栈是一种有序列表,可以使用数组的结构来存储栈的数据内容,下面演示
实现思路:
[1].定义一个数组模拟栈。
[2].定义一个top表示栈顶,初始化为-1。
[3].定义一个maxSize,表示栈的最大长度。
[4].构造函数,传入maxSize,用于初始化数组和栈的最大长度。
[5].判断栈是否为空: return top == -1。
[6].判断栈是否已满:return top == maxSize - 1;
[7].入栈的操作,当栈未满时,将数据添加到栈顶,top++; stack[top] = data;
[8].出栈的操作,当栈非空时,数据出栈,return stack[top–];
实现代码如下:
class ArrayStack{
private int maxSize; //栈的大小
private int [] stack; //数组,数组模拟栈,数据就放在该数组
private int top = -1; //栈顶,初始化为-1
/**
* 构造器
* @param maxSize
*/
public ArrayStack(int maxSize){
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
/**
* 栈满
* @return
*/
public boolean isFull(){
return top == maxSize - 1;
}
/**
* 栈空
* @return
*/
public boolean isEmpty(){
return top == -1;
}
public void push(int value){
//先判断栈是否已满
if(isFull()){
System.out.println("栈满");
return;
}
top++;
stack[top] = value;
}
/**
* 删除并返回栈顶数据
* @return
*/
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空");
}
return stack[top--];
}
/**
* 遍历
* 需要从栈顶显示数据
*/
public void show(){
if(isEmpty()){
System.out.println("栈空!");
return;
}
for(int i = top; i >= 0; i--){
System.out.printf("stack[%d] = %d\t",i,stack[i]);
}
System.out.println();
}
}
3.2单链表模拟栈
//节点类
class ListNode{
int val;
ListNode next;
public ListNode(int val){
this.val = val;
}
}
//单链表栈
class SingleLinkedListStack{
/**
* 头结点
*/
private ListNode head = new ListNode(0);
/**
* 栈空
*/
public boolean isEmpty(){
return head.next == null;
}
/**
* 入栈
*/
public void push(int val){
//添加入栈,插入到头结点之后
ListNode listNode = new ListNode(val);
listNode.next = head.next;
head.next = listNode;
nums++; //有效节点数+1
}
/**
* 出栈
* @return
*/
public int pop(){
//栈为空
if(isEmpty()){
throw new RuntimeException("栈空");
}
//返回链表的头一个有效结点,并从链表中删除
int value = head.next.val;
head.next = head.next.next;
nums--;//节点数减1
return value;
}
/**
* 展示所有数据
*/
public void show(){
if (isEmpty()){
System.out.println("栈空,无法显示数据!");
}
//定义辅助指针打印数据
ListNode temp = head.next;
while(temp != null){
System.out.print(temp.val+"\t");
temp = temp.next;
}
System.out.println();
}
}
4.前缀,中缀,后缀表达式
4.1前缀表达式(波兰表达式)
[1].前缀表达式又称波兰表达式,前缀表达式的运算符位于操作数之前。
[2].举例说明:(3+4)x 5-6 对应的前缀表达式就是 - x +3 4 5 6
[3]前缀表达式的计算机求值:
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对他们做相应的计算(栈顶元素和次顶元素),并将结果压入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。
例如:(3+4)x 5 - 6对应的前缀表达式就是**- x + 3 4 5 6**
针对前缀表达式求值步骤如下:
(1)从右至左扫描,将数6 5 4 3依次压入栈
(2)遇到+运算符,因此弹出3和4,计算3+4的值,得到的结果7压入栈
(3)接下来就是x运算符,因此弹出7和5,计算结果35压入栈
(4)最后是 - 运算符,弹出35 和 6,计算结果29压入栈
(5)最后栈中唯一元素29就是结算结果
4.2中缀表达式
[1].中缀表达式就是常见的运算表达式,如**(3+4)x 5 -6**
[2].中缀表达式的求值是我们人最熟悉的,但是对于计算机来说却不好操作,因此,在计算结果时,往往会把中缀表达式转换成其他表达式来操作(一般转换成后缀表达式)。
4.3后缀表达式
[1].后缀表达式又称为逆波兰表达式与前缀表达式相似,只是运算符位于操作数之后。
[2].举例说明:(3+4)x5-6对应的后缀表达式就是3 4 + 5 x 6 -
[3].再比如:
[4].逆波兰表达式的计算机求值
从左至右扫描表达式,遇到数字时,直接将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对他们做相应的运算(次顶元素和栈顶元素运算),并将计算结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。
例如:(3+4)x 5 - 6对应的后缀表达式就是:3 4 + 5 x 6 -
表达式求值步骤如下:
[1].从左至右扫描,将3和4压入栈;
[2].遇到+运算符,因此弹出4和3,计算3+4的值,得7,再将7鸭绒衣栈。
[3].将5压入栈。
[4].接下来是x运算符,因此弹出5和7,计算出7x5=35,将35入栈。
[5].将6压入栈。
[6].最后是-运算符,计算出35-6的值,即29,因此得出最终结果。
实现代码如下:
/**
* 根据后缀表达式list获得计算结果
* @return
*/
public double getResult(){
//1.如果后缀表达式结合为空,抛出异常
if(suffixExpressionList.isEmpty()){
throw new RuntimeException("请输入正确的表达式!");
}
//2.遍历后缀表达式list
double res = 0;
double num2 = 0;
double num1 = 0;
for(String item : suffixExpressionList){
//2.1如果是数直接加入到数栈
if(item.matches("-?[0-9]+.?[0-9]*")){
numStack.push(Double.parseDouble(item));
}
//2.2如果是运算符,pop出两个数,
// 使用该运算符计算,并将计算结果push到值栈中
else{
num2 = numStack.pop();//第二个参与运算的数
num1 = numStack.pop();//第一个参与运算的数
res = cal(num1,num2,item);//获得计算结果
numStack.push(res);//计算结果入栈
}
}
//3.运算结束后,栈顶元素就是表达式的结果
//返回即可
return numStack.peek();
}
/**
* 计算 num2 oper num1
* @param num1 第一个出栈数
* @param num2 第二个出栈数
* @param oper 运算符
* @return
*/
public double cal(double num1,double num2,String oper){
/**
* 用于返回的结算结果
*/
double res = 0;
switch (oper){
case "+":
res = num1 + num2;
break;
case "-":
res = num1 - num2;
break;
case "*":
res = num1 * num2;
break;
case "/":
if(num2 == 0){
throw new RuntimeException("被除数不能为0");
}
res = num1 / num2;
break;
default:
break;
}
return res;
}
4.4中缀转后缀
后缀表达式适合计算式的运算,但是人却不容易写出来,尤其是表达式很长的情况下,因此在开发中我们需要将中缀表达式转成后缀表达式。
具体步骤如下:
[1].初始化两个栈:运算符栈s1和存储中间结果的栈s2;
[2].从左至右扫描中缀表达式;
[3].遇到数时,将其压入s2;
[4].遇到运算符时,比较其与s1栈顶运算符的优先级:
[4.1].如果此运算符是 “(”,直接压入栈s1;
[4.2].否则如果此运算符是 “)”,栈s1运算符出栈并压入到s2,直到遇到 “(”,并且作废这一对括号;
[4.3].否则如果s1为空,或者栈顶运算符为“(”,则直接将此运算符入栈;
[4.4].否则如果此运算符的优先级小于等于s1栈顶运算符优先级,栈s1运算符出栈并压入到s2,直到此运算符优先级大于s1栈顶运算符优先级,将此运算符入栈s1;
[4.5]否则即表示此运算符大于栈顶运算符优先级,此运算符入栈即可;
[5].扫描结束后,将栈s1的运算符逐个弹出并加入到s2;
[6].依次弹出s2中的元素并输出,结果的逆序即为后缀表达式;
注:从执行过程可以 看出,栈s2不需要弹出元素,所以可以使用lArrayList集合代替栈s2,正向遍历集合中的元素就是正向的后缀表达式。
代码如下:
private void getSuffixList(){
//1.初始化两个栈已完成,存放数字的栈使用suffixExpressionList,
// 存放运算符的栈使用numStack
//2.从左到右遍历中缀list
//定义辅助字符串
String item = "";
for(int i = 0; i < infixExpressionList.size(); i++){
item = infixExpressionList.get(i);
//3.如果是数字,直接加入到后缀表达式list
if(item.matches("-?[0-9]+.?[0-9]*")){
suffixExpressionList.add(item);
}
//4.如果不是字符串,就判断是何种运算符
//4.1如果运算符栈为空,或者栈顶元素为左括号,直接入栈
else if(operStack.isEmpty() || operStack.peek().equals("(")){
operStack.push(item);
}
//4.2如果是左括号,直接入栈
else if(item.equals("(")){
operStack.push(item);
}
//4.3如果是右括号,运算符栈一直pop添加到suffixExpressionList
//直到遇到左括号,并且作废该对括号
else if(item.equals(")")){
while(!operStack.isEmpty() && !operStack.peek().equals("(")){
suffixExpressionList.add(operStack.pop());
}
if(operStack.isEmpty()){
throw new RuntimeException("表达式中的括号非法!!");
}
operStack.pop();
}
//4.3如果当前运算符的优先级小于等于栈顶运算符优先级
//栈一直pop,直到栈顶优先级小于item,最后当前运算符入栈
else if(priority(operStack.peek()) >= priority(item)){
while(!operStack.isEmpty() && priority(operStack.peek()) >= priority(item)){
suffixExpressionList.add(operStack.pop());
}
operStack.push(item);
}
//4.4当前运算符的优先级大于栈顶运算符优先级
else{
operStack.push(item);
}
}
//5.将栈中运算符全部添加到后缀list
while(!operStack.isEmpty()){
suffixExpressionList.add(operStack.pop());
}
/**
* 判断运算符的优先级
* 用数字大小表示优先级,数字越大优先级越高
*/
public int priority(String oper){
switch (oper){
case "+" :
return 1;
case "-" :
return 1;
case "*" :
return 2;
case "/" :
return 2;
default:
return 0;
}
}