一、用栈实现四则运算
核心思路:双栈法
- 操作数栈:存放数字
- 运算符栈:存放
+ - * / ( )规则:先把中缀表达式,按照运算符优先级,决定是压栈还是弹出计算;括号改变优先级。 运算符优先级:(<+、-<*、/<)
1. 优先级判断函数
// 获取运算符优先级
int getPriority(char op)
{
if(op == '(') return 1;
else if(op == '+' || op == '-') return 2;
else if(op == '*' || op == '/') return 3;
else return 0; // ')'或者非法字符
}
规则解读:
- 遇到数字:直接压入操作数栈
- 遇到
(左括号:直接压运算符栈 - 遇到
)右括号:不断弹出运算符计算,直到碰到(,弹出(丢弃 - 遇到运算符:只要栈顶运算符优先级 ≥ 当前运算符,就弹出计算;直到栈顶优先级更小或者栈空,再压入当前运算符
- 表达式扫描结束后,运算符栈里剩下的依次弹出计算,最终操作数栈剩下唯一结果
2. 计算函数(弹出两个数,注意顺序!)
⚠️ 重点易错:先弹出的是右操作数 b ,后弹出的是左操作数 a ,
a op b例:6-2,先 pop 得到 2,再 pop 得到 6 →6 - 2,不能写反!
double calc(double a, double b, char op)
{
switch(op)
{
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if(b == 0)
{
printf("除零错误!\n");
exit(0);
}
return a / b;
default: return 0;
}
}
3. 完整可运行代码(顺序栈实现,支持多位数、括号)
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#define MaxSize 50
// 操作数栈(存小数,支持除法)
typedef struct{
double data[MaxSize];
int top;
}NumStack;
// 运算符栈(存字符)
typedef struct{
char data[MaxSize];
int top;
}OpStack;
// 栈基础操作
void InitNumStack(NumStack *s){s->top=0;}
int NumEmpty(NumStack *s){return s->top==0;}
int NumPush(NumStack *s, double x){
if(s->top>=MaxSize) return 0;
s->data[s->top++] = x;
return 1;
}
int NumPop(NumStack *s, double *x){
if(NumEmpty(s)) return 0;
*x = s->data[--s->top];
return 1;
}
void InitOpStack(OpStack *s){s->top=0;}
int OpEmpty(OpStack *s){return s->top==0;}
int OpPush(OpStack *s, char ch){
if(s->top>=MaxSize) return 0;
s->data[s->top++] = ch;
return 1;
}
int OpPop(OpStack *s, char *ch){
if(OpEmpty(s)) return 0;
*ch = s->data[--s->top];
return 1;
}
// 读取栈顶运算符(不弹出)
char GetOpTop(OpStack *s){
return s->data[s->top-1];
}
int getPriority(char op)
{
if(op == '(') return 1;
else if(op == '+' || op == '-') return 2;
else if(op == '*' || op == '/') return 3;
else return 0;
}
double calc(double a, double b, char op)
{
switch(op)
{
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if(b == 0){printf("除零错误\n");exit(0);}
return a / b;
default: return 0;
}
}
// 中缀表达式求值主函数
double Evaluate(char *expr)
{
NumStack numSt;
OpStack opSt;
InitNumStack(&numSt);
InitOpStack(&opSt);
int i=0;
while(expr[i] != '\0')
{
if(isspace(expr[i])){i++;continue;} //跳过空格
// 是数字,处理多位数
if(isdigit(expr[i]) || expr[i]=='.')
{
double num = 0;
double dec = 0.1;
int hasDot = 0;
while(isdigit(expr[i]) || expr[i]=='.')
{
if(expr[i]=='.')
{
hasDot = 1;
i++;
continue;
}
if(!hasDot)
num = num*10 + (expr[i]-'0');
else
{
num += (expr[i]-'0') * dec;
dec *= 0.1;
}
i++;
}
NumPush(&numSt, num);
}
else if(expr[i] == '(')
{
OpPush(&opSt, expr[i]);
i++;
}
else if(expr[i] == ')')
{
// 弹出直到左括号
char op;
while(GetOpTop(&opSt) != '(')
{
OpPop(&opSt, &op);
double b,a;
NumPop(&numSt, &b);
NumPop(&numSt, &a);
NumPush(&numSt, calc(a,b,op));
}
OpPop(&opSt, &op); // 弹出'('丢弃
i++;
}
// + - * /
else
{
char curOp = expr[i];
while(!OpEmpty(&opSt) && getPriority(GetOpTop(&opSt)) >= getPriority(curOp))
{
char op;
OpPop(&opSt, &op);
double b,a;
NumPop(&numSt, &b);
NumPop(&numSt, &a);
NumPush(&numSt, calc(a,b,op));
}
OpPush(&opSt, curOp);
i++;
}
}
// 表达式读完,清算剩下运算符
while(!OpEmpty(&opSt))
{
char op;
OpPop(&opSt, &op);
double b,a;
NumPop(&numSt, &b);
NumPop(&numSt, &a);
NumPush(&numSt, calc(a,b,op));
}
double res;
NumPop(&numSt, &res);
return res;
}
int main()
{
char exp[] = "(1+2)*3-6/2";
printf("表达式 %s = %.2f\n", exp, Evaluate(exp));
return 0;
}
运行结果:(1+2)*3-6/2 = 6.00
4. 补充:中缀转后缀(逆波兰表达式)
四则运算另一条路线:先把中缀 → 后缀(逆波兰) ,再用一个栈计算后缀表达式。 后缀表达式优点:没有括号、不需要判断优先级 ,从左往右扫描,遇到数字入栈,遇到运算符弹出两个数计算。 示例:(1+2)*3-6/2 转后缀:1 2 + 3 * 6 2 / -
后缀求值极简逻辑:
遍历每个token:
数字 → 压栈
运算符 → pop b, pop a,计算a op b,结果压栈
最后栈顶就是答案
✅ 四则运算小结
- 双栈法:数栈、符号栈,优先级是核心
- 坑点:减法除法不满足交换律,出栈顺序不能颠倒
- 括号:
(提高内部优先级,遇到)强制计算到左括号
二、栈的递归应用
本质:程序里面的递归,底层就是操作系统的函数调用栈! 每一次递归调用,都会把当前函数的局部变量、参数、返回地址压入系统栈;递归到终止条件时逐层返回,不断出栈。栈的 LIFO 完美匹配递归 "先调用、后返回"。
1. 递归的栈模型图解(文字版)
以阶乘 fact(3):
fact(3) n=3,未结束 → 入栈
fact(2) n=2,未结束 → 入栈
fact(1) n=1,触发终止条件 return 1 → 出栈
fact(2)拿到返回值,计算 2*1=2 → 出栈
fact(3)拿到返回值,计算3*2=6 → 出栈
栈里保存每一层现场,后进先出返回。
2. 经典例子 1:阶乘(递归)
#include <stdio.h>
// 递归求n!
int fact(int n)
{
if(n == 0 || n == 1) // 递归出口(基线条件)
return 1;
return n * fact(n - 1); // 递归调用,现场压入系统栈
}
int main()
{
printf("5! = %d", fact(5));
return 0;
}
重点:递归深度不能无限大,栈的空间是有限的!递归太深会栈溢出(Stack Overflow)
3. 经典例子 2:斐波那契数列
int fib(int n)
{
if(n == 1 || n == 2)
return 1;
return fib(n-1)+fib(n-2);
}
⚠️ 缺点:朴素递归大量重复计算,同时递归深度大,容易栈溢出。
4. 手动栈模拟递归(非常重要考点!)
很多场景不允许递归(防止栈溢出),我们可以自己写栈,手动模拟递归过程,把递归改成非递归。 示例:非递归阶乘,用我们前面写的顺序栈模拟:
#include <stdio.h>
#define MaxSize 20
typedef struct{
int data[MaxSize];
int top;
}SqStack;
void Init(SqStack *s){s->top=0;}
void Push(SqStack *s, int x){s->data[s->top++]=x;}
int Pop(SqStack *s){return s->data[--s->top];}
int Empty(SqStack *s){return s->top == 0;}
// 栈模拟阶乘,非递归
int fact_stack(int n)
{
SqStack st;
Init(&st);
int res = 1;
while(n >= 1)
{
Push(&st, n);
n--;
}
while(!Empty(&st))
{
int t = Pop(&st);
res *= t;
}
return res;
}
int main()
{
printf("5! = %d", fact_stack(5));
return 0;
}
5. 递归常见栈应用场景
- 树的深度优先遍历(DFS,二叉树前 / 中 / 后序遍历,递归版底层都是栈)
- 迷宫回溯、八皇后、全排列回溯算法(回溯 = 递归 + 栈)
- 快速排序、归并排序递归版本
- 表达式递归求值
6. 递归的优缺点
✅ 优点:代码简洁,逻辑贴近数学定义 ❌ 缺点:
- 系统栈容量有限,深度过大会栈溢出
- 每层调用都要保存现场,额外开销大,效率低于循环
递归转非递归,就是用自定义栈去模拟系统栈
7. 初学者易错点
- 忘记写递归出口 → 无限递归,直接栈溢出
- 分不清递归的压栈时机:调用的时候压栈,return 的时候出栈
- 混淆:递归不是栈,递归是利用栈这种数据结构的特性来实现
三、栈整套知识点汇总(总复习)
- 基础:栈,LIFO,顺序栈、链栈(O (1) push/pop)
- 应用一:括号匹配(简单入门)
- 应用二:四则表达式求值(双栈法 / 逆波兰后缀表达式)
- 应用三:递归、回溯、DFS(系统栈、手动栈模拟递归)