这一期的内容已经过半,部分同学开始觉得吃力。如果这时候放弃,那前边的努力就白费了。今天我们来看看上一篇中的课后题。
1. 题目
给出一组代数表达式,请编程判断出他们的括号是否配对正确。
10
[a+b)-(d+e]
(a+b))+((d-e)
(a+b)-[c+d
[a+(b+c)]*(a+b]
(a-1+b)-(b+c)
x-[a*(b+c))]
a+(b+c+(d-(e+m))
a+b+[c-d*e-(a-b)-c]
[a+b-d]+e)
[a-(b+)+[d-a]-b]
第一行中的10表示共有10个表达式需要判断。下面的每一行有一个表达式。要求把这组数据原样保存在文件input.txt
中,通过读文件的方式读入数据完成判断。
原题只有5个表达式,这里我又加入了5个新的,有一些特殊的情况需要添加。
2. 分析
这道题从功能上讲需要做两件事:
- 第一件:把保存在
input.txt
文件中的数据读入程序中 - 第二件:扫描每行字符串,判断是否所有的括号都匹配
是不是看起来没那么复杂呢?下面我们就来看如何实现这两个功能。
3. 读数据
3.1 基本方法
说到读数据,我们本能地想到C语言教科书中文件操作的那一章,用基本的打开文件,读取数据的方法就能解决问题。这个方法完全正确。如果你对文件的基本操作还有问题,请参考我之前的文章21天C语言代码训练营(第十五天)。
这里分享一段Aha_斌提交作业中的代码,请参考。
#include
#include
#include
//用宏去存储文件路径
#define FILE_PATH "input.txt"
void main()
{
FILE *fp;
char str[50];//存储从文件读取的字符串
int strNum;
char ch;
if ((fp = fopen(FILE_PATH, "r")) == NULL)
{
printf("System can`t find this file!\n");
exit(0);
}
fscanf(fp, "%d", &strNum);//读取要比较表达式的个数
fscanf(fp, "%c", &ch);//读取第一行的换行符
while (strNum--)//当表达式没有比较完
{
fgets(str, 50, fp);//读取一个表达式
// 这里加入判断str是否符合要求的代码
}
system("PAUSE");
}
3.2 文件流重定向方法
这里我要介绍另外一种方法,这是ACM比赛中常用的一种数据读取方法。它的特点是把打开的文件重定向到标准输入流中,这样我们就可以像读取键盘输入数据一样读入数据。
先看一下这段代码:
int main()
{
int n, i;
int ret;
char str[MAX_SIZE];
FILE* fp;
freopen_s(&fp, "input.txt", "r", stdin);
if (fp == NULL)
{
ret = 0;
}
scanf("%d", &n);
printf("%d\n", n);
for (i = 0; i < n; i++)
{
scanf("%s", str);
printf("%s\n", str);
}
}
这段代码的功能是把input.txt
文件中的内容读入程序,之后打印在屏幕上。很容易发现,我们在打开文件并没有使用大家熟悉的fopen()
函数数,而是用了这句话:
freopen_s(&fp, "input.txt", "r+", stdin);
这句话的作用是把input.txt
文件用只读方式打开,之后重定向到stdin
表示的标准输入流中。当fp
为空时,表示此文件不存在。在这之后,我们可以用scanf()
函数像读取键盘输入那样读取文件内容了。是不是很神奇。
如不需要得到文件指针,我们还可以写成这样:
freopen("input.txt", "r+", stdin);
这个形式是标准库函数的写法。
我们来看看执行结果:
4. 判断括号匹配
4.1 常见方法
完成了作业的同学都采用了一种最容易想到的方法,那就是分别统计[]()
四个符号的个数,之后比较个数是否相同,如果相同就说明匹配,如果不相同就说明不匹配。
但是这种方法有很多bug,比如))((
这种问题。还有我新加入的几个公式都会判断错误。不过也不是完全不可能,只不过需要另外添加一些约束条件,多写一些代码。有兴趣的同学可以参考完成作业的代码。
4.2 逐个匹配法
我们这里介绍一种大家没有想到的算法。这个算法的思想是这样的,从前到后遍历表达式,忽略字母和运算符,只关注括号。一旦遇到匹配成功的括号就整对删掉,如果最后剩下半个括号(无论是左半边还是右半边),说明不匹配,否则就说明是匹配的。
如图,我们利用一个数组来完成它,具体分为三个动作:
- 遇到左括号写入数组
- 遇到右括号拿它和数组中最后一个括号做匹配
- 匹配成功两个括号都删除,继续后面的动作
- 匹配失败直接跳出,返回失败
我们看看代码如何实现:
#define _CRT_SECURE_NO_WARNINGS
#include
#define MAX_SIZE 100
char g_arr[MAX_SIZE];
int g_index;
int CheckChar(char ch)
{
if (ch == '(')
{
g_arr[g_index++] = ch;
}
else if (ch == '[')
{
g_arr[g_index++] = ch;
}
else if (ch == ')')
{
if (g_arr[g_index - 1] == '(')
{
g_index--;
}
else
{
return 0;
}
}
else if (ch == ']')
{
if (g_arr[g_index - 1] == '[')
{
g_index--;
}
else
{
return 0;
}
}
else
{
// Do nothing
}
return 1;
}
int Check(char* pStr)
{
int i;
g_index = 0;
for (i = 0; pStr[i] != '\0'; i++)
{
if (CheckChar(pStr[i]) == 0)
{
return 0;
}
}
if (g_index == 0)
{
return 1;
}
else
{
return 0;
}
}
5. 匹配算法分析
我们来仔细分析一下这段代码,它一共做了三件事。
5.1 定义数据结构
#define MAX_SIZE 100
int g_arr[MAX_SIZE];
int g_index;
数据结构非常简单,就是一个大小为MAX_SIZE
的一维字符数组。我们用g_index
这个变量保存这个数组的当前下标,也就是按顺序存入内容时最左边的那个空位。
这个数组的使用方法如图所示,每插入一个字符g_index
加1,标记为一个新的空白位置。删除时动作相反。
注意,这里使用全局变量只是为了方便大家理解,真正的项目中并不鼓励用全局变量的方式设计代码。
5.2 判断字符
int CheckChar(char ch)
这个函数主要负责使用g_arr
进行匹配动作。把表达式中的每个字符传递给参数ch
,它会负责判断这个符号是否符合要求。如果拿到的符号不是括号,直接忽略掉。如果是左括号就存入数组,如果是右括号就和数组中最后一个括号(g_arr[g_index - 1])做匹配。
这里重点要说的是匹配成功后,需要把数组中最后一个字符删掉。只用了一句话:
g_index--;
这句话用到了一个重要的思想,删除最后一个元素只需要更新当前下标,并不用真的把那一位改成0。我们在维护g_arr
这个数组时,其实看的是g_index
的值,我们定义了它永远标记的是第一个空着的可用位置。如果g_index
标记了一个有内容的位置,那么下一次写入时会通过g_arr[g_index++] = '(';
这样的语句把这块内容覆盖掉。所以,更新g_index
的方法和删除这块内容的效果是完全一样的。
5.3 处理字符串
int Check(char* pStr)
这个函数是为了方便我们循环处理每个表达式字符串。只要把表达式字符串传递给参数pStr
,它会通过调用CheckChar()
函数逐个分析每个字符,直到判断出结果。
函数最前面,有这样一行代码:
g_index = 0;
按照前面讲过的理论,它其实完成的是g_arr
数组清空的动作。是不是很方便。
6. 功能整合
最后,我们需要把从文件中读入的数据通过前面的功能进行处理。代码如下:
int main()
{
int n, i;
int ret;
char str[50];
FILE* fp;
freopen_s(&fp, "input.txt", "r+", stdin);
if (fp == NULL)
{
ret = 0;
}
scanf("%d", &n);
for (i = 0; i < n; i++)
{
scanf("%s", str);
ret = Check(str);
printf("%d\n", ret);
}
}
执行结果如下:
7. 课后作业
编程统计出input.txt
文件保存的文章中,每个单词出现的次数。文章内容如下:
In this chapter we will be looking at files and directories and how to manipulate them. We will learn how to create files, open them, read, write and close them. We'll also learn how programs can manipulate directories, to create, scan and delete them, for example. After the last chapter's diversion into shells, we now start programming in C.
Before proceeding to the way UNIX handles file I/O, we'll review the concepts associated with files, directories and devices. To manipulate files and directories, we need to make system calls (the UNIX parallel of the Windows API), but there also exists a whole range of library functions, the standard I/O library (stdio), to make file handling more efficient.
这段文字来自网络。为了统计更有意义,加入两个条件:
- 统计过程中不考虑空格和标点符号
- 不区分大小写(可以把所有字母转成小写后参与统计)
我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。
上一篇:天花板编程手把手计划-第1期-第5天