luogu-P1597 语句解析-系列题目3

luogu-P1597 语句解析-系列题目3

前文回顾

已完成的工作:

截至目前,我们在完成P1597题目本身要求的基础上,又拓展支持了以下情况:

  • 变量仍然只有3个abc
  • 变量值可以是多位整数(int范围内)或者变量名

上次留的作业是:

Tip

  • 支持更多的变量名,且变量名的长度可以是多位。
  • 进一步利用四级中的函数和模块化思想,并利用值传递、引用传递的特性,重构简化函数。

针对上述问题,实现代码应如何调整?今天分享下我和孩子的“答卷”。


原题回顾(luogu-P1597 语句解析)

题目要求

题目描述

一串长度不超过 255255 的 PASCAL 语言代码,只有 a,b,ca,b,c 三个变量,而且只有赋值语句,赋值只能是一个一位的数字或一个变量,每条赋值语句的格式是 [变量]:=[变量或一位整数];。未赋值的变量值为 00 输出 a,b,ca,b,c 的值。

输入格式

一串符合语法的 PASCAL 语言,只有 a,b,ca,b,c 三个变量,而且只有赋值语句,赋值只能是一个一位的数字或一个变量,未赋值的变量值为 00

输出格式

输出 a,b,ca,b,c 最终的值。

输入输出样例 #1

输入 #1

a:=3;b:=4;c:=5;

输出 #1

3 4 5

说明/提示

输入的 PASCAL 语言长度不超过 255255


本次题目分析

问题定位

针对本次问题定位,现有代码的主要局限在于:

// 初始化三个变量的值为0
int a = 0, b = 0, c = 0;
std::string v_name = "";

// 根据变量名赋值
if (v_name == "a") {
    a = value;
} else if (v_name == "b") {
    b = value;
} else {
    c = value;
}

之前我们使用了固定的三个变量,并且通过硬编码的方式进行赋值判断。如果要支持更多变量,这种方式显然不够灵活。

数据结构选择

当我们需要存储不确定数量的同类数据时,我们可以使用数组来存储变量名和对应的值。

// 定义变量名数组和对应的值数组,最多支持30个变量
std::string v_arys[30];
std::string v_values[30];

基于数组存储变量名和值的算法设计主要包含以下几个关键点:

  1. 数组设计
  • 使用两个平行数组分别存储变量名和对应的值
  • v_arys[]数组存储变量名(字符串类型)
  • v_values[]数组存储变量值(字符串类型)
  • 两个数组的下标一一对应,形成键值对关系
  1. 变量查找
  • 遍历数组查找变量名是否存在
  • 如果找到则返回对应的值
  • 如果未找到则返回默认值"0"
  1. 变量赋值
  • 先查找变量是否已存在
  • 如果存在则更新值
  • 如果不存在则在数组末尾添加新变量
  1. 变量值获取
  • 支持直接数字值
  • 支持变量引用(需要递归查找变量值)

这样的设计既保持了原有功能,又增加了灵活性。下面我们来看具体的各部门的核心代码


变量查找和赋值操作

我们不能再使用简单的if-else来判断变量名了,需要在数组中查找变量。这是一个常见操作,我们可以将其抽象为一个函数:

/**
 * 根据变量名查找对应的值
 * @param v_name 变量名
 * @return 变量值,如果未找到返回"0"
 */
std::string find_v_value(std::string v_name) {
    for (int i = 0; i < 30; i++) {
        if (v_arys[i] == v_name) {
            return v_values[i];
        }
    }
    return "0";
}

同样,我们也需要一个函数来设置变量值:

/**
 * 设置变量值
 * @param v_name 变量名
 * @param v_val 变量值
 * @param idx 当前变量索引
 */
void set_v_value(std::string v_name, std::string v_val, int& idx) {
    bool flag = true;
    // 查找变量是否已存在
    for (int i = 0; i < 30; i++) {
        if (v_arys[i] == v_name) {
            v_values[i] = v_val;
            flag = false;
            break;
        }
    }
    // 如果变量不存在,则新增
    if (flag) {
        v_arys[idx] = v_name;
        v_values[idx] = v_val;
        idx++;
    }
}

注意这里我们使用了引用参数int& idx,这样可以直接修改调用者的变量,便于处理主函数的逻辑。

改进变量值的获取逻辑

在之前的代码中,我们的get_value函数只能处理数字,现在我们需要扩展它,使其能够处理变量名:

/**
 * 从字符串中获取等号右边的数值
 * @param str 输入的PASCAL代码字符串
 * @param start 等号的位置
 * @return 等号右边的数值
 */
std::string get_value(std::string str, int& start) {
    // 从start位置开始查找分号的位置
    int end = (int)str.find(';', start);
    // 截取等号到分号之间的子串
    std::string r_val = str.substr(start, end - start);
    start = end;
    // 如果是数字直接返回,否则查找变量值
    if (isdigit(r_val[0])) {
        return r_val;
    } else {
        return find_v_value(r_val);
    }
}

这里我们做了两个改进:

  1. 返回类型从int改为std::string,以支持字符串形式的变量值
  2. 使用引用参数int& start,直接修改调用者的索引位置,避免主函数种重复计算。

抽象变量名的获取逻辑

既然变量名可能是多字符的,我们也需要一个函数来获取变量名:

/**
 * 获取变量名
 * @param str 输入字符串
 * @param start 开始位置
 * @return 变量名
 */
std::string get_v_name(std::string str, int& start) {
    // 查找冒号位置
    int end = (int)str.find(':', start);
    // 截取变量名
    std::string result = str.substr(start, end - start);
    start = end;
    return result;
}

添加输出函数

最后,我们需要一个函数来打印变量值,这个逻辑设计是为了让我们的输出可以通过P1597原题检查

/**
 * 打印变量值
 * @param v_name 变量名,为空时打印所有变量值
 */
void print_v_values(std::string v_name = "") {
    if (v_name.empty()) {
        // 打印所有变量值
        for (int i = 0; i < 30; i++) {
            std::cout << v_values[i] << " ";
        }
    } else {
        // 打印指定变量值
        for (int i = 0; i < 30; i++) {
            if (v_arys[i] == v_name) {
                std::cout << v_values[i] << " ";
            }
        }
    }
}

重构主程序逻辑

有了这些辅助函数,我们的主程序就可以大大简化,且框架逻辑非常清晰

int main() {
    // 读入PASCAL代码字符串
    std::string str;
    std::cin >> str;
    
    // 初始化索引
    int v_name_idx = 0;  // 变量名位置
    int val_idx = 0;     // 变量值位置
    int ary_idx = 0;     // 数组当前索引
    
    // 解析所有赋值语句
    while (v_name_idx < str.length() && val_idx < str.length()) {
        // 获取变量名
        // 通过引用传递v_name_idx,函数内部修改会直接反映到这里
        std::string v_name = get_v_name(str, v_name_idx);
        // 跳过":="
        val_idx = v_name_idx + 2;
        // 获取变量值
        // 通过引用传递val_idx,函数内部修改会直接反映到这里
        std::string v_val = get_value(str, val_idx);
        // 设置变量值
        // 通过引用传递ary_idx,函数内部对数组索引的修改会直接反映到这里
        set_v_value(v_name, v_val, ary_idx);
        // 移动到下一个语句
        v_name_idx = val_idx + 1;
    }
    
    // 输出结果
    print_v_values("a");
    print_v_values("b");
    print_v_values("c");
    return 0;
}

总结:重构的核心思想

通过这次重构,我们学到了几个重要的编程思想:

  1. 数据结构的选择:根据问题特点选择合适的数据结构,这里我们用数组替代了固定变量
  2. 函数模块化:将常用操作抽象为函数,提高代码可读性和可维护性
  3. 引用传递:通过引用参数优化性能,避免不必要的数据复制

这种重构不仅使代码更加灵活,能够支持更多变量和多字符变量名,还提高了代码的可维护性和可扩展性。


示例代码

#include <cctype>
#include <iostream>
#include <string>

// 定义变量名数组和对应的值数组,最多支持30个变量
std::string v_arys[30];
std::string v_values[30];

/**
 * 根据变量名查找对应的值
 * @param v_name 变量名
 * @return 变量值,如果未找到返回"0"
 */
std::string find_v_value(std::string v_name) {
    for (int i = 0; i < 30; i++) {
        if (v_arys[i] == v_name) {
            return v_values[i];
        }
    }
    return "0";
}

/**
 * 从字符串中获取等号右边的数值
 * @param str 输入的PASCAL代码字符串
 * @param start 等号的位置
 * @return 等号右边的数值
 */
std::string get_value(std::string str, int& start) {
    // 从start位置开始查找分号的位置
    int end = (int)str.find(';', start);
    // 截取等号到分号之间的子串并转换为整数返回
    std::string r_val = str.substr(start, end - start);
    start = end;
    // 如果是数字直接返回,否则查找变量值
    if (isdigit(r_val[0])) {
        return r_val;
    } else {
        return find_v_value(r_val);
    }
}

/**
 * 获取变量名
 * @param str 输入字符串
 * @param start 开始位置
 * @return 变量名
 */
std::string get_v_name(std::string str, int& start) {
    // 查找冒号位置
    int end = (int)str.find(':', start);
    // 截取变量名
    std::string result = str.substr(start, end - start);
    start = end;
    return result;
}

/**
 * 设置变量值
 * @param v_name 变量名
 * @param v_val 变量值
 * @param idx 当前变量索引
 */
void set_v_value(std::string v_name, std::string v_val, int& idx) {
    bool flag = true;
    // 查找变量是否已存在
    for (int i = 0; i < 30; i++) {
        if (v_arys[i] == v_name) {
            v_values[i] = v_val;
            flag = false;
            break;
        }
    }
    // 如果变量不存在,则新增
    if (flag) {
        v_arys[idx] = v_name;
        v_values[idx] = v_val;
        idx++;
    }
}

/**
 * 打印变量值
 * @param v_name 变量名,为空时打印所有变量值
 */
void print_v_values(std::string v_name = "") {
    if (v_name.empty()) {
        // 打印所有变量值
        for (int i = 0; i < 30; i++) {
            std::cout << v_values[i] << " ";
        }
    } else {
        // 打印指定变量值
        for (int i = 0; i < 30; i++) {
            if (v_arys[i] == v_name) {
                std::cout << v_values[i] << " ";
            }
        }
    }
}

int main() {
    // 读入PASCAL代码字符串
    std::string str;
    std::cin >> str;
    
    // 初始化索引
    int v_name_idx = 0;  // 变量名位置
    int val_idx = 0;     // 变量值位置
    int ary_idx = 0;     // 数组当前索引
    
    // 解析所有赋值语句
    while (v_name_idx < str.length() && val_idx < str.length()) {
        // 获取变量名
        std::string v_name = get_v_name(str, v_name_idx);
        // 跳过":="
        val_idx = v_name_idx + 2;
        // 获取变量值
        std::string v_val = get_value(str, val_idx);
        // 设置变量值
        set_v_value(v_name, v_val, ary_idx);
        // 移动到下一个语句
        v_name_idx = val_idx + 1;
    }
    
    // 按要求顺序输出a、b、c的值。
    // 这里这么输出,是为了满足P1597的要求。
    // 通过print_v_value();可以打印所有的变量值。
    print_v_values("a");
    print_v_values("b");
    print_v_values("c");
    return 0;
}

后续拓展

现在,我们在原题解的基础上额外支持了:

Note

  • 变量值可以是 多位整数(int范围内) 或者变量名
  • 支持多个、多字符变量名,如a1b2c3等,不再局限于abc三个变量
  • 使用函数抽象了一些通用逻辑

但可能优秀的同学已经有疑问了,我们为什么要用的两个数组,模拟键值对的行为,C++中不是有相关的数据结果么:

  • std::map:C++标准库中的关联容器,用于存储键值对。
  • std::unordered_map:C++标准库中的关联容器,用于存储键值对,基于哈希表实现,查找速度快。

因此,下一步,我们计划:

Tip

利用std::mapstd::unordered_map,来改进我们的实现代码,让孩子真正熟悉、掌握该数据结构。

有兴趣的同学可以尝试,后续我会继续更新我和孩子的学习成果。


本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

luogu-”系列题目可在 洛谷题库 在线评测。

bcqm-”系列题目可在 编程启蒙题库 在线评测。

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
最后更新于