文章

【GESP】C++三级、四级练习 luogu-P1597 语句解析-系列题目3

前文回顾

已完成的工作:

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

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

上次留的作业是:

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

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


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

题目要求

题目描述

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

输入格式

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

输出格式

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

输入输出样例 #1

输入 #1

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

输出 #1

1
3 4 5

说明/提示

输入的 PASCAL 语言长度不超过 $255$。


本次题目分析

问题定位

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

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化三个变量的值为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;
}

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

数据结构选择

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

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

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

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

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


变量查找和赋值操作

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 根据变量名查找对应的值
 * @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";
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 设置变量值
 * @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函数只能处理数字,现在我们需要扩展它,使其能够处理变量名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 从字符串中获取等号右边的数值
 * @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,直接修改调用者的索引位置,避免主函数种重复计算。

抽象变量名的获取逻辑

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 获取变量名
 * @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原题检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 打印变量值
 * @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] << " ";
            }
        }
    }
}

重构主程序逻辑

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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. 引用传递:通过引用参数优化性能,避免不必要的数据复制

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


示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#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;
}

后续拓展

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

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

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

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

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

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

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


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

GESP 学习专题站:GESP WIKI

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

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

欢迎加入Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答

欢迎加入C++ GESP/CSP认证学习QQ频道,考试资源总结汇总

欢迎加入C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
本文由作者按照 CC BY 4.0 进行授权