学到关联容器,10.6,容器的综合应用:文本查询程序,决定不看课本实现它.
题目如下:
------------------------------------------------------------------------------
我们将实现一个简单的文本查询程序来结束本章。
我们的程序将读取用户指定的任意文本文件,然后允许用户从该文件中查找
单词。查询的结果是该单词出现的次数,并列出每次出现所在的行。如果某单词
在同一行中多次出现,程序将只显示该行一次。行号按升序显示,即第 7 行应
该在第 9 行之前输出,依此类推。
例如,以本章的内容作为文件输入,然后查找单词“element”。输出的前
几行应为:
element occurs 125 times
(line 62) element with a given key.
(line 64) second element with the same key.
(line 153) element |==| operator.
(line 250) the element type.
(line 398) corresponding element.
后面省略了大约 120 行。
------------------------------------------------------------------------------
看完题目,写了一个程序如下:
{
in .close();
in .clear();
in .open(file.c_str());
return in ;
}
int main( int argc, char * argv[])
{
ifstream ifst;
if ( ! open_file(ifst,argv[ 1 ])) // 文件名通过命令行指定
throw runtime_error( " 没有找到指定的文本文件! " );
cout << " 请输入要查找的单词: " << flush;
string target;
cin >> target;
typedef multimap < int , string > Direct;
Direct result;
int line_no = 1 ;
string line;
while (getline(ifst,line)) // 读文件的每一行
{
istringstream istring(line);
string word;
while (istring >> word) // 读这一行的每个单词
{
if (word == target)
result.insert(make_pair(line_no,line)); // 如果出现了要找的单词,把这一行放进multimap
}
line_no ++ ; // 下一行
}
cout << target << " occurs " << result.size() << " times " << endl; // 共出现的次数
Direct::iterator iter = result.begin();
while (iter != result.end())
{
cout << " (line " << iter -> first << " ) " << iter -> second << " . " ; // 输出行
int wcount = result.count(iter -> first);
if (wcount > 1 )
cout << " ( " << wcount << " times) " ; // 如果一行出现了大于1次,在后面注上这一行出现在这个单词的次数
cout << endl;
iter = result.upper_bound(iter -> first); // 下一行
}
system( " PAUSE " );
return 0 ;
}
写完了感觉不对啊,好像课本上这一节挺长的啊,就这么点?然后看了看10.6.1小节设计的部分.顿时觉得我那刚才写的能叫程序吗…首先,只能满足查询一次,查完了就退出了;其次,每次执行这些代码都要开辟大量的存储空间用来存储行的内容,如果文本相当大那么代价太高;再次,如果把这项功能想成一个模块,是写给另一个程序员使用的,或许更贴近实际;最后,bug,贴着单词的标点会被当成是这个单词的一部分.好,重写.
首先,定义一个类,类名就跟课本一致,TextQuery,至于成员我定义了三个公共的成员函数和两个私有成员:
{
public :
void input_file(ifstream & ); // 从文件读入文本,建立起result和text.
set < int > get_list( const string );
// 查询单词在哪些行出现了,行号在返回的set容器中.
string & get_line( int ); // 得到行的内容,返回引用,代价较小.
private :
vector < string > text; // 储存了文本文件的每一行.
map < string , set < int > > result;
// result将每个单词与记录它出现的行数的set容器关联起来.
};
下面是主程序,这里将所有的单词全部转换为小写,也就是说查找的时候不进行大小写匹配:
{
string file_name;
cout << " 请输入文件名: " << flush;
cin >> file_name;
ifstream ifst(file_name.c_str());
if ( ! ifst)
{
cout << " 文件不存在! " << endl;
system( " PAUSE " );
return - 1 ;
}
TextQuery artical;
artical.input_file(ifst);
string target;
cout << " 请输入要查找的单词(以Ctrl+Z结束): " << flush;
while (cin >> target)
{
target = to_lower(target); // 不匹配大小写
set < int > line_list = artical.get_list(target); // 获得行号
cout << target << " occurs " << line_list.size() << " times " << endl; // 一行出现了多次也只算一次
for ( set < int > ::iterator iter = line_list.begin();iter != line_list.end();iter ++ )
cout << " (line " <<* iter << " ) "
<< artical.get_line( * iter) << endl; // 输出每一行
cout << " 请输入要查找的单词(以Ctrl+Z结束): " << flush;
}
system( " PAUSE " );
return 0 ;
}
其中的to_lower()函数定义如下(使用了cctype头文件):
{
for ( string ::iterator iter = word.begin();iter != word.end();iter ++ )
if (isupper( * iter)) * iter = tolower( * iter);
return word;
}
下面实现类里面的三个公共成员函数:
1.最重要的就是input_file()函数,它读取文件中的文本,建立起map容器和vector容器供以后查询使用,注意在建立map容器的时候要准确找到每个单词,不能将标点符号也算做单词的一部分:
{
string line; // 行文本
string ::iterator char_iter; // 指向字符元素的迭代器
int flag; // 标志,0表示没有遇到字母,1表示在收集连续的字母组成单词的过程中.
string word; // 单词
text.push_back( "" ); // 从text[1]开始储存文本,跟行号对应
int line_no = 1 ; //
while (getline(file,line))
{
text.push_back(line); // 建立起vector型容器text
char_iter = line.begin();
flag = 0 ;
while (char_iter != line.end())
{
char c =* char_iter;
if (isletter(c)) // 收集连续的字母组成单词
{
if (isupper(c))c = tolower(c); // 不匹配大小写
if (flag == 0 )flag = 1 ;
word.push_back(c);
}
if (flag == 1 && ((char_iter == line.end() - 1 ) ||! isletter(c))) // 不考虑一个单词被断开在两行出现
{
result[word].insert(line_no); // 如果没有键为word的元素则会自动建立一个再执行insert().
word.clear(); // 清空word
flag = 0 ; // 重置标志位
}
char_iter ++ ; // 下一个字符
}
line_no ++ ; // 下一行
}
}
其中判断是否是字母的函数isletter()如下:
{
return ((c >= 97 && c <= 122 ) || (c >= 65 && c <= 90 ) || (c >= 48 && c <= 57 )); // 可以识别1st这样的单词,但it's要算两个单词.
}
2.查询函数get_list(),返回一个set容器,里面有出现了所查单词的行的行号:
{
set < int > se;
map < string , set < int > > ::iterator word_iter = result.find(word);
if (word_iter != result.end())
se = word_iter -> second;
return se;
}
3.打印行文本的函数get_line():
{
return text[i];
}
写完之后自我感觉良好.然后看课本,感觉有些地方课本上的写法确实有她的好处,比如储存行号的set容器,我的定义是set<int>,而课本上是:
typedef std::vector<std::string>::size_type line_no;然后set<line_no>,这样就能保证line_no的范围合适,并且程序更具逻辑性,更易读.
再比如课本上将我写的input_file()函数分成了两个函数store_file()和build_map();这样也使得程序更易读.但我觉得这样做的代价更大,因为分成两个函数与一个函数比起来多了一次对vector容器的遍历.
不过,我也有比课本写得更完善一点的地方, 就是对于单词的判断上,课本的程序只通过空格来判断,误差比较大.