介绍
调度场算法是计算机科学史上的经典算法之一,由 Dijkstra(听到这个名字大家应该都不陌生吧)发明,不仅应用广泛,也是考试面试的重要内容。
算法应用场景
是在编译器、计算器、表达式求值等场景中的底层核心算法
核心思想
只用一个栈将中缀表达式转换成后缀表达式(也叫逆波兰式)
那么在介绍这个算法前,我们就得先知道有三种不同的表达式:
- 中缀表达式:运算符在两个操作数中间,也是我们日常使用的表达式写法
- 前缀表达式(波兰式):运算符在两个操作数之前。
- 后缀表达式(逆波兰式):运算符在两个操作数之后。
那为什么要发明这个算法将中缀表达式转换成后缀表达式呢?原因就在于中缀表达式很难直接计算,而后缀表达式无优先级、无括号,计算机可以通过栈就能直接计算,因此这个算法就是为了解决计算机计算中缀表达式的麻烦
那知道了不同的表达式及其之间的联系后,我们就来看这个算法的几个核心思想:
- 算法用栈来暂存运算符,而数字就之间输出到后缀表达式的结果之中去
- 算法根据运算符优先级,决定运算符入栈还是弹出到后缀表达式的结果中去
- 括号强制改变运算符的顺序,因此括号内部的内容要优先处理
知道核心思想后,我们还得清楚从中缀表达式转换到后缀表达式的规则:
- 运算符优先级,从高到低为:
- 括号
() - 乘除
*/ - 加减
+-
- 括号
- 运算符栈的比较规则:
- 当栈顶运算符优先级 ≥ 当前运算符时:栈顶运算符弹出到后缀表达式的结果中,而当前运算符入栈
- 当栈顶运算符优先级 < 当前运算符时:当前运算符直接入栈即可
算法步骤
- 准备阶段:创建运算符栈,用来存运算符
+ - * / ( );创建存放后缀表达式结果的容器。 - 遍历中缀表达式:遍历中每次都要分情况来操作:
- 遇到数字:直接加入到结果之中
- 遇到左括号
(:直接压入栈 - 遇到右括号
):从栈顶不停弹出运算符到结果之中,直到遇到左括号(结束 (注: 左右括号在运算符栈中经历相关操作后就直接丢弃,不要加入到结果之中) - 遇到
+ - * /运算符:通过循环来不停比较:若栈顶运算符优先级 ≥ 当前运算符(这即是循环条件),则弹出栈顶运算符到结果之中;直到栈顶运算符优先级 < 当前运算符或栈已为空,则当前运算符入栈;注意,若栈顶是(则直接入栈
- 遍历结束:将栈中剩余的所有运算符依次弹出到结果之中
实例演示
比如将中缀表达式 1 * 3 - 4 * 2 / (5 - 1) 转换成后缀表达式:
| 遍历元素 | 操作 | 运算符栈 | 后缀表达式 |
|---|---|---|---|
1 | 数字,送入结果中 | 空 | 1 |
* | 栈空则直接入栈 | * | 1 |
3 | 数字,送入结果中 | * | 1 3 |
- | * 优先级 > - ,* 弹出,- 入栈 | - | 1 3 * |
4 | 数字,送入结果中 | - | 1 3 * 4 |
* | - 优先级 < * ,* 入栈 | - * | 1 3 * 4 |
2 | 数字,送入结果中 | - * | 1 3 * 4 2 |
/ | * 优先级 = / ,* 弹出,/ 入栈 | - / | 1 3 * 4 2 * |
( | 左括号,直接入栈 | - / ( | 1 3 * 4 2 * |
5 | 数字,送入结果中 | - / ( | 1 3 * 4 2 * 5 |
- | 栈顶是 ( ,则直接入栈 | - / ( - | 1 3 * 4 2 * 5 |
1 | 数字,送入结果中 | - / ( - | 1 3 * 4 2 * 5 1 |
) | 弹栈直到遇到 ( ,故把 - 弹出,( ) 则直接丢弃 | - / | 1 3 * 4 2 * 5 1 - |
| 遍历结束 | 弹出所有运算符送入结果中 | 空 | 1 3 * 4 2 * 5 1 - / - |
因此最终的后缀表达式为:1 3 * 4 2 * 5 1 - / -
若对结果不确定,我们还可以验证验证: 我们可以用栈来对后缀表达式求值,若得到的结果与用中缀表达式的结果一致,那我就有很大的信心能够确定这个后缀表达式是正确的。而具体的计算步骤如下,本文就不在具体演示了:
- 创建一个栈来暂存中间结果
- 从左往右依次读取后缀表达式的一个字符
- 若读入的是操作数,则将其压入栈中
- 若读入的是运算符,则从栈中连续弹出两个元素,进行相应的运算,并将结果压入栈中(注: 先弹出来的为运算符的右操作数,而后弹出来到则为左操作数)
- 若读入结束,则栈顶元素就是计算结果
示例代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;
// 判断是否为运算符
bool isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
// 获取运算符优先级
int priority(char c) {
if(c == '*' || c == '/') return 2;
if(c == '+' || c == '-') return 1;
return 0; // 代表这是括号,虽然括号优先级最高,但为了不同情况弹栈入栈操作逻辑的一致性,还是把括号的优先级设为了 0
}
// 调度场算法 ---> 中缀转为后缀
string Fix(string inf) {
stack<char> op;
// 运算符栈
string pos;
// inf 为中缀表达式,pos 为后缀表达式结果
for(char c : inf) {
// 情况1:数字 → 直接加入结果
if(isdigit(c)) {
pos += c;
}
// 情况2:左括号 → 入栈
else if(c == '(') {
op.push(c);
}
// 情况3:右括号 → 弹出直到左括号
else if(c == ')') {
while(!op.empty() && op.top() != '(') {
pos += op.top();
op.pop();
}
op.pop(); // 丢弃左括号
}
// 情况4:运算符 → 按优先级处理
else if(isOperator(c)) {
while(!op.empty() && priority(op.top()) >= priority(c)) {
pos += op.top();
op.pop();
}
op.push(c);
}
}
// 弹出栈中剩余运算符
while(!op.empty()) {
pos += op.top();
op.pop();
}
return pos;
}
// 测试
int main() {
string inf = "1*3-4*2/(5-1)";
cout << "后缀表达式:" << Fix(inf) << endl;
// 输出:13*42*51-/-
return 0;
}
复杂度分析
- 时间复杂度:
O(n) - 空间复杂度:
O(n)
算法优点
- 高效简洁:只用一个栈就可实现求值,且时间复杂度仅
O(n) - 化繁为简:完美处理优先级、括号,计算机无法直接计算的中缀表达式,可瞬间转成可直接计算的后缀表达式来计算
- 应用广泛:计算器、编译器、表达式解析、脚本语言核心
- 经典算法:此算法是栈最经典的实战应用
拓展
还有一个小小的拓展就是上述所说的三个表达式还与表达式树有着很深的联系,这三个表达式其实就对应着表达式树的三种遍历方式