luogu-P1597 语句解析-系列题目3
前文回顾
已完成的工作:
截至目前,我们在完成P1597题目本身要求的基础上,又拓展支持了以下情况:
- 变量仍然只有3个
a
、b
、c
- 变量值可以是多位整数(int范围内)或者变量名
上次留的作业是:
Tip
- 支持更多的变量名,且变量名的长度可以是多位。
- 进一步利用四级中的函数和模块化思想,并利用值传递、引用传递的特性,重构简化函数。
针对上述问题,实现代码应如何调整?今天分享下我和孩子的“答卷”。
原题回顾(luogu-P1597 语句解析)
题目要求
题目描述
一串长度不超过 的 PASCAL 语言代码,只有 三个变量,而且只有赋值语句,赋值只能是一个一位的数字或一个变量,每条赋值语句的格式是
[变量]:=[变量或一位整数];
。未赋值的变量值为 输出 的值。
输入格式
一串符合语法的 PASCAL 语言,只有 三个变量,而且只有赋值语句,赋值只能是一个一位的数字或一个变量,未赋值的变量值为 。
输出格式
输出 最终的值。
输入输出样例 #1
输入 #1
a:=3;b:=4;c:=5;
输出 #1
3 4 5
说明/提示
输入的 PASCAL 语言长度不超过 。
本次题目分析
问题定位
针对本次问题定位,现有代码的主要局限在于:
// 初始化三个变量的值为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];
基于数组存储变量名和值的算法设计主要包含以下几个关键点:
- 数组设计
- 使用两个平行数组分别存储变量名和对应的值
v_arys[]
数组存储变量名(字符串类型)v_values[]
数组存储变量值(字符串类型)- 两个数组的下标一一对应,形成键值对关系
- 变量查找
- 遍历数组查找变量名是否存在
- 如果找到则返回对应的值
- 如果未找到则返回默认值"0"
- 变量赋值
- 先查找变量是否已存在
- 如果存在则更新值
- 如果不存在则在数组末尾添加新变量
- 变量值获取
- 支持直接数字值
- 支持变量引用(需要递归查找变量值)
这样的设计既保持了原有功能,又增加了灵活性。下面我们来看具体的各部门的核心代码
变量查找和赋值操作
我们不能再使用简单的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);
}
}
这里我们做了两个改进:
- 返回类型从
int
改为std::string
,以支持字符串形式的变量值 - 使用引用参数
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;
}
总结:重构的核心思想
通过这次重构,我们学到了几个重要的编程思想:
- 数据结构的选择:根据问题特点选择合适的数据结构,这里我们用数组替代了固定变量
- 函数模块化:将常用操作抽象为函数,提高代码可读性和可维护性
- 引用传递:通过引用参数优化性能,避免不必要的数据复制
这种重构不仅使代码更加灵活,能够支持更多变量和多字符变量名,还提高了代码的可维护性和可扩展性。
示例代码
#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范围内) 或者变量名- 支持多个、多字符变量名,如
a1
、b2
、c3
等,不再局限于a
、b
、c
三个变量 - 使用函数抽象了一些通用逻辑
但可能优秀的同学已经有疑问了,我们为什么要用的两个数组,模拟键值对
的行为,C++中不是有相关的数据结果么:
std::map
:C++标准库中的关联容器,用于存储键值对。std::unordered_map
:C++标准库中的关联容器,用于存储键值对,基于哈希表实现,查找速度快。
因此,下一步,我们计划:
Tip
利用std::map
和std::unordered_map
,来改进我们的实现代码,让孩子真正熟悉、掌握该数据结构。
有兴趣的同学可以尝试,后续我会继续更新我和孩子的学习成果。
本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
“luogu-”系列题目可在 洛谷题库 在线评测。
“bcqm-”系列题目可在 编程启蒙题库 在线评测。
