LeetCode 036:逆波兰表达式求值,策略模式求解

文章内容

https://leetcode.cn/problems/8Zf90G/description/

一、题目解析

1.1 题意

逆波兰表达式(Reverse Polish Notation),说白了就是把运算符写在两个操作数后面。比如中缀 (2+1)*3 写成后缀就是 ["2","1","+","3","*"]——2 1 + 表示 2+1。
逆波兰的好处是没有括号,也不用考虑优先级——顺序本身就编码了计算次序。求值规则只有两条:

  • 遇到数字,压入栈。
  • 遇到运算符,从栈里弹出两个数,算完把结果压回去。
    整个数组扫完,栈里剩下的唯一一个数就是答案。

1.2 示例推演

拿 ["2","1","+","3","*"] 走一遍,对应中缀的 (2+1)*3,答案是 9。

plain text
token "2"   数字入栈                     栈:[2]token "1"   数字入栈                     栈:[2, 1]token "+"   弹出 1、2,算 2+1=3,压回     栈:[3]token "3"   数字入栈                     栈:[3, 3]token "*"   弹出 3、3,算 3*3=9,压回     栈:[9]

扫完了,栈顶的 9 就是结果。

这里有个细节值得停一下:弹出的第一个数是 1,第二个数是 2,但算的是 2+1 而不是 1+2。

逆波兰表达式求值流程图解

二、栈解法模板

java
// 通用骨架:扫一遍 token,数字入栈,运算符弹两个算完再压回Deque<Integer> stack = new ArrayDeque<>();for (String token : tokens) {    if (isOperator(token)) {        int right = stack.pop();   // 先弹出的是右操作数        int left = stack.pop();    // 后弹出的是左操作数        stack.push(calc(left, right, token));    } else {        stack.push(Integer.valueOf(token));    }}return stack.peek();

两个要点。

一是弹出顺序。栈是后进先出,左操作数比右操作数先入栈,所以出栈时反过来——先出来的是右边那个。

二是运算符怎么判断。最直觉的写法是 if (token.equals("+")) ... else if (token.equals("-")) ...,四个分支堆下来又长又乱,而且"判断它是什么"和"该怎么算"这两件事被拆在了两处。下面这份代码换了个思路。

三、代码详解

3.1 Action 接口

代码里定义了一个只有一个方法的私有接口:

java
// 约定:a 是左操作数,b 是右操作数private interface Action {    int doAction(int a, int b);}

然后用 HashMap<String, Action> 把符号和运算绑起来,每种运算用匿名内部类实现。本质就是把"怎么算"封装成一个个对象,用的时候按符号查表取出来——策略模式最朴素的落地方式。

3.2 主循环逻辑

java
for (String token : tokens) {    // 能在表里查到,说明是运算符    if (actions.containsKey(token)) {        int a = stack.pop();   // 先弹出,这是右操作数        int b = stack.pop();   // 后弹出,这是左操作数        stack.add(actions.get(token).doAction(b, a));  // 调换回 (左, 右)    } else {        stack.add(Integer.valueOf(token));  // 数字,直接入栈    }}return stack.peek();

doAction(b, a) 这一行是核心。变量名 a 是先 pop 出来的,按 1.2 的推演它是右操作数;b 后 pop,是左操作数。而 Action 接口的语义是 doAction(左, 右),所以传参必须写成 (b, a),把两者对调回来。

另外 stack.add(...) 和 stack.push(...) 在 Stack 上是等价的——Stack 继承自 Vector,add 追加到尾部,而 Stack 的栈顶就是尾部。

最后 return stack.peek(),因为表达式合法,扫完必然只剩一个元素。

3.3 完整代码

java
class Solution {    // 运算策略接口:约定 a 是左操作数,b 是右操作数    private interface Action {	    // 运算操作        int doAction(int a, int b);    }     public int evalRPN(String[] tokens) {        Stack<Integer> stack = new Stack<>();        // 符号 -> 运算实现的映射表,替代 if-else 链        Map<String, Action> actions = new HashMap<>();        actions.put("+", new Action() {            @Override            public int doAction(int a, int b) {                return a + b;            }        });        actions.put("-", new Action() {            @Override            public int doAction(int a, int b) {                return a - b;            }        });        actions.put("*", new Action() {            @Override            public int doAction(int a, int b) {                return a * b;            }        });        actions.put("/", new Action() {            @Override            public int doAction(int a, int b) {                // int 除法自动向零截断                return a / b;            }        });         for (String token : tokens) {            // 查表命中,说明当前 token 是运算符            if (actions.containsKey(token)) {                int a = stack.pop();  // 先弹出的是右操作数                int b = stack.pop();  // 后弹出的是左操作数                // 调换回 (左, 右) 的顺序再交给策略计算,结果压回栈                stack.add(actions.get(token).doAction(b, a));            } else {                // 否则是数字,转成 Integer 后入栈                stack.add(Integer.valueOf(token));            }        }        // 表达式合法,扫完栈里只剩最终结果        return stack.peek();    }}

四、复杂度分析

时间 O(n)。每个 token 处理一次,栈操作和 HashMap 查表都是 O(1)。数字解析的开销跟位数相关,但位数有上界,当常数看就行。构建 actions 表是固定四次 put,和 n 无关。

空间 O(n)。最坏情况是所有数字都排在表达式前头,栈一口气涨到 n。运算符每次消耗两个数、产出一个,中途栈会缩,但上界还是 O(n)。actions 表只有四个条目,常数空间,不占大头。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:题解

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

LeetCode 036:逆波兰表达式求值,策略模式求解 · Yinchen 的博客