俗话说得好:“光说不练是假把式。”学习C++也是这样,无论看再多的书,如果不自己动手练一练,是体会不到C++的真谛的。在这里,我给自己找了一个简单的练习题:
有一个文本文件,其中保存了100万条email地址的纪录,每一条记录为一行,要求对这个文件中的记录进行排序,并去除重复的项,结果写入另外一个文件。
经常逛CSDN的朋友对这个题目肯定不陌生,因为在CSDN上就曾经有一个讨论是C++更快还是Python更快的帖子,使用的测试题就是这样的,不过他们使用的记录只有78万条,我这里只是增加到了100万条而已。
现代C++的观点更以前相比已经发生了转变,效率已经不是最重要的考虑因素了,最重要的是怎样更快更正确的编写程序,这一点通过《C++ Primer》第四版和第三版的比较就可以看出来。在第四版中,作者更加偏重于介绍STL中的vector和bitset,而不再是数组指针和位操作符;更加偏重于std::string而不是char * ,虽然对于某些在效率方面的要求有些偏执狂的人来说,std::string的实现并不是最完美的。
因此,使用标准库来完成这个题目是很简单的,代码如下:
1 #include <iostream>
2 #include <fstream>
3 #include <vector>
4 #include <string>
5 #include <algorithm>
6
7 int main()
8 {
9 //读取文件中的email地址到vector中
10 std::ifstream input_file("emails100w.txt");
11 std::string tmp;
12 std::vector<std::string> emails;
13 while(input_file >> tmp)
14 {
15 emails.push_back(tmp);
16 }
17
18 //排序
19 std::sort(emails.begin(),emails.end());
20
21 //去除重复项
22 std::vector<std::string>::iterator end_after_unique = std::unique(emails.begin(),emails.end());
23
24 //写入结果文件
25 std::ofstream output_file("results.txt");
26 for(std::vector<std::string>::iterator it = emails.begin(); it != end_after_unique; it++)
27 {
28 output_file << *it << std::endl;
29 }
30
31 return 0;
32 }
加上注释和程序中的空行,也只需要32行代码。使用标准库的好处是显而易见的,整个程序的意义都非常清晰,而且不容易出错,使用STL真的是太方便了。那么,运行效率如何呢?我使用Linux中自带的time命令对程序的运行时间进行分析,如下:
$ time ./SortAndUnique
real 0m35.786s
user 0m26.613s
sys 0m9.437s 那么,STL中的容器还有别的可以完成这个任务吗?我想到了std::set,该容器在插入数据的时候,会自动抛弃重复的值,而且它里面的内容都是排好序的,这么看来,这个容器更加适合于我们的任务。那么,写个代码试一下:
1 #include <iostream>
2 #include <fstream>
3 #include <set>
4 #include <string>
5 #include <algorithm>
6
7 int main()
8 {
9 //读取文件中的email地址到std::set中
10 std::ifstream input_file("emails100w.txt");
11 std::string tmp;
12 std::set<std::string> emails;
13 while(input_file >> tmp)
14 {
15 emails.insert(tmp);
16 }
17
18 //写入结果文件
19 std::ofstream output_file("results.txt");
20 for(std::set<std::string>::iterator it = emails.begin(); it != emails.end(); it++)
21 {
22 output_file << *it << std::endl;
23 }
24
25 return 0;
26 }
嗯,不错,这个代码的行数更少。那它的运行效率呢?比使用std::vector的那个版本是快些还是慢些呢?请看下面的测试数据:
$ time ./SortWithSet
real 0m21.544s
user 0m12.370s
sys 0m9.609s 哇塞,这个程序比前一个整整快了14秒多,其中sys的时间是差不多的,说明这两个版本在输入输出的操作上没多大区别,而排序和去除重复项的工作,使用std::set只有使用std::vector一半不到的时间。
为什么会这样?我认为主要有以下几个原因:
1、std::sort算法使用的排序方法我们不清楚,我们知道,排序有很多种方法,如简单排序、快速排序、堆排序等,简单排序是最慢的,它的时间复杂度为O(n
2) ,而快速排序呢,它在最好情况下能达到O(n*log
2n),而最坏情况下就只有O(n
2)了,堆排序速度最快,时间复杂度为O(n*log
2n)。我不知道std::sort算法使用的是不是堆排序,但是我可以肯定它绝对不会使用简单排序,编写STL的人可不会那么笨。而std::set使用的是什么数据结构呢?一般都是使用的红黑树(平衡二叉树、AVL树),使用该结构的特点是查找一个元素的时间复杂度绝对不会超过log
2n+1,因此,使用std::set进行排序,它的时间复杂度肯定是O(n*log
2n)了。另外,在C++ 0x标准中,会加入另外一些标准容器,如std::unordered_set,从名字上可以看出,它是一个没有排序的set,它使用的数据结构就是哈希表,虽然没有排序,但是它查找数据的时间复杂度却是一个常数。
2、使用std::set容器减少了std::string的复制次数,我们知道STL的容器中保存的是我们的数据的副本,因此,将std::string对象放到std::vector容器中的时侯,会发生一个复制操作,而在使用std::sort算法的时候,容器中的元素交换位置,又会发生很多次的复制操作,再使用std::unique算法的时候,移动容器中的元素也要发生复制操作。使用std::set容器,它只在insert的时候复制一次而已。所以,使用std::set的这个版本比较快那是理所当然的了。
当然,如果你不使用std::string而是用char *,不使用容器和算法而是自己实现平衡二叉树,当然可以写出更快的版本,不过要付出更多的调试代价。
最后,为了让大家都能够找个100w行记录的文本练练手,下面给出一个随机生成100w个email地址的小程序,写得不好,请不要见笑:
1 #include <iostream>
2 #include <fstream>
3 #include <cstdlib>
4 #include <string>
5 #include <vector>
6 using namespace std;
7
8 int main()
9 {
10 //创建1000个用户名
11 char letters[] = "abcdefghijklmnopqrstuvwxyz1234567890_";
12 vector<string> names;
13 for(unsigned int i=0; i<1000; i++)
14 {
15 //获取一个30以内的随机数作为用户名的长度
16 int length = rand()%30 + 1;
17 string name;
18 for(unsigned int j=0; j<length; j++){
19 int index = rand()%37;
20 name.append(1,letters[index]);
21 }
22 names.push_back(name);
23 }
24
25 //创建700个网站名
26 string domains[] = {".com",".cn",".com.cn",".gov",".gov.cn",".net",".net.cn"};
27 vector<string> sites;
28 for(unsigned int i=0; i<100; i++)
29 {
30 //获取一个10以内的随机数作为网站名的长度
31 int length = rand()%10 + 1;
32 string name;
33 for(unsigned int j=0; j<length; j++){
34 int index = rand()%37;
35 name.append(1,letters[index]);
36 }
37 for(int k=0; k<7; k++){
38 name.append(domains[k]);
39 sites.push_back(name);
40 }
41 }
42
43 //构建100万个email地址
44 ofstream emails("emails100w.txt");
45 for(int i=0; i<1000000; i++){
46 emails << names[rand()%1000] << "@" << sites[rand()%700] << endl;
47 }
48
49 return 0;
50 }