這一期的內(nèi)容已經(jīng)過半,部分同學(xué)開始覺得吃力。如果這時候放棄,那前邊的努力就白費了。今天我們來看看上一篇中的課后題。
1. 題目
給出一組代數(shù)表達式,請編程判斷出他們的括號是否配對正確。
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個表達式需要判斷。下面的每一行有一個表達式。要求把這組數(shù)據(jù)原樣保存在文件input.txt
中,通過讀文件的方式讀入數(shù)據(jù)完成判斷。
原題只有5個表達式,這里我又加入了5個新的,有一些特殊的情況需要添加。
2. 分析
這道題從功能上講需要做兩件事:
- 第一件:把保存在
input.txt
文件中的數(shù)據(jù)讀入程序中 - 第二件:掃描每行字符串,判斷是否所有的括號都匹配
是不是看起來沒那么復(fù)雜呢?下面我們就來看如何實現(xiàn)這兩個功能。
3. 讀數(shù)據(jù)
3.1 基本方法
說到讀數(shù)據(jù),我們本能地想到C語言教科書中文件操作的那一章,用基本的打開文件,讀取數(shù)據(jù)的方法就能解決問題。這個方法完全正確。如果你對文件的基本操作還有問題,請參考我之前的文章21天C語言代碼訓(xùn)練營(第十五天)。
這里分享一段Aha_斌提交作業(yè)中的代碼,請參考。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//用宏去存儲文件路徑
#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);//讀取要比較表達式的個數(shù)
fscanf(fp, "%c", &ch);//讀取第一行的換行符
while (strNum--)//當表達式?jīng)]有比較完
{
fgets(str, 50, fp);//讀取一個表達式
// 這里加入判斷str是否符合要求的代碼
}
system("PAUSE");
}
3.2 文件流重定向方法
這里我要介紹另外一種方法,這是ACM比賽中常用的一種數(shù)據(jù)讀取方法。它的特點是把打開的文件重定向到標準輸入流中,這樣我們就可以像讀取鍵盤輸入數(shù)據(jù)一樣讀入數(shù)據(jù)。
先看一下這段代碼:
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
文件中的內(nèi)容讀入程序,之后打印在屏幕上。很容易發(fā)現(xiàn),我們在打開文件并沒有使用大家熟悉的fopen()
函數(shù)數(shù),而是用了這句話:
freopen_s(&fp, "input.txt", "r+", stdin);
這句話的作用是把input.txt
文件用只讀方式打開,之后重定向到stdin
表示的標準輸入流中。當fp
為空時,表示此文件不存在。在這之后,我們可以用scanf()
函數(shù)像讀取鍵盤輸入那樣讀取文件內(nèi)容了。是不是很神奇。
如不需要得到文件指針,我們還可以寫成這樣:
freopen("input.txt", "r+", stdin);
這個形式是標準庫函數(shù)的寫法。
我們來看看執(zhí)行結(jié)果:

4. 判斷括號匹配
4.1 常見方法
完成了作業(yè)的同學(xué)都采用了一種最容易想到的方法,那就是分別統(tǒng)計[]()
四個符號的個數(shù),之后比較個數(shù)是否相同,如果相同就說明匹配,如果不相同就說明不匹配。
但是這種方法有很多bug,比如))((
這種問題。還有我新加入的幾個公式都會判斷錯誤。不過也不是完全不可能,只不過需要另外添加一些約束條件,多寫一些代碼。有興趣的同學(xué)可以參考完成作業(yè)的代碼。
4.2 逐個匹配法
我們這里介紹一種大家沒有想到的算法。這個算法的思想是這樣的,從前到后遍歷表達式,忽略字母和運算符,只關(guān)注括號。一旦遇到匹配成功的括號就整對刪掉,如果最后剩下半個括號(無論是左半邊還是右半邊),說明不匹配,否則就說明是匹配的。

如圖,我們利用一個數(shù)組來完成它,具體分為三個動作:
- 遇到左括號寫入數(shù)組
- 遇到右括號拿它和數(shù)組中最后一個括號做匹配
- 匹配成功兩個括號都刪除,繼續(xù)后面的動作
- 匹配失敗直接跳出,返回失敗
我們看看代碼如何實現(xiàn):
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#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 定義數(shù)據(jù)結(jié)構(gòu)
#define MAX_SIZE 100
int g_arr[MAX_SIZE];
int g_index;
數(shù)據(jù)結(jié)構(gòu)非常簡單,就是一個大小為MAX_SIZE
的一維字符數(shù)組。我們用g_index
這個變量保存這個數(shù)組的當前下標,也就是按順序存入內(nèi)容時最左邊的那個空位。

這個數(shù)組的使用方法如圖所示,每插入一個字符g_index
加1,標記為一個新的空白位置。刪除時動作相反。
注意,這里使用全局變量只是為了方便大家理解,真正的項目中并不鼓勵用全局變量的方式設(shè)計代碼。
5.2 判斷字符
int CheckChar(char ch)
這個函數(shù)主要負責(zé)使用g_arr
進行匹配動作。把表達式中的每個字符傳遞給參數(shù)ch
,它會負責(zé)判斷這個符號是否符合要求。如果拿到的符號不是括號,直接忽略掉。如果是左括號就存入數(shù)組,如果是右括號就和數(shù)組中最后一個括號(g_arr[g_index - 1])做匹配。
這里重點要說的是匹配成功后,需要把數(shù)組中最后一個字符刪掉。只用了一句話:
g_index--;
這句話用到了一個重要的思想,刪除最后一個元素只需要更新當前下標,并不用真的把那一位改成0。我們在維護g_arr
這個數(shù)組時,其實看的是g_index
的值,我們定義了它永遠標記的是第一個空著的可用位置。如果g_index
標記了一個有內(nèi)容的位置,那么下一次寫入時會通過g_arr[g_index++] = '(';
這樣的語句把這塊內(nèi)容覆蓋掉。所以,更新g_index
的方法和刪除這塊內(nèi)容的效果是完全一樣的。
5.3 處理字符串
int Check(char* pStr)
這個函數(shù)是為了方便我們循環(huán)處理每個表達式字符串。只要把表達式字符串傳遞給參數(shù)pStr
,它會通過調(diào)用CheckChar()
函數(shù)逐個分析每個字符,直到判斷出結(jié)果。
函數(shù)最前面,有這樣一行代碼:
g_index = 0;
按照前面講過的理論,它其實完成的是g_arr
數(shù)組清空的動作。是不是很方便。
6. 功能整合
最后,我們需要把從文件中讀入的數(shù)據(jù)通過前面的功能進行處理。代碼如下:
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);
}
}
執(zhí)行結(jié)果如下:

7. 課后作業(yè)
編程統(tǒng)計出input.txt
文件保存的文章中,每個單詞出現(xiàn)的次數(shù)。文章內(nèi)容如下:
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.
這段文字來自網(wǎng)絡(luò)。為了統(tǒng)計更有意義,加入兩個條件:
- 統(tǒng)計過程中不考慮空格和標點符號
- 不區(qū)分大小寫(可以把所有字母轉(zhuǎn)成小寫后參與統(tǒng)計)
我是天花板,讓我們一起在軟件開發(fā)中自我迭代。
如有任何問題,歡迎與我聯(lián)系。