【数据结构实战】栈的经典应用:后缀表达式求值 +中缀转后缀 ,原理 + 代码双通透

在数据结构的栈应用中,后缀表达式(逆波兰表达式)是经典的实战场景。相较于我们日常使用的中缀表达式,后缀表达式无需括号 即可明确表达运算优先级,极大简化了计算机的表达式解析过程。本文将从后缀表达式的核心概念出发,结合 C 语言完整代码,讲解后缀表达式求值中缀表达式转后缀表达式的实现思路,同时对代码进行逐行详细注释,让新手也能轻松理解。

一、核心概念铺垫

1. 中缀表达式

日常书写的表达式形式,运算符位于两个操作数中间,需要通过括号运算符优先级 确定运算顺序,例如:x/(i-j)*y3+4*2。计算机解析中缀表达式时,需要频繁处理优先级和括号,效率较低。

2. 后缀表达式(逆波兰表达式)

运算符位于两个操作数之后,天然无需括号,运算顺序由表达式本身决定,例如:

  • 中缀3+4*2 → 后缀342*+
  • 中缀x/(i-j)*y → 后缀xij-/y*
  • 中缀82/2+56*-(本文测试用例)→ 直接为后缀形式,计算结果为-24

3. 栈的核心作用

无论是后缀表达式求值 还是中缀转后缀,栈都是核心数据结构:

  • 求值:栈存储操作数,遇到运算符则弹出两个操作数计算,结果重新入栈;
  • 转换:栈存储运算符,遇到操作数直接输出,遇到运算符则根据优先级处理栈内元素,保证运算顺序。

二、后缀表达式求值:原理 + 完整注释代码

1. 求值核心原理

  1. 初始化一个空栈,用于存储操作数;
  2. 从左到右遍历后缀表达式的每个字符:
    • 若为操作数,直接压入栈中;
    • 若为运算符 ,从栈中依次弹出右操作数(op2)和左操作数(op1) (注意弹出顺序,后缀表达式中后弹出的是左操作数),用运算符计算op1 运算符 op2,将结果压入栈;
  3. 遍历结束后,栈中仅剩一个元素,即为表达式的计算结果。

2. 完整注释代码(C 语言)

该代码实现了栈的基本操作(初始化、判空、入栈、出栈、取栈顶),并基于栈完成后缀表达式求值,测试用例为82/2+56*-,计算结果为-24

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100  // 定义栈的最大容量,避免溢出

// 定义栈中存储的元素类型为int(操作数为整型)
typedef int ElemType;

// 定义栈的结构体:顺序栈实现
typedef struct 
{
	ElemType *data;  // 动态数组,存储栈的元素
	int top;         // 栈顶指针,初始为-1(空栈)
}Stack;

// 定义token类型:对表达式中的字符进行分类,方便解析
typedef enum
{
	LEFT_PARE,  // 左括号 ( ,枚举值0
	RIGHT_PARE, // 右括号 ) ,枚举值1
	ADD,        // 加号 + ,枚举值2
	SUB,        // 减号 - ,枚举值3
	MUL,        // 乘号 * ,枚举值4
	DIV,        // 除号 / ,枚举值5
	MOD,        // 取模 % ,枚举值6
	EOS,        // 表达式结束符 \0 ,枚举值7
	NUM         // 数字操作数 ,枚举值8
} contentType;

char expr[] = "82/2+56*-";  // 待求值的后缀表达式

// 栈的初始化:分配内存,初始化栈顶指针
Stack* initStack()
{
    // 为栈结构体分配内存
	Stack *s = (Stack*)malloc(sizeof(Stack));
    // 为栈的动态数组分配内存,容量为MAXSIZE
	s->data = (ElemType*)malloc(sizeof(ElemType) * MAXSIZE);
	s->top = -1;  // 空栈的栈顶指针为-1
	return s;     // 返回初始化后的栈指针
}

// 判断栈是否为空:栈顶指针为-1则为空
// 返回值:1-空栈,0-非空
int isEmpty(Stack *s)
{
	if (s->top == -1)
	{
		// printf("空的\n"); // 调试用,可注释
		return 1;
	}
	else
	{
		return 0;
	}
}

// 进栈/压栈操作:将元素e压入栈s
// 返回值:1-入栈成功,0-入栈失败(栈满)
int push(Stack *s, ElemType e)
{
    // 栈满判断:栈顶指针 >= 最大容量-1
	if (s->top >= MAXSIZE - 1)
	{
		printf("栈满,入栈失败\n");
		return 0;
	}
	s->top++;                // 栈顶指针上移一位
	s->data[s->top] = e;     // 将元素e存入栈顶位置
	return 1;
}

// 出栈操作:将栈顶元素弹出,存入*e中
// 返回值:1-出栈成功,0-出栈失败(空栈)
int pop(Stack *s, ElemType *e)
{
    // 空栈判断,无法出栈
	if (isEmpty(s))
	{
		printf("空栈,出栈失败\n");
		return 0;
	}
	*e = s->data[s->top];    // 将栈顶元素赋值给*e
	s->top--;                // 栈顶指针下移一位,完成出栈
	return 1;
}

// 获取栈顶元素:将栈顶元素存入*e中,不弹出
// 返回值:1-获取成功,0-获取失败(空栈)
int getTop(Stack *s, ElemType *e)
{
	if (isEmpty(s))
	{
		printf("空栈,无栈顶元素\n");
		return 0;
	}
	*e = s->data[s->top];    // 仅获取栈顶元素,不修改栈顶指针
	return 1;
}

// 解析表达式字符:将当前字符转换为对应的token类型
// symbol:存储当前解析的字符,index:表达式遍历的索引(传地址,实现索引自增)
contentType getToken(char *symbol, int *index)
{
	*symbol = expr[*index];  // 获取当前索引的字符
	*index = *index + 1;     // 索引自增,指向下一个字符
    // 根据字符类型返回对应的token
	switch(*symbol)
	{
		case '(': return LEFT_PARE;
		case ')': return RIGHT_PARE;
		case '+': return ADD;
		case '-': return SUB;
		case '*': return MUL;
		case '/': return DIV;
		case '%': return MOD;
		case '\0': return EOS; // 表达式结束
		default: return NUM;   // 非上述字符,判定为数字操作数
	}
}

// 后缀表达式求值核心函数:基于栈实现计算
// 参数s:初始化后的空栈,返回值:1-计算成功
int eval(Stack *s)
{
	char symbol;            // 存储当前解析的字符
	int op1, op2;           // 存储弹出的两个操作数,op1左操作数,op2右操作数
	int index = 0;          // 表达式遍历的起始索引,从0开始
	contentType token;      // 存储当前字符的token类型
	ElemType result;        // 存储最终的计算结果

    // 解析第一个字符,获取其token类型
	token = getToken(&symbol, &index);
    // 遍历表达式,直到遇到结束符EOS
	while(token != EOS)
	{
		// 如果是数字操作数,压入栈中(字符转整型:symbol - '0')
		if (token == NUM)
		{
			push(s, symbol - '0');
		}
		// 如果是运算符,弹出两个操作数计算,结果入栈
		else
		{
			pop(s, &op2);  // 先弹出右操作数
			pop(s, &op1);  // 后弹出左操作数
            // 根据运算符类型计算,结果压入栈
			switch(token)
			{
				case ADD: push(s, op1 + op2); break; // 加法
				case SUB: push(s, op1 - op2); break; // 减法
				case MUL: push(s, op1 * op2); break; // 乘法
				case DIV: push(s, op1 / op2); break; // 除法(整型除法)
				case MOD: push(s, op1 % op2); break; // 取模
				default: break; // 无其他运算符,无需处理
			}
		}
        // 解析下一个字符,更新token
		token = getToken(&symbol, &index);
	}
    // 遍历结束,栈中仅剩一个元素,即为结果,弹出并打印
	pop(s, &result);
	printf("后缀表达式%s的计算结果为:%d\n", expr, result);
	return 1;
}

// 主函数:程序入口,初始化栈并调用求值函数
int main(int argc, char const *argv[])
{
	Stack *s = initStack();  // 初始化栈
	eval(s);                 // 后缀表达式求值
	return 0;
}

3. 运行结果

编译并运行代码,输出结果为:

4. 关键注意点

  • 操作数弹出顺序:先弹 op2,后弹 op1 ,因为后缀表达式中运算符在操作数后,例如82/表示8/2,而非2/8
  • 字符转整型:数字字符'0'-'9'减去'0'即可得到对应的整型数值;
  • 栈的判空 / 判满:所有栈操作前必须做判空 / 判满,避免数组越界和空栈操作。

三、中缀表达式转后缀表达式:原理 + 完整注释代码

1. 转换核心原理

转换的关键是利用栈管理运算符优先级 ,定义栈内优先级栈外优先级(优先级数值越大,运算优先级越高),本文定义的优先级规则:

  • 左括号(:栈外优先级最高(20),栈内优先级最低(0),保证括号内的表达式优先处理;
  • 乘 / 除 / 取模(* / %):优先级 13,高于加 / 减(+ -)的 12;
  • 加 / 减(+ -):优先级 12;
  • 结束符 EOS:栈内 / 栈外优先级均为 0。

转换步骤:

  1. 初始化一个栈,栈底压入结束符EOS,用于遍历结束后处理栈内剩余运算符;
  2. 从左到右遍历中缀表达式的每个字符:
    • 若为操作数,直接输出(即为后缀表达式的一部分);
    • 若为右括号) ,从栈中弹出运算符并输出,直到遇到左括号(,弹出左括号但不输出(消除括号);
    • 若为其他运算符(含左括号) ,比较栈顶运算符的栈内优先级当前运算符的栈外优先级
      • 若栈顶优先级 当前运算符栈外优先级,弹出栈顶运算符并输出,重复比较;
      • 若栈顶优先级 < 当前运算符栈外优先级,将当前运算符压入栈;
  3. 遍历结束后,从栈中依次弹出运算符并输出,直到遇到结束符EOS,最终输出的序列即为后缀表达式。

2. 完整注释代码(C 语言)

该代码在栈基本操作的基础上,实现了中缀表达式转后缀表达式 ,测试用例为x/(i-j)*y,转换结果为xij-/y*

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100  // 栈的最大容量,防止栈溢出

// 枚举类型:定义表达式中所有字符的类型(token)
// 用于区分运算符、括号、结束符、操作数,方便优先级判断
typedef enum
{
    LEFT_PARE,  // 0 左括号 (
    RIGHT_PARE, // 1 右括号 )
    ADD,        // 2 加号 +
    SUB,        // 3 减号 -
    MUL,        // 4 乘号 *
    DIV,        // 5 除号 /
    MOD,        // 6 取模 %
    EOS,        // 7 字符串结束符 \0
    NUM         // 8 操作数(字母/数字,如x、y、123)
} contentType;

// 栈存储的元素类型 = 字符类型枚举,统一类型,避免报错
typedef contentType ElemType;

// 栈的结构体定义
typedef struct
{
    ElemType* data;  // 动态数组,存储栈内元素(运算符/括号/结束符)
    int top;         // 栈顶指针,-1代表空栈,指向栈顶元素下标
} Stack;

// 待转换的中缀表达式(支持字母变量)
char expr[] = "x/(i-j)*y";

// ------------------- 栈的基础操作函数(带详细注释) -------------------
// 功能:初始化栈,分配内存,设置空栈状态
// 返回值:初始化好的栈指针
Stack* initStack()
{
    // 分配栈结构体的内存
    Stack* s = (Stack*)malloc(sizeof(Stack));
    // 分配栈数据数组的内存(最大容量MAXSIZE)
    s->data = (ElemType*)malloc(sizeof(ElemType) * MAXSIZE);
    s->top = -1;  // 栈顶指针置-1,表示空栈
    return s;
}

// 功能:判断栈是否为空
// 参数:s-栈指针
// 返回值:1=空,0=非空
int isEmpty(Stack* s)
{
    return (s->top == -1);
}

// 功能:入栈操作(将元素压入栈顶)
// 参数:s-栈指针,e-待入栈的元素
// 返回值:1=成功,0=失败(栈满)
int push(Stack* s, ElemType e)
{
    // 栈满判断:栈顶指针到达最大下标,无法入栈
    if (s->top >= MAXSIZE - 1)
    {
        printf("栈满,入栈失败\n");
        return 0;
    }
    s->top++;                // 栈顶指针上移
    s->data[s->top] = e;     // 将元素存入栈顶
    return 1;
}

// 功能:出栈操作(取出栈顶元素)
// 参数:s-栈指针,e-存储出栈的元素
// 返回值:1=成功,0=失败(空栈)
int pop(Stack* s, ElemType* e)
{
    if (isEmpty(s))
    {
        printf("空栈,出栈失败\n");
        return 0;
    }
    *e = s->data[s->top];  // 取出栈顶元素
    s->top--;              // 栈顶指针下移
    return 1;
}

// ------------------- 表达式处理工具函数 -------------------
// 功能:从表达式中读取一个字符,并返回它的token类型
// 参数:symbol-存储读取到的字符,index-表达式遍历下标(会自动递增)
// 返回值:字符对应的枚举类型
contentType getToken(char* symbol, int* index)
{
    *symbol = expr[*index];  // 读取当前下标的字符
    *index = *index + 1;    // 下标+1,准备读取下一个字符
    // 根据字符返回对应的类型
    switch (*symbol)
    {
        case '(': return LEFT_PARE;
        case ')': return RIGHT_PARE;
        case '+': return ADD;
        case '-': return SUB;
        case '*': return MUL;
        case '/': return DIV;
        case '%': return MOD;
        case '\0': return EOS;
        default: return NUM;  // 字母/数字都判定为操作数
    }
}

// 功能:将运算符token转换为字符打印输出
// 参数:token-运算符类型
// 返回值:1=打印成功,0=不是运算符
int print_token(contentType token)
{
    switch (token)
    {
        case ADD: printf("+"); break;
        case SUB: printf("-"); break;
        case MUL: printf("*"); break;
        case DIV: printf("/"); break;
        case MOD: printf("%%"); break;  // %% 转义输出%
        default: return 0;  // 非运算符,不打印
    }
    return 1;
}

// ------------------- 中缀转后缀核心函数 -------------------
// 功能:将中缀表达式转换为后缀表达式并输出
// 参数:s-用于存储运算符的栈
void postfix(Stack* s)
{
    // 栈内优先级:运算符在栈内时的优先级
    // 索引对应token枚举值:( ) + - * / % \0
    int in_stack[] = {0, 19, 12, 12, 13, 13, 13, 0};
    // 栈外优先级:运算符在表达式中的优先级
    int out_stack[] = {20, 19, 12, 12, 13, 13, 13, 0};

    contentType token;  // 存储当前字符的类型
    int index = 0;      // 表达式遍历下标,从0开始
    ElemType e;         // 存储出栈的元素
    char symbol;        // 存储当前读取的字符

    // 初始化栈:压入结束符EOS作为栈底标记,方便最后弹出所有运算符
    push(s, EOS);

    // 读取第一个字符,获取token类型
    token = getToken(&symbol, &index);

    // 循环遍历表达式,直到读取到结束符\0
    while (token != EOS)
    {
        // 情况1:当前字符是操作数(字母/数字)→ 直接输出
        if (token == NUM)
        {
            printf("%c", symbol);
        }
        // 情况2:当前字符是右括号 )
        else if (token == RIGHT_PARE)
        {
            // 弹出栈中运算符,直到遇到左括号 (
            while (s->data[s->top] != LEFT_PARE)
            {
                pop(s, &e);
                print_token(e);  // 弹出的运算符直接输出
            }
            pop(s, &e);  // 弹出左括号 (,不输出(消除括号)
        }
        // 情况3:左括号/运算符 → 按优先级入栈
        else
        {
            // 规则:栈顶运算符优先级 ≥ 当前运算符 → 弹出栈顶并输出
            while (in_stack[s->data[s->top]] >= out_stack[token])
            {
                pop(s, &e);
                print_token(e);
            }
            push(s, token);  // 当前运算符入栈
        }
        // 读取下一个字符,更新token
        token = getToken(&symbol, &index);
    }

    // 表达式遍历完毕,弹出栈中剩余的所有运算符
    pop(s, &e);
    while (e != EOS)  // 直到弹出栈底的结束符为止
    {
        print_token(e);
        pop(s, &e);
    }

    printf("\n");  // 换行,美化输出
}

// ------------------- 主函数:程序入口 -------------------
int main()
{
    Stack* s = initStack();  // 初始化运算符栈
    printf("中缀表达式:%s\n", expr);
    printf("转换后的后缀表达式:");
    postfix(s);  // 执行中缀转后缀
    return 0;
}

3. 运行结果

编译并运行代码,输出结果为:

4. 关键注意点

  • 优先级数组:in_stackout_stack索引与 token 枚举值严格对应,不能错位;
  • 左括号处理:栈外优先级最高,确保能直接入栈;栈内优先级最低,确保括号内的运算符优先处理;
  • 操作数兼容:代码中NUM不仅包含数字,还包含字母(如 x/i/j/y),可直接处理含变量的中缀表达式;
  • 栈底 EOS:必须在栈底压入 EOS,否则遍历结束后无法终止栈内运算符的弹出。

四、拓展与思考

  1. 支持多位数操作数 :本文代码仅支持单个数字 / 字母操作数,可扩展getToken函数,遍历连续的数字字符并转换为整型,实现多位数求值;
  2. 浮点型运算 :将ElemTypeint改为float/double,修改除法为浮点除法,即可支持小数表达式;
  3. 后缀转中缀 :基于栈实现,遇到操作数入栈,遇到运算符弹出两个操作数,拼接为(op1 运算符 op2)后重新入栈,最终栈内元素即为中缀表达式(会带冗余括号,可后续优化);
  4. 运算符优先级扩展 :可添加平方、开方等运算符,只需在token枚举中添加,并更新优先级数组即可。

五、总结

后缀表达式是栈的经典应用,其核心优势是消除了括号和优先级的歧义 ,让计算机能以线性方式高效解析表达式。本文通过两个完整的 C 语言代码,分别实现了后缀表达式求值中缀转后缀,并对代码进行了逐行注释,核心要点可总结为:

  1. 求值:栈存操作数,遇运算符弹二算一,结果入栈
  2. 转换:栈存运算符,遇操作数直输,遇运算符按优先级处理
  3. 栈的操作:所有入栈 / 出栈前必须判空 / 判满,避免程序崩溃。
相关推荐
炽烈小老头2 小时前
【 每天学习一点算法 2026/03/30】跳跃游戏
学习·算法
m0_626535202 小时前
今日需要注意
数据结构
wuweijianlove2 小时前
算法性能预测的统计模型与参数敏感性分析的技术6
算法
Just right2 小时前
重学算法 数组 LC27移除元素
数据结构·算法
郝学胜-神的一滴2 小时前
巧解括号序列分解问题:栈思想的轻量实现
开发语言·数据结构·c++·算法·面试
Jasmine_llq2 小时前
《B4496 [GESP202603 一级] 数字替换》
数据结构·字符串遍历算法·字符替换算法·条件判断算法·字符串输入输出算法·顺序处理算法·批量字符修改算法
计算机安禾2 小时前
【数据结构与算法】第15篇:队列(二):链式队列的实现与应用
c语言·开发语言·数据结构·c++·学习·算法·visual studio
算法鑫探3 小时前
C语言密码验证:3次机会解锁
c语言·数据结构·算法·新人首发
穿条秋裤到处跑3 小时前
每日一道leetcode(2026.03.30):判断通过操作能否让字符串相等 II
算法·leetcode