中缀表达式值(expr)

GitHub跳转原题关系图返回列表

用记号类别状态机校验中缀表达式,再经调度场算法转后缀、用操作数栈求值;任一环节非法输出 NO。

OJ: roj

题目 ID: 1358

难度:普及-

标签:栈表达式求值字符串python

创建: 2026-09-30 06:50

更新: 2026-10-07 13:50

题目描述

输入一个以 @ 结束的中缀表达式(运算数为 0-9 组成的整数,可带一元负号 -;运算符有 + - * /;含小括号),判断它是否合法:不合法输出 NO,合法则转成后缀形式并用栈求出结果(整数除法向零取整)。

输入格式:一行以 @ 结束的字符串。输出格式:合法时输出一行整数,否则输出一行 NO。样例输入 1+2*8-9@,样例输出 8。

思路

用「当前位置该出现运算数还是运算符」一个状态校验合法性:运算数位只接受数字、( 和一元负号(-4、2*-3 合法),运算符位只接受二元运算符和 ),配合 depth 判括号配对,非法即输出 NO。合法时用调度场算法把中缀记号按优先级转成后缀,再用一个操作数栈从左到右归约求值,三步各扫描一遍,复杂度 O(n)O(n)。

参考代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 06:55
# update_at: 2026-09-30 06:55

import sys
from operator import add, mul, sub

LEV = {'+': 1, '-': 1, '*': 2, '/': 2}  # 二元运算符优先级,调度场算法弹栈用
APPLY = {'+': add, '-': sub, '*': mul}  # 除法单独走分支:要向零取整并判除数为 0


def divide(a: int, b: int) -> int:
    """整数除法向零取整,与 C++ 的 int / int 一致(Python 的 // 是向下取整)。"""
    quotient = abs(a) // abs(b)
    return -quotient if (a < 0) != (b < 0) else quotient


def tokenize(expr: str) -> list[str] | None:
    """把中缀表达式切成记号并校验合法性;非法时返回 None。

    校验只靠“当前位置该出现哪类记号”一个状态:操作数位只接受数字、左括号和
    一元负号 '-'(直接并入后面的数字),运算符位只接受二元运算符和右括号。
    连续两个运算符、空括号、头尾运算符、括号不配对,都会落到不该出现的位置上。
    """
    tokens: list[str] = []
    expect_operand = True  # 下一个记号应当是操作数
    depth = 0              # 尚未闭合的左括号个数
    i = 0
    while i < len(expr):
        ch = expr[i]
        if ch.isdigit() or (ch == '-' and expect_operand):
            negative = ch == '-'  # 操作数位上的 '-' 是负数标志
            if negative:
                i += 1
                if i >= len(expr) or not expr[i].isdigit():
                    return None   # 一元负号后面必须紧跟数字
            j = i
            while j < len(expr) and expr[j].isdigit():
                j += 1
            tokens.append(('-' if negative else '') + expr[i:j])
            i = j
            expect_operand = False
        elif ch in '()':
            if ch == '(':
                if not expect_operand:
                    return None    # 左括号只能出现在操作数位,如 5( 这种要拒
                depth += 1
            else:
                if expect_operand or depth == 0:
                    return None    # 空括号 (),或没有配对的右括号
                depth -= 1
            tokens.append(ch)
            expect_operand = ch == '('  # 左括号后要操作数,右括号后要运算符
            i += 1
        elif not expect_operand and ch in LEV:
            tokens.append(ch)
            expect_operand = True
            i += 1
        else:
            return None  # 运算符出现在操作数位(如 +* 连排),或非法字符
    if expect_operand or depth:
        return None  # 空表达式 / 以运算符结尾 / 左括号没闭合
    return tokens


def to_postfix(tokens: list[str]) -> list[str]:
    """调度场算法:运算符按优先级弹栈,把中缀记号改写成后缀序列。"""
    out: list[str] = []
    ops: list[str] = []
    for tok in tokens:
        if tok in LEV:
            # 栈顶优先级不低于当前运算符的先算,保证同级从左到右结合
            while ops and ops[-1] != '(' and LEV[ops[-1]] >= LEV[tok]:
                out.append(ops.pop())
            ops.append(tok)
        elif tok == '(':
            ops.append(tok)
        elif tok == ')':
            while ops[-1] != '(':
                out.append(ops.pop())
            ops.pop()  # 丢掉左括号
        else:
            out.append(tok)  # 操作数直接进输出
    while ops:
        out.append(ops.pop())
    return out


def eval_postfix(postfix: list[str]) -> int | None:
    """用操作数栈求后缀表达式的值;除数为 0 视为非法,返回 None。"""
    stack: list[int] = []
    for tok in postfix:
        if tok in LEV:
            rhs, lhs = stack.pop(), stack.pop()
            if tok == '/':
                if rhs == 0:
                    return None
                stack.append(divide(lhs, rhs))
            else:
                stack.append(APPLY[tok](lhs, rhs))
        else:
            stack.append(int(tok))
    return stack.pop()


def solve() -> None:
    # '@' 是结束符;题面样例里省略了它,取第一个 '@' 之前的部分即可兼容两种写法
    data = iter(sys.stdin.buffer.read().split(b'@'))
    expr = next(data).decode().strip()
    tokens = tokenize(expr)
    if tokens is None:
        print('NO')
        return
    value = eval_postfix(to_postfix(tokens))
    print('NO' if value is None else value)


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 11:53
 * update_at: 2026-10-05 11:53
 */
// main.cpp:中缀表达式求值。先切词校验合法性,再用调度场算法转成后缀,
// 最后用操作数栈从左到右归约求值;任一阶段失败就输出 NO。

#include <cstdio>
#include <cstring>

typedef long long ll;

const int MAXN = 512; // 表达式长度很小,512 足够容纳整行

char raw[MAXN];  // 读入的原始内容(可能带首尾空白)
char expr[MAXN]; // 去掉首尾空白、截到 '@' 之前的表达式
int len;         // expr 的字符个数

// 记号数组:kind 为 0 表示运算数(值在 tok_num),为 1 表示运算符或括号(字符在 tok_op)
int tok_kind[MAXN];
ll tok_num[MAXN];
char tok_op[MAXN];
int tok_cnt;

// 后缀序列,含义同记号数组
int post_kind[MAXN];
ll post_num[MAXN];
char post_op[MAXN];
int post_cnt;

char op_stack[MAXN]; // 调度场算法:暂存还没到计算时机的中缀运算符
int op_top;

ll num_stack[MAXN]; // 求值用的操作数栈
int num_top;

int is_digit(char ch) { return '0' <= ch && ch <= '9'; }

int is_space(char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; }

// 二元运算符的优先级:乘除高于加减。
int level(char op) {
    if (op == '*' || op == '/') return 2;
    return 1;
}

void add_number(ll value) {
    tok_kind[tok_cnt] = 0;
    tok_num[tok_cnt] = value;
    tok_cnt++;
}

void add_op(char op) {
    tok_kind[tok_cnt] = 1;
    tok_op[tok_cnt] = op;
    tok_cnt++;
}

// 读入整行:'@' 是结束符,取它之前的部分作为表达式。
void read_input() {
    int ch;
    int n = 0;
    while ((ch = getchar()) != EOF && ch != '@' && n < MAXN - 1) {
        raw[n] = (char)ch;
        n++;
    }
    raw[n] = '\0';

    // 去掉首尾空白,内部空白留给 tokenize 判非法
    int left = 0;
    int right = n - 1;
    while (left <= right && is_space(raw[left])) left++;
    while (right >= left && is_space(raw[right])) right--;
    int k = 0;
    for (int i = left; i <= right; i++) {
        expr[k] = raw[i];
        k++;
    }
    expr[k] = '\0';
}

// 切词并校验合法性:运算数位只接受数字、'(' 和一元负号,运算符位只接受
// 二元运算符和 ')'。合法返回 1,非法返回 0。
int tokenize() {
    tok_cnt = 0;
    len = strlen(expr);
    int expect_operand = 1; // 下一个记号是否应当出现在运算数位置
    int depth = 0;          // 还没闭合的左括号个数
    int i = 0;
    while (i < len) {
        char ch = expr[i];
        if (is_digit(ch)) {
            if (!expect_operand) return 0; // 数字只能出现在运算数位置,如 (5)3
            ll value = 0;
            while (i < len && is_digit(expr[i])) {
                value = value * 10 + (expr[i] - '0');
                i++;
            }
            add_number(value);
            expect_operand = 0;
        } else if (ch == '-' && expect_operand) {
            // 运算数位置上的 '-' 是一元负号,后面必须紧跟数字
            i++;
            if (i >= len || !is_digit(expr[i])) return 0;
            ll value = 0;
            while (i < len && is_digit(expr[i])) {
                value = value * 10 + (expr[i] - '0');
                i++;
            }
            add_number(-value);
            expect_operand = 0;
        } else if (ch == '(') {
            if (!expect_operand) return 0; // 5( 这种缺少运算符
            add_op('(');
            depth++;
            expect_operand = 1;
            i++;
        } else if (ch == ')') {
            if (expect_operand || depth == 0) return 0; // 空括号或多余的右括号
            add_op(')');
            depth--;
            expect_operand = 0;
            i++;
        } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
            if (expect_operand) return 0; // 运算符出现在运算数位置,如 +*
            add_op(ch);
            expect_operand = 1;
            i++;
        } else {
            return 0; // 其它字符(含内部空白)一律非法
        }
    }
    if (expect_operand || depth != 0) return 0; // 空串、以运算符结尾、括号没配对
    return 1;
}

// 调度场算法:按优先级把中缀记号重排成后缀序列。
void to_postfix() {
    post_cnt = 0;
    op_top = 0;
    for (int i = 0; i < tok_cnt; i++) {
        if (tok_kind[i] == 0) {
            post_kind[post_cnt] = 0;
            post_num[post_cnt] = tok_num[i];
            post_cnt++;
        } else if (tok_op[i] == '(') {
            op_stack[op_top] = '(';
            op_top++;
        } else if (tok_op[i] == ')') {
            while (op_stack[op_top - 1] != '(') {
                post_kind[post_cnt] = 1;
                post_op[post_cnt] = op_stack[op_top - 1];
                post_cnt++;
                op_top--;
            }
            op_top--; // 弹出并丢掉左括号
        } else {
            // 栈顶优先级不低于当前运算符的先出栈,保证同级从左到右结合
            while (op_top > 0 && op_stack[op_top - 1] != '(' &&
                   level(op_stack[op_top - 1]) >= level(tok_op[i])) {
                post_kind[post_cnt] = 1;
                post_op[post_cnt] = op_stack[op_top - 1];
                post_cnt++;
                op_top--;
            }
            op_stack[op_top] = tok_op[i];
            op_top++;
        }
    }
    while (op_top > 0) {
        post_kind[post_cnt] = 1;
        post_op[post_cnt] = op_stack[op_top - 1];
        post_cnt++;
        op_top--;
    }
}

// 用操作数栈求后缀表达式的值;除以 0 视为非法,返回 0 表示失败。
int eval_postfix(ll &answer) {
    num_top = 0;
    for (int i = 0; i < post_cnt; i++) {
        if (post_kind[i] == 0) {
            num_stack[num_top] = post_num[i];
            num_top++;
        } else {
            ll right = num_stack[num_top - 1]; // 后入栈的是右操作数
            num_top--;
            ll left = num_stack[num_top - 1];
            num_top--;
            char op = post_op[i];
            ll value;
            if (op == '+') {
                value = left + right;
            } else if (op == '-') {
                value = left - right;
            } else if (op == '*') {
                value = left * right;
            } else {
                if (right == 0) return 0;
                value = left / right; // C++ 整数除法向零取整
            }
            num_stack[num_top] = value;
            num_top++;
        }
    }
    answer = num_stack[0];
    return 1;
}

int main() {
    read_input();

    if (!tokenize()) {
        printf("NO\n");
        return 0;
    }
    to_postfix();

    ll answer = 0;
    if (!eval_postfix(answer)) {
        printf("NO\n");
        return 0;
    }
    printf("%lld\n", answer);
    return 0;
}