表达式计算

乘上与平时相反的列车,为了去看曾未见过的风景

表达式计算是程序设计中常见的问题,通常考察我们对于栈的理解和运用。但是在表达式计算的题目中,经常出现各种细节的改变,如

  • 是否考察一元运算符”-“。
  • 是否考虑括号 ()。
  • 是否考虑乘除高级运算。

有时一个小小的条件改变就让我们变得手足无措,因此本博客将从易到难,带你体验两种不同的设计方法,来解决此类题目。

  • 延迟计算法
  • 双栈法

简单加减运算

我们首先通过一种最简单的情况来理解两种方法的核心思想,题目:

要求:实现只包含线性加减运算表达式的求值。

示例:1 + 1 + 2 - 3 + 4 - 5

延迟计算法

顾名思义,延迟计算法的计算是延迟完成的。我们将在读取到下一个运算符时,才进行上一个运算符及其操作数的运算。

核心思路如下:

  1. 维护一个全局计算结果 result和一个当前的符号 sign(+1-1)
  2. 从左到右遍历字符串。
  3. 遇到数字,将他累加的result中。(result = sign * currentNumber)
  4. 遇到 +-,跟新 sign(1表示加, -1表示减)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Example1{

int i = 0;

public int calculate(String s) {
int result = 0;
int currnetNumber = 0;
int sign = 1;

while(i < s.length()){
if(Character.isDigit(s.charAt(i))){
currnetNumber = currnetNumber * 10 + (s.charAt(i) - '0');
}
else if(s.charAt(i) == '+'){
result += sign * currnetNumber;
sign = 1;
currnetNumber = 0;
}
else if(s.charAt(i) == '-'){
result += sign * currnetNumber;
sign = -1;
currnetNumber = 0;
}
i++;
}

return result;
}

public static void main(String[] args) {
Example1 s = new Example1();
System.out.println(s.calculate("1 + 2 - 3- 5 + 1"));
}
}

并且我们发现延迟计算方法天然的支持处理一元运算符”-“的问题。因为我们几乎将每个运算符都当作一个一元运算符来处理。

我们将整个表达式视作一些有符号数的相加运算。比如 -1+1-2

程序处理时将视为:(-1)+1+(-2)

双栈法

双栈法的特点是创建两个栈:

  • 数字栈:只存放数字
  • 符号栈:只存放运算符号

数字栈用于压入数字,符号栈压入符号。每读取一对数字和一个操作符后就进行一次计算,从左向右计算。

算法流程:

  1. 初始化两个栈
  2. 从左到右遍历表达式
  3. 遇到数字:解析出完整数字,压入 nums
  4. 遇到操作符:将操作符入栈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

public class Example {

static Set<Character> opsTable = new HashSet<>();
static {
opsTable.add('+');
opsTable.add('-');
}

public int calculate(String s){
LinkedList<Integer> nums = new LinkedList<>();
LinkedList<Character> ops = new LinkedList<>();
int i = 0;
while(i < s.length()){
if(Character.isDigit(s.charAt(i))){
int num = 0;
while(i < s.length() && Character.isDigit(s.charAt(i))){
num = num*10 + s.charAt(i) - '0';
i++;
}
nums.push(num);
}
else if(opsTable.contains(s.charAt(i))){
if(i == 0 && s.charAt(i) == '-'){
nums.push(0);
}
while(!ops.isEmpty()){
applyOp(nums, ops);
}
ops.push(s.charAt(i));
i++;
}
else{
i++;
}
}
while (!ops.isEmpty()) {
applyOp(nums, ops);
}
return nums.pop();
}

private void applyOp(LinkedList<Integer> nums, LinkedList<Character> ops){
int num2 = nums.pop();
int num1 = nums.pop();
char op = ops.pop();
switch (op) {
case '+':
nums.push(num1 + num2);
break;
case '-':
nums.push(num1 - num2);
break;
default:
break;
}
}

public static void main(String[] args) {
Example s = new Example();
System.out.println(s.calculate(" 2-1 + 2 "));
}
}

双栈发并不能天然的支持单元运算符’-‘,需要我们特殊处理。我们将 -a转化为 0-a的形式。

这是应为双栈法的处理过程将每个运算符都当作二元运算来处理。每次运算都是两个数进行运算,逐渐和二为一。

如此简单的情况下,可能看不出来双栈法的好处,但是双栈法其实是实现计算器最经典、最强大的标准算法之一。

带括号加减表达式

224. 基本计算器 - 力扣(LeetCode)

1760336450189

1760336623329

接下来我们来完成leetcode这个题目,该题被标记为困难,但是只要理解上面的两种算法,要解决该题并不困难。

延迟计算

延迟计算方法是这一题的官方题解。因为该题要求考虑一元运算符,且不考虑乘除运算。

在不考虑乘除时,我们可以不使用递归。流程如下:

  • 状态result(当前总和),sign(下一个符号)
  • 遇到 (:把result和sign这两个数值压入栈。然后重置 result = 0, sign = 1重新开始计算。
  • 遇到 ):完成括号内的计算得到 currentResult。然后从栈中弹出之前的 resultsign,进行 result = result + sign * currentResult

但是为了铺垫接下来再增加乘法运算,所以此处我使用递归的方法,流程如下:

  • 遇到 (:令 currentNum = calculate(s)
  • 遇到 ):跳出循环,将值返回给上层。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Example1{

int i = 0;

public int calculate(String s) {
int result = 0;
int currnetNumber = 0;
int sign = 1;

while(i < s.length()){
if(Character.isDigit(s.charAt(i))){
currnetNumber = currnetNumber * 10 + (s.charAt(i) - '0');
i++;
}
else if(s.charAt(i) == '+'){
result += sign * currnetNumber;
sign = 1;
currnetNumber = 0;
i++;
}
else if(s.charAt(i) == '-'){
result += sign * currnetNumber;
sign = -1;
currnetNumber = 0;
i++;
}
else if(s.charAt(i) == '('){
i++;
currnetNumber = calculate(s);
}
else if(s.charAt(i) == ')'){
result += sign * currnetNumber;
currnetNumber = 0;
i++;
break;
}
else
i++;
}
result += sign*currnetNumber;
return result;
}

public static void main(String[] args) {
Example1 s = new Example1();
System.out.println(s.calculate("(1+(4+5+2)-3)+(6+8)"));
}
}

双栈法

双栈法要将括号当作栈中的分割符,每次计算不能越过正括号 (,在遇到反括号 )时,必须计算两个括号间的所有运算。

  • 遇到 (:将 (压入符号栈
  • 遇到 ):进行运算,直到 (在符号栈栈顶。最后弹出 (
  • 一元运算符:必须跳过空格去查找上一个字符是不是 (,或 i==0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

public class Example {

static Set<Character> opsTable = new HashSet<>();
static {
opsTable.add('+');
opsTable.add('-');
}

public int calculate(String s){
LinkedList<Integer> nums = new LinkedList<>();
LinkedList<Character> ops = new LinkedList<>();
int i = 0;
while(i < s.length()){
if(s.charAt(i) == ' '){
i++;
continue;
}
if(Character.isDigit(s.charAt(i))){
int num = 0;
while(i < s.length() && Character.isDigit(s.charAt(i))){
num = num*10 + s.charAt(i) - '0';
i++;
}
nums.push(num);
}
else if(s.charAt(i) == '('){
ops.push('(');
i++;
}
else if(s.charAt(i) == ')'){
while(ops.peek()!= '('){
applyOp(nums, ops);
}
ops.pop();
i++;
}
else if(opsTable.contains(s.charAt(i))){
// 检查是否为一元负号
// 条件:当前是第一个字符,或者前一个字符不是数字也不是右括号
boolean isUnary = false;
if (s.charAt(i) == '-') {
int prev_i = i - 1;
// 跳过前面的空格
while(prev_i >= 0 && s.charAt(prev_i) == ' ') {
prev_i--;
}
if (prev_i < 0 || (s.charAt(prev_i) != ')' && !Character.isDigit(s.charAt(prev_i)))) {
isUnary = true;
}
}

if (isUnary) {
nums.push(0);
}

while(!ops.isEmpty() && ops.peek() != '('){
applyOp(nums, ops);
}
ops.push(s.charAt(i));
i++;
}
else{
i++;
}
}
while (!ops.isEmpty()) {
applyOp(nums, ops);
}
return nums.pop();
}

private void applyOp(LinkedList<Integer> nums, LinkedList<Character> ops){
int num2 = nums.pop();
int num1 = nums.pop();
char op = ops.pop();
switch (op) {
case '+':
nums.push(num1 + num2);
break;
case '-':
nums.push(num1 - num2);
break;
default:
break;
}
}

public static void main(String[] args) {
Example s = new Example();
System.out.println("2-(5-6) = " + s.calculate("2-(5-6)")); // 应该输出 3
System.out.println("1-( -2) = " + s.calculate("1-( -2)")); // 应该输出 3
}
}

高级运算符乘法

除法会导致计算过程中出现浮点数,所以我们通常只考察乘法,以确保数据类型的一致性。

我们来看看牛客中的这道题。在加减括号中增加了运算符乘法。不过不用考虑一元运算符”-“。但是我们以下的代码都是支持一元运算符”-“的。

1760342960954

延迟计算法

对于有高优先级的表达式,不能直接使用result进行简单的线性相加。我们必须使用一个栈,将所有需要相加的数都压入栈中。如果遇到乘法,则立即计算,将所有相连的乘法运算最终计算为一个数。则最后栈中只剩下所有需要相加的数。

步骤:

  1. 遇到 *:弹出刚刚入栈的数字,赋值给symbol。

解释:1 + 2 * 5 * 3

之前的延迟计算法中,sign通过1和-1表示加减,这里的加减同时可以理解为:+1*a,+(-1)*a

即,加上a的一倍,或加上a的负一倍。

当我们读取字符 *时,我们将2赋值给Symbol。则可以表示为加上5的2倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

class Solution {

static Set<Character> operator = new HashSet<>();
static{
operator.add('+');
operator.add('-');
operator.add('*');
}
int i = 0;

public int calculate(String s) {
LinkedList<Integer> numStack = new LinkedList<>();
numStack.push(0);
int currnetNumber = 0;
int symbol = 1;
while(i < s.length()){
if(Character.isDigit(s.charAt(i))){
currnetNumber = currnetNumber*10 + s.charAt(i) - '0';
i++;
}
else if(operator.contains(s.charAt(i))){
numStack.push(symbol * currnetNumber);
if(s.charAt(i) == '+'){
symbol = 1;
}
else if(s.charAt(i) == '-'){
symbol = -1;
}
else if(s.charAt(i) == '*'){
symbol = numStack.pop();
}
currnetNumber = 0;
i++;
}
else if(s.charAt(i) == '('){
i++;
currnetNumber = symbol*calculate(s);
symbol = 1;
}
else if(s.charAt(i) == ')'){
i++;
break;
}
else{
i++;
}
}
numStack.push(symbol * currnetNumber);
int result = 0;
while(!numStack.isEmpty()){
result += numStack.pop();
}
return result;
}

public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.calculate("(2*(3-4))*5"));
}
}

双栈法

双栈法的拓展性很好,也就是程序的解耦性很好。添加乘法运算只需添加很少的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

public class Solution {

static Set<Character> opsTable = new HashSet<>();
static {
opsTable.add('+');
opsTable.add('-');
opsTable.add('*');
}

public int solve(String s){
LinkedList<Integer> nums = new LinkedList<>();
LinkedList<Character> ops = new LinkedList<>();
int i = 0;
while(i < s.length()){
if(s.charAt(i) == ' '){
i++;
continue;
}
if(Character.isDigit(s.charAt(i))){
int num = 0;
while(i < s.length() && Character.isDigit(s.charAt(i))){
num = num*10 + s.charAt(i) - '0';
i++;
}
nums.push(num);
}
else if(s.charAt(i) == '('){
ops.push('(');
i++;
}
else if(s.charAt(i) == ')'){
while(ops.peek()!= '('){
applyOp(nums, ops);
}
ops.pop();
i++;
}
else if(opsTable.contains(s.charAt(i))){
// 检查是否为一元负号
// 条件:当前是第一个字符,或者前一个字符不是数字也不是右括号
boolean isUnary = false;
if (s.charAt(i) == '-') {
int prev_i = i - 1;
// 跳过前面的空格
while(prev_i >= 0 && s.charAt(prev_i) == ' ') {
prev_i--;
}
if (prev_i < 0 || (s.charAt(prev_i) != ')' && !Character.isDigit(s.charAt(prev_i)))) {
isUnary = true;
}
}
if (isUnary) {
nums.push(0);
}
while(!ops.isEmpty() && ops.peek() != '(' && precedence(ops.peek()) >= precedence(s.charAt(i)) ){
applyOp(nums, ops);
}
ops.push(s.charAt(i));
i++;
}
else{
i++;
}
}
while (!ops.isEmpty()) {
applyOp(nums, ops);
}
return nums.pop();
}

private void applyOp(LinkedList<Integer> nums, LinkedList<Character> ops){
int num2 = nums.pop();
int num1 = nums.pop();
char op = ops.pop();
switch (op) {
case '+':
nums.push(num1 + num2);
break;
case '-':
nums.push(num1 - num2);
break;
case '*':
nums.push(num1*num2);
break;
default:
break;
}
}

private int precedence(char c){
if(c == '*' || c == '/')
return 2;
if(c == '+' || c == '-')
return 1;
return 0;
}

public static void main(String[] args) {
Solution s = new Solution();
System.out.println( s.solve("(2*(3-4))*5"));
}
}

表达式计算
http://blog.ulna520.com/2025/10/13/表达式计算_20251013_104406/
Veröffentlicht am
October 13, 2025
Urheberrechtshinweis