栈的四则运算、栈的递归应用

一、用栈实现四则运算

核心思路:双栈法

  • 操作数栈:存放数字
  • 运算符栈:存放 + - * / ( ) 规则:先把中缀表达式,按照运算符优先级,决定是压栈还是弹出计算;括号改变优先级。 运算符优先级:( < +、- < *、/ < )

1. 优先级判断函数

复制代码
// 获取运算符优先级
int getPriority(char op)
{
    if(op == '(') return 1;
    else if(op == '+' || op == '-') return 2;
    else if(op == '*' || op == '/') return 3;
    else return 0; // ')'或者非法字符
}

规则解读:

  1. 遇到数字:直接压入操作数栈
  2. 遇到 ( 左括号:直接压运算符栈
  3. 遇到 ) 右括号:不断弹出运算符计算,直到碰到 (,弹出(丢弃
  4. 遇到运算符:只要栈顶运算符优先级 ≥ 当前运算符,就弹出计算;直到栈顶优先级更小或者栈空,再压入当前运算符
  5. 表达式扫描结束后,运算符栈里剩下的依次弹出计算,最终操作数栈剩下唯一结果

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. 递归常见栈应用场景

  1. 树的深度优先遍历(DFS,二叉树前 / 中 / 后序遍历,递归版底层都是栈)
  2. 迷宫回溯、八皇后、全排列回溯算法(回溯 = 递归 + 栈)
  3. 快速排序、归并排序递归版本
  4. 表达式递归求值

6. 递归的优缺点

✅ 优点:代码简洁,逻辑贴近数学定义 ❌ 缺点:

  1. 系统栈容量有限,深度过大会栈溢出
  2. 每层调用都要保存现场,额外开销大,效率低于循环

递归转非递归,就是用自定义栈去模拟系统栈

7. 初学者易错点

  1. 忘记写递归出口 → 无限递归,直接栈溢出
  2. 分不清递归的压栈时机:调用的时候压栈,return 的时候出栈
  3. 混淆:递归不是栈,递归是利用栈这种数据结构的特性来实现

三、栈整套知识点汇总(总复习)

  1. 基础:栈,LIFO,顺序栈、链栈(O (1) push/pop)
  2. 应用一:括号匹配(简单入门)
  3. 应用二:四则表达式求值(双栈法 / 逆波兰后缀表达式)
  4. 应用三:递归、回溯、DFS(系统栈、手动栈模拟递归)
相关推荐
stolentime1 小时前
CF2277E Tree Guessing题解
c++·算法·贪心算法·交互·ad-hoc
逐光者9332 小时前
杨氏模量:从定义到工程应用
算法
荆棘鸟智能3 小时前
扬尘在线监测数据接入与异常清洗:从断点补传到质量审计
人工智能·算法·系统架构
动词ing3 小时前
【题目练习】旋转图像
算法
天疆说3 小时前
【无标题】
算法
GG.WinGel3 小时前
三元组中心问题
java·数据结构·算法·青少年编程·蓝桥杯
开开心心就好4 小时前
电脑小说阅读器,支持朗读和自定义皮肤
服务器·开发语言·javascript·算法·pdf·c#·音视频
程序员清风4 小时前
缓存、索引与数据结构的工程选择
数据结构·spring·缓存
闻缺陷则喜何志丹4 小时前
【c++20】协程与屏障
c++·算法·c++20·携程·屏障