2024-06-11    2024-08-16     3118 字  7 分钟
DSA

Case Study

来源:Lambert, K.A., 2018. Fundamentals Of Python: Data Structures, 2nd ed. Cengage Learning.

目标

该程序允许用户输入一个任意的后缀表达式,并显示表达式的值或错误消息(如果表达式无效)。其核心是展示栈的应用。

基本要求

用户应该能够测试各种后缀表达式,同时保留结果的记录。表达式中的错误不应停止程序,而应生成消息,以便让用户了解和评估过程中的错误位置。基于这些要求,设计如下用户界面:

请输入后缀表达式: 6 2 5 + *

您输入的后缀表达式为: 6 2 5 + *
其结果为: 42
________________________________________


请输入后缀表达式: 10 2 300 *+ 20/

您输入的后缀表达式为: 10 2 300 * + 20 /
其结果为: 30
________________________________________


请输入后缀表达式: 3 + 4

您输入的后缀表达式为: 3 + 4
错误: 栈上的操作数太少

处理的表达式部分: 3 +
栈上的操作数: 3
________________________________________


请输入后缀表达式: 5 6 %

您输入的后缀表达式为: 5 6
错误: 栈上的操作数太多

处理的表达式部分: 5 6
栈上的操作数: 5 6
________________________________________


请输入后缀表达式:

用户在终端中运行该程序时,会显示 请输入后缀表达式: 这样的提示,让用户在冒号 : 后输入其需要计算结果的后缀表达式。用户输入的后缀表达式应该被限制在该冒号后的一行文本中,表达式的每个 token 之间可以有任意的空格,相邻的操作数之间应该有一些空白。

当用户按下键盘上的 Enter 或 Return 键后,由用户输入的后缀表达式将被重新显示,例如: 您输入的后缀表达式为:6 2 5 + *。注意,由程序显示的后缀表达式应包含提示文本,如此处的 您输入的后缀表达式为:。并且,显示的后缀表达式中的各个 token 间应该恰好有一个空格。

如果用户输入了正确格式的后缀表达式,则在重新显示的后缀表达式的后一行显示结果,否则,应该显示相应的错误提示。然后显示另一个表达式的提示。

程序应检测并报告“所有”输入错误,无论是故意的还是无意的。一下是一些常见错误包括:

  • 表达式包含太多操作数;即,当遇到表达式结束时,栈上有多个操作数。
  • 表达式包含太少操作数;即,当遇到操作符时,栈上操作数少于两个。
  • 表达式包含无法识别的 token。程序期望表达式由整数、四个算术操作符(+、-、*、/)和空格(空格或制表符)组成。其他任何内容都是无法识别的。
  • 表达式包含除以 0 的情况。

下面是一些示例,展示了以上错误类型及其相应的错误消息:

请输入后缀表达式:
错误: 您没有输入任何后缀表达式,请输入有效的后缀表达式。
________________________________________


请输入后缀表达式: 1 2 3 +

您输入的后缀表达式为: 1 2 3 +
错误: 栈上的操作数太多

处理的表达式部分: 1 2 3 +
栈上的操作数: 1 5
________________________________________


请输入后缀表达式: 1 + 2 3 4 *

您输入的后缀表达式为: 1 + 2 3 4 *
错误: 栈上的操作数太少

处理的表达式部分: 1 +
栈上的操作数: 1
________________________________________


请输入后缀表达式: 1 2 % 3 +

您输入的后缀表达式为: 1 2 3 +
错误: 栈上的操作数太多

处理的表达式部分: 1 2 3 +
栈上的操作数: 1 5
________________________________________


请输入后缀表达式: 1 2 0 / +

您输入的后缀表达式为: 1 2 0 / +
错误: 除以零

处理的表达式部分: 1 2 0 /
栈上的操作数: 1
________________________________________

程序设计

为了构建一个能够处理后缀表达式并输出结果或错误信息的程序,我们需要将整体任务分解为多个具体的部分,并将这些部分封装到适当的类中,以便于管理和维护。

首先,程序需要处理用户的输入,这意味着它必须能够从用户获取后缀表达式。为了实现这一点,我们需要一个用户界面类(PFEvaluatorView)。这个类的 run 方法将负责与用户交互,提示用户输入后缀表达式,并将用户输入的字符串传递给后续的处理模块。

接下来,输入的后缀表达式字符串需要被解析为独立的 token。为了实现这一点,我们需要一个扫描器类(Scanner)。该类在初始化时接收输入字符串(a_string),并将其分割成一个个独立的 Token 对象。这些 Token 对象表示表达式中的每个元素(如操作数和操作符)。Scanner 类还应提供方法遍历这些 token,以便后续的处理模块可以逐个获取和处理它们。

在解析 token 之后,我们需要一个类来表示和管理这些 token。Token 类的设计目标就是封装表达式中的每个基本元素。每个 Token 对象都有类型和值两个属性。类型属性表示 token 是操作数还是操作符,如果是操作符,则进一步区分是哪种操作符;值属性则存储操作数的具体数值或操作符的字符表示。此外,Token 类还提供了另外一些方法,如判断 token 是否为操作符、获取 token 类型和值等。

解析和表示 token 后,程序需要对输入的表达式进行格式化,使其显示更加规范。这可以有一个新类(PFEvaluatorModel)来处理的。该类的 format 方法接收一个表达式字符串(a_string),并使用 Scanner 类将其解析为 token 序列,然后将这些 token 格式化为标准形式,并返回格式化后的字符串。

表达式格式化之后,接下来是核心的求值过程。这可以由 PFEvaluator 类来完成。该类使用栈结构来求值后缀表达式。其 evaluate 方法通过遍历由 Scanner 类提供的 token 序列,根据操作符的类型执行相应的计算。具体地说,当遇到操作数时,将其压入栈中;当遇到操作符时,从栈中弹出相应数量的操作数,执行计算,并将结果重新压入栈中。在此过程中,该类还负责处理各种可能的错误情况,如操作数不足、未知 token 类型、除以 0 等。

为了捕获和处理求值过程中的错误,并向用户提供有用的错误信息,我们需要在 PFEvaluatorModel 类中实现相关功能。该类的 evaluation_status 方法负责返回评估器的状态信息,包括处理的表达式部分和栈内容。如果在求值过程中发生错误,PFEvaluator 类会抛出异常,PFEvaluatorModel 类捕获这些异常并生成相应的错误信息。

最后,求值结果或错误信息需要显示给用户。这由 PFEvaluatorView 类来完成。在用户输入表达式后,该类调用 PFEvaluatorModelformat 方法进行格式化,并调用 evaluate 方法进行求值,然后将结果或错误信息显示给用户。如果发生错误,PFEvaluatorView 类还会调用 PFEvaluatorModelevaluation_status 方法来获取并显示详细的错误信息。

下面的时序图展示了程序各部分之间的动态交互过程,特别是用户输入表达式后如何通过各个类的协作来解析、格式化、评估表达式并最终显示结果或错误信息。

sequenceDiagram
    participant 用户
    participant PFEvaluatorView
    participant PFEvaluatorModel
    participant PFEvaluator
    participant Scanner
    participant Token
    participant Stack
    participant String

    用户->>PFEvaluatorView: 输入后缀表达式 (a_string)
    PFEvaluatorView->>PFEvaluatorModel: format(a_string)
    PFEvaluatorModel->>Scanner: 初始化 Scanner(a_string)
    Scanner->>Token: 创建 token
    Token-->>Scanner: 返回 token
    Scanner-->>PFEvaluatorModel: 返回 token序列
    PFEvaluatorModel->>PFEvaluatorView: 返回格式化字符串

    用户->>PFEvaluatorView: 输入后缀表达式 (a_string)
    PFEvaluatorView->>PFEvaluatorModel: evaluate(a_string)
    PFEvaluatorModel->>PFEvaluator: 初始化 PFEvaluator(Scanner)
    PFEvaluator->>Scanner: hasNext(), next()
    Scanner->>Token: 获取下一个 token
    Token-->>Scanner: 返回 token
    Scanner-->>PFEvaluator: 返回 token
    PFEvaluator->>Stack: 存储操作数
    PFEvaluator->>Token: 获取 token 类型和值
    Token-->>PFEvaluator: 返回类型和值
    PFEvaluator->>PFEvaluatorModel: 返回结果或错误信息

    PFEvaluatorModel->>PFEvaluatorView: 返回结果或错误信息
    PFEvaluatorView->>用户: 显示结果或错误信息
  

以下类图展示了程序中各个类及其属性、方法,以及类之间的关系。

classDiagram
    direction LR
    class PFEvaluatorView {
        +format(a_string: String)
        +evaluate(a_string: String)
        +evaluationStatus(): String
    }

    class PFEvaluatorModel {
        +format(a_string: String)
        +evaluate(a_string: String)
        +evaluationStatus(): String
    }

    class PFEvaluator {
        +hasNext()
        +next()
        +evaluate()
        +evaluationStatus(): String
    }

    class Scanner {
        +hasNext(): bool
        +next(): Token
        +tokenize(a_string: String): List~Token~
    }

    class Token {
        +getType()
        +getValue()
    }

    PFEvaluatorView --> PFEvaluatorModel : Uses
    PFEvaluatorModel --> PFEvaluator : Uses
    PFEvaluator --> Scanner : Uses
    Scanner --> Token : Creates
    PFEvaluator --> Stack : Stores operands on
  

程序实现

以上类及主程序的实现请查看惟真学堂

Exercise 1

编写一个程序,将中缀表达式转换为后缀表达式。这个程序应该使用案例中的 Token 类和 Scanner 类。此外,该程序应包含一个执行输入和输出的 main 函数和一个名为 IFToPFConverter 的类。主函数接收一个输入字符串并用它创建一个扫描器(Scanner)。然后扫描器被作为参数传递给转换器对象(converter object)的构造函数 (convert 方法)。接着运行转换器对象的 convert 方法,使用本章描述的算法将中缀表达式转换为后缀表达式。这个方法返回一个表示后缀字符串的 token 列表。然后主函数显示这个字符串。你还应在 Token 类中定义一个新方法 get_precedence,该方法返回一个整数,表示运算符的优先级。(注意:在这个项目中,假设用户总是输入语法正确的中缀表达式。)

Exercise 2

Directions Reduction