(本资料qq讨论群112133686)
C++语言提供的标准模板库(Standard Template Library,STL)是面向对象程序设计与泛型程序设计(Generic Programming)思想相结合的一个良好典范。C++标准库中容器类可以方便地存储和操作群体数据,用好C++标准库可以大大提高程序的开发效率。本章介绍了与C++标准库有关的几个基本术语:容器、容器适配器、迭代器、迭代器适配器、通用算法、函数对象和函数适配器,详细介绍了几种可以用于处理线性群体的容器:向量、双端队伍、 和适配器、标准栈、标准队伍和优先队伍。详细介绍4种通用算法以及迭代器和函数对象的应用,并给出了相应的实例。
标准库中给出了很多的算法,为了使读者能够很好地了解和应用这些算法,本章给出了通用算法的函数功能说明。学会使用这些算法可以有效地缩短程序的开发周期,提高程序的效率。本章重点讲解了这些模板库的使用,于初学者在以后编程中经常遇到,反复多实践,使用好这些模板库可事半功倍提供编程效率,缩短开发周期。
14.1 理解STL编码时的防错
在STL程序设计中,容器(container)就是通用的数据结构。容器用来承载不同类型的数据对象,就如同现实生活中,人们使用容器用来装载各种物品一样。但C++中的容器还存在一定的“数据加工能力”,如同一个对数据对象进行加工的模具,可以把不同类型的数据放到这个模具中进行加工处理,形成具有一定共同特性的数据结构。
例如将int型、char型或者float型放到队列容器中,就分别生成int型队列、char型队列或者float型队列,它们都是队列具有队列的基本特性,但是具体的数据类型是不一样的。本例效果如图14-1所示。
图14-1 使用STL库创建容器
程序定义list容器list1,把字符数组x[7]的值赋给list1,反转链表list1,遍历list1,输出list1每个元素的值。代码实现如下:
#include
#include
#include
#include
using namespace std;
int main()
{
char x[7] = {'a', 'b', 'c', 'd', 'e','f','g'};
list
reverse(list1.begin(), list1.end()); //reverse()反转链表
list
cout.precision(10); //设置精度
for (i = list1.begin(); i != list1.end(); ++i) //begin返回第一个元素的指针,end返回最后一个元素的下一位置的指针(list为空时end()=begin())
cout << *i << endl; //打印容器元素
cout << endl;
system("pause");
return 0;
}
(1)上述代码用list创建一个列表list1。在STL程序设计中,容器就是通用的数据结构。STL容器主要包括向量(vector)、列表(list)、队列(deque)、集合(set/ multiset)和映射(map/multimap)等。在STL中容器大致可以分为两类:顺序容器和关联容器。
(2)STL中的所有容器都是类模板,是一个已经建立完成的抽象的数据结构。
提示:可以使用容器来存储任何类型的数据,甚至是自己定义的类,而无需自己再定义数据结构。例如利用deque容器,就很容易建立一个队列。
STL通过采用C++模板机制实现了算法与数据类型的无关性。目的是为了支持通用程序设计,STL实现了算法与容器(数据结构)的分离。本实例定义容器,实现数据类型和算法如移除等分离。本例效果如图14-2所示。
图14-2 裁员计划——获取、删除和清空容器
定义容器vec_A,填充8个数据元素'A',从第3个元素用4个'B'代替,统计数据元素;移除数据元素'A';移除所有元素,清空容器vec_A。代码实现如下:
#include
#include
#include
using namespace std;
template
void print(T &con) //输出容器中所有元素
{
if(con.empty())
cout<<"Container is empty!"< else { T::iterator it; //设置迭代器 for(it=con.begin(); it!=con.end();it++) //遍历容器 { cout<<*it<<" "; //显示容器元素的值 } cout< }} int main() { int num; vector cout<<"Fill vec_A with 'A':"< fill(vec_A.begin(),vec_A.end(),'A'); //填充数据元素 char array_B[]={'B','B','B','B',}; vector copy(vec_B.begin(),vec_B.end(),vec_A.begin()+2);//复制数据元素 print(vec_A); //输出容器中所有元素 cout<<"Copy element of vector to vec_A:"< num=count(vec_A.begin(),vec_A.end(),'B'); //统计数据元素 cout<<"Counting the number of 'B' in vec_A:"< cout< vector it=remove(vec_A.begin(),vec_A.end(),'A'); //移除数据元素 vec_A.erase(it,vec_A.end()); //删除数据元素 print(vec_A); //输出容器中所有元素 vec_A.clear(); //移除所有元素,清空容器 print(vec_A); //输出容器中所有元素 system("pause"); return 0; } (1)算法(algorithm)就是指一些常用的数据处理方法,如向容器中插入、删除容器中的元素、查找容器中的元素、对容器中的元素排序、复制容器中的元素等,这些数据处理方法都是以函数模板的形式实现的。 (2)算法之美就在于不仅独立于底层元素的类型,而且也独立于所操作的容器。利用这些已经定义的算法和迭代器,程序员可以方便灵活地存取容器中存储的各种数据元素。 在STL中,算法的神奇之处在于:算法并非容器的一部分,而是工作在迭代器基础之上,通过迭代器这个“中间人”存取容器中的元素,算法并没有和特定的容器进行绑定。 提示:在传统的软件开发方法中,算法与数据类型、数据结构是紧密耦合的,缺乏通用性。而STL倡导泛型编程风格,即以通用的方式来编写程序。 有容器a和b,分别存有元素,如何把容器a的元素复制到容器b。如一个list容器中的元素复制到另一个已有元素的vector容器。本例效果如图14-3所示。 图14-3 图书印刷——复制元素并自动输出 这是两个演示,演示一把list容器的元素复制到另一个list容器中,演示二把一个list容器中的元素复制到另一个已有元素的vector容器的后面。其代码如下: #include #include #include using namespace std; int main() { list list int i; for (i=0; i < 30; ++i, a.push_back(i)); //在容器后端增加元素 std::list std::list for(++rit;rit != rend;++rit) //遍历容器b b.push_back(*rit); //插入b中 rit=b.begin(); //返回容器前端的迭代器 rend=b.end(); //返回容器末端的迭代器 for(;rit != rend;++rit) //遍历容器b cout << *rit << ", "; //显示容器元素的值 system("pause"); list deque int val = 0 ; cout << "Please input the value : " << endl ;//输入一个值 while (cin >> val) { ilst.push_back(val) ; //插入list容器ilst中 } for (list { if (*iter % 2) //如果是奇数 { oddDq.push_back(*iter) ; //插入deque容器到oddDq中 } else evenDq.push_back(*iter) ; //插入deque容器到evenDq中 } for (deque { cout << *dter << " " ; //显示容器元素的值 } system("pause"); return 0; } (1)代码中演示了把list容器的元素复制到另一个list容器中,一个vector容器中的元素复制到另一个已有元素的vector容器的后面。 (2)insert()操作可以加入iterator范围的元素,如vector(向量)、list(列表)push_back()。 提示:运用C++语言的STL时记住一句话,99%的情况不需要写for循环。如相同类型的vector a, b;,就可以编程为:a.insert(a.end(), b.begin(), b.end());。 14.2 使用序列式容器 向量容器灵活性在于定义向量容器后随时可以增加元素。本实例演示的是输入学生成绩计算标准分,因输入的数据长度是未知的,具有向量增长特性,这方面特性以后可以应用很广,不需要初始化内存。本例效果如图14-4所示。 图14-4 学生成绩转换为标准分(向量增长) 学生的成绩一般是原始成绩,要将学生的成绩转换为标准分,必须首先比较所有学生的成绩,取得最高分,将学生原始成绩除以最高分,然后乘上100。代码实现如下: #include #include using namespace std; int main(){ vector double max,temp; //最高分,暂存分数 int i; cout<<"Input -1 to stop:"< cout<<"Enter the original score 1: "; cin>>max; //取得输入分数 scorevector.push_back(max); //在容器后端增加元素 for(i=1;true;i++) { //一直循环下去 cout<<"Enter the original score "<
cin>>temp; if(temp==-1){ //输入-1 break; } scorevector.push_back(temp); //在容器后端增加元素 if(temp>max) //输入大于最高分 max=temp; } max/=100; //同于max=max/100; cout<<"Output the standard scores: "< for(i=0;i scorevector[i]/=max; //同于scorevector[i]=scorevector[i]/max cout< } cout< system("pause"); return 0; } push_back()在容器后端增加元素。由于代码中没有给出学生人数,所以采用向量作为数据存储结构,因为向量的元素个数可以自动实现动态增长。 顺序容器可以用于存储线性表类型的数据结构,编程时候需要知道容器中较为重要的成员函数,list容器是一个标准双向链表。本例打印list容器元素的值,效果如图14-5所示。 图14-5 打印容器元素的值 程序主要是一个打印容器元素的值的函数PRINT_ELEMENTS(),输入一个容器的名称,返回第一个元素的指针,函数end()返回最后一个元素的下一位置的指针。代码实现如下: #include #include #include using namespace std; template //打印容器元素的值 inline void PRINT_ELEMENTS (const T& coll, const char* optcstr="") { typename T::const_iterator pos; std::cout << optcstr; //begin返回第一个元素的指针,end返回最后一个元素的下一位置的指针(list为空时end()=begin()) for (pos=coll.begin(); pos!=coll.end(); ++pos) { std::cout << *pos << ' '; //打印容器元素 } std::cout << std::endl; } int main() { list // insert elements from 1 to 8 for (int i=1; i<=8; ++i) { coll.push_back(i); //增加一元素到链表尾 } PRINT_ELEMENTS(coll,"initialized: "); system("pause"); } (1)上述代码begin()返回第一个元素的指针,end()返回最后一个元素的下一位置的指针(list为空时end()=begin());push_back()增加一个元素到链表尾。 (2)list容器是一个标准双向链表,每个元素都知道其前一个与下一个数据元素,查找速度较慢,只能根据链表指针的指示逐一查找,但一旦找到相关位置,完成元素的插入和删除则很快。 这是个综合的实例,目的是加深对顺序容器如vector和类函数理解及综合应用,编写软件目的大部分是信息处理,这是一个雏形的小型信息处理系统,本实例是个简单的学生管理系统管理方面的小软件,涉及到向量,通过输入如姓名信息进行查询。本例效果如图14-6所示。 图14-6 一个简单的学生管理系统 (1)定义个学生类Cstudent,基于CStudent创建vector对象classes1,定义类函数输入学生信息inputStudentInfo、查找学生信息findStudent。其代码如下: #include #include using namespace std; #ifndef STUDENTh #define STUDENTh #include using namespace std; class CStudent //学生类 { private: string strName; //姓名 double chinese; //语文 double math; //数学 double english; //英语 public: CStudent(); CStudent(std::string Name,double c,double m,double e); void SetChinese(double a); //输入语文成绩 void SetMath(double a); //输入英语成绩 void SetEnglish(double a); //输入数学成绩 void SetName(std::string c); //输入姓名 std::string returnName(); double returnChinese(); //返回语文成绩 double returnMath(); //返回数学成绩 double returnEnglish(); //返回英语成绩 double returnTotalPerformance(); //返回总成绩 double returnAverage(); //返回平均分 ~CStudent(){} //析构函数 }; #endif CStudent::CStudent() //构造函数 { strName="General Student"; //初始化 chinese=0; math=0; english=0; } CStudent::CStudent(std::string Name,double c,double m,double e)//类赋值 { strName=Name; chinese=c; math=m; english=e; } void CStudent::SetChinese(double a){chinese=a;} //输入语文成绩 void CStudent::SetEnglish(double a){english=a;} //输入英语成绩 void CStudent::SetMath(double a){math=a;} //输入数学成绩 double CStudent::returnChinese (){return chinese;}//返回语文成绩 double CStudent::returnEnglish (){return english;}//返回英语成绩 double CStudent::returnMath (){return math;} //返回数学成绩 double CStudent::returnTotalPerformance(){return chinese+math+english;}//返回总成绩 double CStudent::returnAverage (){return (chinese+math+english)/3;}//返回平均分 string CStudent::returnName (){return strName;} //返回姓名 void CStudent::SetName (std::string c){strName=c;} void inputStudentInfo(std::vector int findStudent(std::vector std::vector std::string str); void inputStudentInfo(std::vector { //代码略 } int findStudent(std::vector std::vector std::string str) //查找学生信息 { for( it=vec.begin() ; it != vec.end() ; it++ ) //遍历vec { if(str == (*it).returnName()) //查找相同的姓名 { return 1 ; //查找到,返回1 break ; } } it = NULL ; return 0 ; //未查找到,返回0 } (2)基于CStudent创建vector对象classes1,输入学生信息,根据姓名查找学生信息,输出记录号、姓名和总成绩。代码如下: int main() { std::vector std::vector inputStudentInfo(classes1); //输入学生信息 std::cout<<"Enter the name you want to check : " < char a[20]; std::cin>>a; if(findStudent(classes1,iter,a)) //查找学生信息 { std::cout< <<"Chinese : "< <<"\nMath : "< <<"\nEnglish : "< } else { std::cout<<"No found!\n"; } int i ; std::cout<<"All students' performance descend by total performance :\n" ; //遍历iter for( iter = classes1.begin() , i = 1 ; iter != classes1.end() ; iter++ , i++ ) { std::cout<<"The rank "<记录后 std::cout< std::cout< } system("pause");; //按任意键退出 return 0 ; } (1)这是个简单的学生管理系统,根据输入的信息例如姓名等进行查询。设计顺序容器vector存储学生信息,根据姓名实现快速查询。 (2)vector函数begin()、end()返回第一个元素、最后一个元素的指针,push_back()、pop_back()在容器后端增加、删除元素。 提示:在此代码的基础上可以增加其他维护功能:例如插入、删除、修改等,按成绩进行排序等。 这是个综合的实例,目的是加深对顺序容器如vector(向量),容器适配器如stack(栈)和类理解及综合应用。灵活运用这些概念,可把现实生活中的事务用程序来实现,本实例演示计算器的程序,涉及到向量、容器适配器如栈、类等概念,运行效果如图14-7所示。 图14-7 编写一个简单的计算器 (1)定义个类calculator,定义向量s存储计算表达式,栈symbol、result处理数字和运算符号。其代码如下: #include #include #include #include #include using namespace std; class calculator { private: vector stack stack vector public: int compare(char m) { switch(m) { case '*': case '/': return 2;break; case '+': case '-': return 1;break; default:return 0;break; } } void InputNumbers() //输入计算的表达式 { //代码略 } void OutputResult() //输出计算的表达式 { //代码略 }; (2)主函数定义个计算器类,调用输出计算的表达式函数。代码如下: int main() { char m='y'; while(m=='y' || m=='Y') { calculator cal; //定义类 cal.InputNumbers(); //输入计算的表达式 cal.OutputResult(); //输出计算的表达式 cout<<"是否继续计算【Y/N】"; m=getchar();m=getchar(); } } (1)这是一个简单的计算器程序,根据用户输入计算表达式进行计算。代码是把数字和计算符号存储在栈中的。 (2)代码主要对vector(向量),容器适配器如stack(栈)和类的理解和运用。push()进栈,symbol.pop()取栈顶元素;输入计算的表达式InputNumbers()分别把数字和计算符号入栈,再一个个取栈顶元素。输出计算的表达式OutputResult()对数字入栈和运算符号栈取栈顶元素,进行计算。 提示:在此代码的基础上可以增加计算器其他计算功能如平方、开方等。 游戏、音频、视频及图片等占据空间太大,在上传互联网的时候必须压缩,下载该文件后进行相应的解压缩才能运行。压缩的算法很多:字典算法、固定位长算法、RLE算法、LZ77算法等等。本实例演示的是经典的压缩算法Huffman算法,效果如图14-8所示。 图14-8 一个经典的压缩算法 压缩成员函数,遍历输入的strs每个字符,调用_encode_single(),进行压缩,把压缩结果插入容器。代码实现如下: //以下为辅助类 template class will_delete{ public: will_delete(T* ptr){p= ptr;} operator T*(){return p;} T* operator->(){return p;} T* p; }; template class vtree{ //存储HuffmanTree的类 public: vtree(){val=0;} vtree(will_delete ~vtree(){ for(size_t i=0; i<_list.size(); i++) delete _list[i]; delete val; } bool leaf()const{return _list.empty();} //是否为空 vector T* val; }; //辅助类结束 struct huffman_node{ huffman_node(string st, size_t w){s=st;wight=w;} string s; size_t wight; }; //辅助函数 bool vtree_huffman_node_ja(vtree return a->val->wight > b->val->wight; } class huffman_rule{ public: huffman_rule(vector for(size_t i=0; i encode._list.push_back(new vtree while(encode._list.size()> 2){ sort(encode._list.begin(), encode._list.end(), &vtree_huffman_node_ja); //排序算法 vtree encode._list.pop_back(); //在容器后端删除元素 vtree encode._list.pop_back(); //在容器后端删除元素 encode._list.push_back(join(min1,min2));//在容器后端增加元素 } size_t wight=0; switch(encode._list.size()){ case 2:wight+=encode._list[1]->val->wight; case 1:wight+=encode._list[0]->val->wight; default:; } encode.val= new huffman_node("", wight); } vtree vtree ret->_list.push_back(a); //在容器后端增加元素 ret->_list.push_back(b); //在容器后端增加元素 return ret; } vector if(ptree->leaf() && ptree->val->s==symbol) return vector } vector //代码见212 } vector //代码见213 } private: //压缩函数 //代码见212 static vector static string _decode_single(const vtree int _tmain(int argc, _TCHAR* argv[]) { //代码略 } (1)霍夫曼编码针对文本文件的一种压缩算法,基于统计编码的算法,属于无损压缩编码。其码长是变化的,出现频率高编码的长度较短;频率低编码长度较长。这样,处理全部信息的总码长一定小于实际信息的符号长度。 (2)上面代码insert()在容器中间插入元素,rbegin()返回容器前端的倒转迭代器,rend()返回容器末端的倒转迭代器,push_back()在容器末尾增加新元素。 本实例代码见实例14-8。Huffman是一种无损失的压缩算法,是文字或软件文档的最理想压缩方式,现在很流行的压缩软件如ZIP或ARJ都使用它。本例演示Huffman编码实现,效果如图14-9所示。 图14-9 自定义函数实现数据压缩 定义压缩函数encode_string(),遍历输入的待压缩的strs字符,调用压缩函数_encode_single(),递归调用_encode_single(),把压缩内容插入容器。代码实现如下: vector vector for(size_t i=0; i< strs.size(); i++){ //遍历strs字符 temp= _encode_single(encode, strs[i]); ret.insert(ret.end(), temp.rbegin(), temp.rend()); //在容器中间插入元素 } return ret; } …… private: //压缩函数 static vector vector if(obj.val->s==str){ ret.push_back(true); //返回值会被删除 只是表明找到了 return ret; } for(size_t i=0; i ret= _encode_single(*obj._list[i], str); //压缩函数 if(ret.size()){ if(obj._list[i]->leaf()) ret.pop_back(); //删除标识已找到的符号 ret.push_back(i==0?0:1); //在容器后端增加元素 return ret; } } ret.clear(); //清除容器内的元素 return ret; } Huffman编码过程的几个步骤: (1)将信号源的符号按照出现概率递减的顺序排列。 (2)将最下面出现的两个最小概率进行合并相加,得到的结果作为新符号的出现概率。 (3)重复进行步骤1和2直到概率相加的结果等于1为止。 (4)在合并运算时,概率大的符号用编码0表示,概率小的符号用编码1表示。 (5)记录下概率为1处到当前信号源符号之间的0,l序列,从而得到每个符号的编码。 本实例代码见实例14-8。解码是编码的逆过程,即是根据码字查询Huffman码表,还原出初始值。本例演示Huffman解码实现,效果如图14-10所示。 图14-10 自定义函数实现数据解压 解码是编码的逆过程,即是根据码字查询Huffman码表,还原出初始值。其代码如下: vector vector //定义了迭代器i,begin()返回指示容器中第一个元素 vector while(i!= code.end()){ //遍历 ret.push_back(_decode_single(encode, i, code.end()));//在容器后端增加元素 } return ret; } …… //解密 static string _decode_single(const vtree while(!obj.leaf()){ //遍历直到叶节点 bool path= *pbit; if(pbit!=pend){ pbit++; }else{ return ""; //返回值 } return _decode_single(path==0?*obj._list[0]:*obj._list[1], pbit, pend); //递归 } return obj.val->s; //返回值 } vtree }; 霍夫曼编码具有如下特点: (1)编出来的码都是异字头码,保证了码的唯一可译性。 (2)由于编码长度可变,使得译码时间较长,这样采用霍夫曼编码的压缩与还原相当费时。 (3)编码长度不统一,硬件实现有难度。 (4)对不同信号源的编码效率不同,当信号源的符号概率为2的负幂次方时,达到100%的编码效率;若信号源符号的概率相等,则编码效率最低。 (5)由于0与1指定是任意的,故由上述过程编出的最佳码不是唯一的,但其平均码长是一样的,故不影响编码效率与数据压缩性能。 提示:解压是压缩的反过程,算法的实现过程正好相反。 C++中algorithm的sort算法简单方便,可用来排序容器的内容,输入的值往往是默认的值,程序运行中能自动识别默认值。本例演示的是倍增算法,效果如图14-11所示。 图14-11 高效倍增算法 主函数调用buildSA(),buildSA()调用函数getLCP()和cSort()实现排序算法的功能。代码实现如下: #include #include using namespace std; const int MAX_SFX = 210000; struct Sfx { //定义结构 int i; int key[2]; bool operator < (const Sfx& s) const { return key[0] < s.key[0] || key[0] == s.key[0] && key[1] < s.key[1]; } //(||)逻辑或,(&&)逻辑与 }; int g_buf[MAX_SFX + 1]; Sfx g_tempSfx[2][MAX_SFX], *g_sa = g_tempSfx[0]; //基数排序 void cSort(Sfx* in, int n, int key, Sfx* out) { //将 cnt 中前 sizeof(int) * (n + 1) 个字节用 0 替换并返回 cnt int* cnt = g_buf; memset( cnt, 0, sizeof(int) * (n + 1) ); for (int i = 0; i < n; i++) { cnt[ in[i].key[key] ]++; } for ( i = 1; i <= n; i++) { cnt[i] += cnt[i - 1]; } for ( i = n - 1; i >= 0; i--) { out[ --cnt[ in[i].key[key] ] ] = in[i]; } } //创建字符串'text',长度为'len'的后缀数组 //写结果到全局数组'g_sa' //创建后缀数组并排序复杂度 O(n * log n) void buildSA(char* text, int len) { Sfx *temp = g_tempSfx[1]; int* rank = g_buf; //rank[i]记录排序后的序号 for (int i = 0; i < len; i++) //g_sa[i].i 表示排名为i的位置 { g_sa[i].i = g_sa[i].key[1] = i; g_sa[i].key[0] = text[i]; } sort(g_sa, g_sa + len); //排序 for ( i = 0; i < len; i++) { g_sa[i].key[1] = 0; } int wid = 1; while (wid < len) { rank[ g_sa[0].i ] = 1; for (int i = 1; i < len; i++) { rank[ g_sa[i].i ] = rank[ g_sa[i - 1].i ]; if ( g_sa[i-1] < g_sa[i] ) { rank[ g_sa[i].i ]++; } } for ( i = 0; i < len; i++) { g_sa[i].i = i; g_sa[i].key[0] = rank[i]; g_sa[i].key[1] = i + wid < len? rank[i + wid]: 0; } cSort(g_sa, len, 1, temp); //按次关键字排序 cSort(temp, len, 0, g_sa); //按主关键字排序 wid *= 2; //倍增 } } //是将a所指的内容赋给b所指的内容,l累加并返回值 int getLCP(char* a, char* b) { int l=0; while(*a && *b && *a==*b) { l++; a++; b++; } return l; } void getLCP(char* text, Sfx* sfx, int len, int* lcp) { int* rank = g_buf; //rank[i]记录排序后的序号 for (int i=0, r=0; i < len; i++, r++) { rank[ sfx[i].i ] = r; } lcp[0] = 0; if (rank[0]) //递归 { lcp[ rank[0] ] = getLCP( text, text + sfx[ rank[0]-1 ].i ); } for ( i = 1; i < len; i++) { if ( !rank[i] ) { continue; } if (lcp[ rank[i - 1] ] <= 1) //递归 { lcp[ rank[i] ] = getLCP( text+i, text+sfx[ rank[i]-1 ].i ); } else { int L = lcp[ rank[i - 1] ] - 1; //递归 lcp[rank[i]] = L+getLCP(text+i+L, text+sfx[rank[i]-1].i+L); } } } //测试套件和用法的例子 #include using namespace std; int main() { char str[] = "aabbaa{post.content}ababab"; //输入字符串 int from[] = {0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1}; int lcp[13]; //存储输出字符串 //调用buildSA(str, n),建立后缀数组并排序复杂度 O(n * log n) buildSA(str, 13); getLCP(str, g_sa, 13, lcp); for (int i=1; i<13; i++) //第一个后缀是无用的(空的) { cout< system("pause"); return 0; } (1)倍增法是构造后缀数组一个比较实用的算法。其基本思想是先计算出每个后缀的k-前缀的rank值,然后在此基础上计算每个后缀的2k-前缀的rank值;k从1开始,直到每个后缀都排出先后顺序为止,任何两个后缀都不会相等,也就是说每个后缀最终都能排出先后顺序。在处理2k-前缀时,只需要使用基数排序算法,先后对两位数字排序(可以采用基数排序算法对每一位数字排序)。 (2)符号(||)逻辑或,(&&)逻辑与;memset( cnt, 0, sizeof(int) * (n + 1) ),将cnt中前sizeof(int) * (n + 1)个字节用0替换并返回cnt。 提示:倍增法在最坏情况下,需要做lgn次基数排序,每一次基数排序的操作次数为2*O(n)。因此它的时间复杂度是O(nlgn)。倍增法虽然没有达到像DC3算法的线性复杂度,但是它的优点是实现比较简单,因此在实践中常被采用。 本实例演示的是训练玩家的逻辑思考能力的游戏,本实例举推箱子游戏,涉及到类和BFS算法,同时还要产生随机数,是类和算法的综合应用。本例效果如图14-12所示。 图14-12 推箱子 定义每一步的数据类型node,推箱子类Sokoban实现各种动作,Box_Bfs()是实现BFS算法。代码实现如下: //箱子路径验证函数 //用BFS算法对箱子验证是否可到目的地 void Sokoban::Box_Bfs(int bx, int by, int px, int py) { queue //visit对上一步走到下一步的记录,防止箱子走重复路劲 //visit[i][j][z][k]表示箱子从点(i,j)到点(z,k) //visit[][][][]为0时表示未走过,1时表示已走过 int visit[H][L][H][L]; memset(visit, 0, sizeof(visit)); //visit数组初始化 s.bx = bx; s.by = by; //将起始的箱子、人位置放入队列 s.px = px; s.py = py; _Box.push(s); int pe_x, pe_y; while(!_Box.empty()) //队列为空时跳出 { s = _Box.front(); _Box.pop(); if(GameMap[s.bx][s.by] == Target) //到达目的地 { Prove = 1; break; } for(int i = 0; i < 4; i++) { e.bx = s.bx + dx[i]; e.by = s.by + dy[i]; switch(i) //人推箱子的位置 { case 0: pe_x = s.bx + dx[2]; pe_y = s.by + dy[2]; break; case 1: pe_x = s.bx + dx[3]; pe_y = s.by + dy[3]; break; case 2: pe_x = s.bx + dx[0]; pe_y = s.by + dy[0]; break; case 3: pe_x = s.bx + dx[1]; pe_y = s.by + dy[1]; break; } //验证箱子和人的位置合法性 if(!Check(e.bx, e.by) || !Check(pe_x, pe_y) || GameMap[e.bx][e.by] == Block || GameMap[pe_x][pe_y] == Block || visit[s.bx][s.by][e.bx][e.by] ) continue; //如人可推箱子即进入队列 if(Sokoban::People_Bfs(pe_x, pe_y)) { //保存人推箱子后的位置 e.px = pe_x; e.py = pe_y; _Box.push(e); visit[s.bx][s.by][e.bx][e.by] = 1; //箱子路径的标记 } } } } //人路径验证函数 //用BFS算法对人验证是否可推箱子 bool Sokoban::People_Bfs(int ex, int ey) { queue node t, end; //visit数组对人的路径进行标记,0为未走过,1为走过 int visit[H][L]; //visit数组初始化为0 memset(visit, 0, sizeof(visit)); t.px = s.px; t.py = s.py; //人初始位置进入队列 _People.push(t); visit[t.px][t.py] = 1; while(!_People.empty()) //队列为空时跳出 { t = _People.front(); _People.pop(); if(t.px == ex && t.py == ey) //人可到达(ex,ey)该点 return 1; for(int i = 0; i < 4; i++) { end.px = t.px + dx[i]; end.py = t.py + dy[i]; //检查人的位置合法性 if(!Check(end.px, end.py) || GameMap[end.px][end.py] == Block || GameMap[end.px][end.py] == Box || visit[end.px][end.py]) continue; _People.push(end); //进入队列 visit[end.px][end.py] = 1; //记录 } } return 0; } (1)推箱子是一个来自日本的古老游戏,可训练玩家的逻辑思考能力。在一个狭小的仓库中,要求把木箱放到指定的位置,稍不小心就会出现箱子无法移动或者通道被堵住的情况,只有巧妙利用有限的空间和通道,合理安排移动的次序和位置,才能顺利完成箱子的堆放。 (2)BFS算法(宽度优先搜索)原型是图的宽度优先遍历问题,即从源顶点s出发,遍历每一个节点。基本思路先扩展当前节点所有邻接的节点,然后再从这些顶点出发,遍历所有顶点即分层处理,再将遍历路径表示成树,也就是按层次遍历。 提示:每次运行程序的地图是不同的。 14.3 使用关联式容器 双端队列是既可以在队头插入和删除,也可以在队尾插入和删除的一种特殊队列。因此在实际应用中,双端队列比普通队列的应用范围更加广泛。本例效果如图14-13所示。 图14-13 队列镜像 定义deque类型的容器deque_A,显示输出双端队列中的所有元素函数print(),元素从队尾进队列,尾部插入容器元素insert(),尾部删除容器元素pop_back(),头部插入insert()和删除容器元素pop_back()。代码实现如下: #include #include using namespace std; template void print(T &deq, char *str) //显示输出双端队列中的所有元素 { T::iterator it; cout<<"The elements of "< //begin返回第一个元素的指针,end返回最后一个元素的下一位置的指针(list为空时end()=begin()) for(it=deq.begin();it!=deq.end();it++) { cout<<*it<<" "; } //显示容器元素 cout< } int main() { deque deque_A.push_back('c'); //从队尾进队列 deque_A.push_back('d'); deque_A.push_front('b'); //从队头进队列 deque_A.push_front('a'); print(deque_A,"deque_A"); //显示队头元素 cout<<"The first element of deque_A is "< //显示队尾元素 cout<<"The last element of deque_A is "< deque_A.insert(deque_A.begin(),'x'); //在队头插入元素 deque_A.insert(deque_A.end(),'z'); //在队尾插入元素 print(deque_A,"deque_A"); deque_A.pop_front(); //从队头出队列(删除元素) print(deque_A,"deque_A"); deque_A.pop_back(); //从队尾出队列(删除元素) print(deque_A,"deque_A"); system("pause"); return 0; } 这是双端队列容器实例,push_front()在容器前端增加元素;pop_front()在容器前端删除元素;push_back()在容器后端增加元素;pop_back()在容器后端删除元素。 提示:实际应用中,双端队列比普通队列的应用范围更加广泛。 穷举法或称为暴力破解法,是一种针对于密码的破译方法;即将密码进行逐个推算直到找出真正的密码为止。计算机用户出于安全的考虑,需进行反暴力处理。本例演示设下试误的可容许次数,以应对使用密码穷举法的破解者,效果如图14-14所示。 图14-14 反暴力破解 编写一个无回显的密码登录器,输入密码时,来保证安全性,要求在屏幕上显示的是星号******,要求只能够输入三次密码,多于三次,系统自动异常退出。代码实现如下: //检索密码和账号 void save_look( map { string str1,str2; ifstream input("test.txt"); //用于文件的输入操作 while(input >> str1 >> str2) m_mp.insert(make_pair(str1,str2)); //插入元素 input.close(); //关闭文件 map if(iter == m_mp.end()) { cout << "\n这个账号不存在,是否要创建?请输入Y或者N" << endl; char ch1; while (cin >> ch1) { if((ch1 == 'Y')||(ch1 == 'y')) { m_mp.insert(make_pair(m_account,m_code)); //插入元素 ofstream output("test.txt", ios::app | ios::out); //文件的输出操作 // output.open("test.txt"); //打开文件 output.seekp(0,ios::end); output << m_account << '\n' << m_code << endl; output.close(); cout << "创建成功!" << endl; exit(0); } else if((ch1 =='N')||(ch1 =='n')) { exit(0); } else { cout << "输入错误! 请重新输入!" < else { int i = 1; while(m_mp[m_account] != m_code) { char ch1; cout << "\n密码错误请重新输入!" << endl; m_code = ""; i++; if(i == 5) { cout << "\n对不起你重复次数太多!" << endl; exit(0); } else { code_input(ch1,m_code); //密码输入函数 } } cout << "\n账号密码正确!" << endl; } } 密码验证机制是需设下试误的可容许次数,防止使用密码穷举法的破解者。当试误次数达到可容许次数时,密码验证系统会自动拒绝继续验证,有的甚至还会自动启动入侵警报机制。 这是密码登录程序是一个简单的演示,不和具体的数据库连接,暂时把输入的用户登录信息存在文件“test.txt”中。在关联容器map中,函数insert()插入元素,find()查找关键元素。 提示:可参照实例232和233,基于MySQL数据库,利用SQL(结构化查询语言)存储数据,使用SQL语言插入记录、查询账号和密码。 14.4 关联式容器的成员函数操作 在关联容器,如果需要一个键/值对(pair)来存储数据,map是一个很好的选择。map中的数据元素是由关键字和数据两部分组成。例如:在银行管理系统中,每个用户都有一个对应的账号。本例定义容器并插入元素,效果如图14-15所示。 图14-15 插队(在容器中部插入元素) 创建一个pair #include #include #include using namespace std; int main() { pair pair map map //将t插入obM中,返回结果保存在pair对象res中 pair if (res.second) //判断是否插入成功 cout<<"插入成功"< else cout<<"已包含关键字与t相同的元素:"<<(*res.first).second< multimap multimap itDM=obDM.insert(t); //执行插入操作 cout<<"插入的元素为:"<<(*itDM).second< system("pause"); return 0; } (1)容器multimap(多映射),其特性map是{键(key),值)}对的组成的集合。集合中的元素按键排列。multimap是允许键/值对有重复的集合。map和multimap的关系如同set和multiset之间的关系,如果希望将键与值相关联就可以使用map/muitimap。包含在头文件< map >。 (2)如果需要一个键值对(pair)来存储数据,map是一个好的选择。使用参数数目可变的函数时要注意以下几点: 1、在定义函数时,固定参数部分必须放在参数表的前面,可变参数在后面,并用省略号“...”表示可变参数。在函数调用时,可以没有可变的参数,如代码simple_va_fun(int start, ...) 。 2、必须使用函数va_start()来初始化可变参数,为取第一个可变的参数作好准备工作;使用函数va_arg()依次取各个可变的参数值;最后用函数va_end()做好结束工作,以便能正确地返回。 提示:容器set(集合)和multiset(多集)比较,set是一个元素集合,集合中的元素按有序的方式存储,set中没有重复的元素,但multiset中允许有重复的元素。但字需要使用元素集合,而且对元素的查找、插入和删除操作都较为频繁时,就可以使用set/multiset。 14.5 迭代器 这是个综合实例,目的加深对模拟文件资源管理系统理解。遍历目录就是给定一个目录,访问其中的所有文件(包括子目录下的文件),可实现快速对文件的属性修改、查找系统文件的目的。本例效果如图14-16所示。 图14-16 模拟文件资源管理系统 定义个函数getFiles()输入目录和容器,用_findfirst()和_findnext()搜索所有的文件,主函数for循环遍历容器files。其代码如下: void getFiles( string, vector int main() { vector getFiles( ".", files ); // print the files get for (int j=0; j cout << files[j] << endl ; //打印容器的值 } system("pause"); return 0; } void getFiles( string path, vector //文件句柄 long hFile = 0; //文件信息 struct _finddata_t fileinfo; //存储文件各种信息的结构体 string p; //搜索与指定的文件名称匹配的第一个实例 if ((hFile = _findfirst(p.assign(path).append("/*").c_str(), &fileinfo)) != -1) { do { //如果是目录,迭代之 //如果不是,加入列表 if ((fileinfo.attrib & _A_SUBDIR)) { //文件的属性attrib //字符串比较 if (strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name, "..") != 0) getFiles( p.assign(path).append("/").append(fileinfo.name), files ); //递归 } else { files.push_back(p.assign(path).append("/").append(fileinfo.name) ); } //搜索与该函数提供的文件名称匹配的下一个实例 } while (_findnext(hFile, &fileinfo ) == 0); _findclose(hFile); //关闭由FindFirstFile函数创建的一个搜索句柄 } } (1)函数_findfirst(),搜索与指定的文件名匹配的第一个实例;若成功则返回第一个实例的句柄,否则返回-1L。_findnext搜索与该函数提供的文件名匹配的下一个实例,若成功则返回0,否则返回-1。_findclose关闭由FindFirstFile()函数创建的一个搜索句柄。所谓遍历目录,就是给定一个目录,访问其中的所有文件(包括子目录下的文件)。迭代是比较常用的遍历算法。 (2)“资源管理器”是Windows系统提供的资源管理工具,可以用它查看本台电脑的所有资源,特别是它提供的树形的文件系统结构。 提示:“资源管理器”和“我的电脑”工具在使用功能上是一样的,两者都是用来管理系统资源的,或者说都是用来管理文件的。在“资源管理器”中还可以对文件进行各种操作,如:打开、复制、移动等。 14.6 泛型算法 反向输出可以编写个函数实现,但最简单的就是使用STL标准库的泛型算法,本实例讨论这方面问题,了解泛型算法的优点,演示反转容器中的字符串,效果如图14-17所示。 函数demo1()输入一个字符串,主函数用reverse_copy()和reverse()反转字符串。代码实现如下: #include #include #include using namespace std; int demo1() { char a[500],b[500],*p,*q; gets(a); //取得一个字符串 p=a; q=b; while(*p!='\0') //是否是字符串最后面 p++; //指针指向下一个位置 p=p-1; while(p!=a-1) { *q=*p; //字符交换 p--; //指针指向下一个位置 q++; //指针增加一位置 } *q='\0'; //字符串后面加'\0' puts(b); //输出反转的字符串 cout< return 0; } int main() { demo1(); string first,second; cout<<"输入一字符串:"< cin>>first; second.resize(first.size()); //resize函数用来改变string的大小 //begin()指向字符串第一个字符的迭代器,end()超尾值的迭代器 reverse_copy(first.begin(),first.end(),second.begin());//使用reverse_copy函数 cout<<"反向输出:"< string str; cout<<"输入一字符串:"< cin>>str; reverse(str.begin(),str.end()); //使用reverse函数 cout<<"反向输出:"< system("pause"); return 0; } (1)everse()函数实现反转一个容器内元素的顺序的功能。函数reverse(first,last);,first为容器的首迭代器,last为容器的末迭代器,无返回值。 (2)resize()函数用来改变字符串string的大小,若小于string的长度size个字符,则截取前size个字符,若大于则用空格补充。 提示:reverse_copy()函数和reverse()函数的唯一区别在于:reverse_copy会将结果拷贝到另外一个容器中,不影响原容器的内容。 14.7 适配器 容器适配器队列(queue),是在一端插入元素,在另一端取出元素,具有先进先出(first in, first out, FIFO)的特性的一种特殊队列,插入和删除速度都较快。当需要一个FIFO结构时就使用这个队列。本例实现取得容器头尾元素功能,效果如图14-18所示。 程序主要是一个打印容器元素的值函数print(),主函数创建队列q,把元素压入队列,取队头元素和取队尾元素。代码如下: #include #include using namespace std; template void print(queue { if(q.empty()) //判断队列是否空 cout<<"Queue is empty!"< else { int j=q.size(); //取容器大小 for(int i=0; i cout< q.pop(); //出队列 } cout< }} int main() { queue q.push('a'); //进队列 q.push('b'); q.push('c'); q.push('d'); //取队头元素 cout<<"The first element is : "< //取队尾元素 cout<<"The last element is : "< cout<<"The queue is : "< print(q); system("pause"); return 0; } (1)代码front()返回容器中第一个元素的引用,back()返回容器中最后一个元素的引用程序。代码main()函数中创建队列,调用成员函数实现取队头元素和取队尾元素。 (2)queue(队列),在需要一个FIFO结构时就使用这个队列。包含头文件< queue >。 提示:适用于vector的操作都适用于deque,deque还有push_front(将元素插入到前面)和pop_front(删除最前面的元素)操作的功能。 容器适配器是指将某个底层顺序容器转换为另一种容器。即以顺序容器作为数据存储结构,将其转换为某种特定操作特性的新容器,如queue、priority、queue和stack,从而进一步扩展了容器的应用。本例效果如图14-19所示。 图14-19 利用容器适配器实现栈功能 定义个出栈函数output(),创建栈int_s,使用for循环进行进栈操作,调用output()实现出栈,并输出结果。其代码如下: #include #include #include using namespace std; template void output(T &stackdata) //出栈函数 { while(!stackdata.empty()) { //直到栈为空 cout< stackdata.pop(); //出栈 } cout< } int main(){ stack stack< int,vector for(int i=0;i<8;i++) { int_s.push (i); //进栈 vec_stack.push(i); } cout<<"Pop from int stack:"< output(int_s); //调用出栈函数 cout<<"Pop from vec_stack:"< output(vec_stack); system("pause"); return 0; } stack(栈)是一个经典数据结构,要求数据元素具有FILO的特性,因此要转换为stack的顺序容器必须支持push_back(在容器后端添加元素),又支持pop_back(在容器后端删除元素);而顺序容器中vector、deque和list都符合这些条件,因此都可以通过容器适配器转换为stack。 14.8 本章练习【案例分析】
案例14-3 图书印刷——复制元素并自动输出
【案例描述】
【实现过程】
【案例分析】
案例14-4 学生成绩转换为标准分(向量增长)
【案例描述】
【实现过程】
【案例分析】
案例14-5 打印容器元素的值
【案例描述】
【实现过程】
【案例分析】
案例14-6 一个简单的学生管理系统
【案例描述】
【实现过程】
【案例分析】
案例14-7 编写一个简单的计算器
【案例描述】
【实现过程】
【案例分析】
案例14-8 一个经典的压缩算法
【案例描述】
【实现过程】
【案例分析】
案例14-9 自定义函数实现数据压缩
【案例描述】
【实现过程】
【案例分析】
案例14-10 自定义函数实现数据解压
【案例描述】
【实现过程】
【案例分析】
案例14-11 高效倍增算法
【案例描述】
【实现过程】
【案例分析】
案例14-12 推箱子
【案例描述】
【实现过程】
【案例分析】
案例14-13 队列镜像
【案例描述】
【实现过程】
【案例分析】
案例14-14 反暴力破解
【案例描述】
【实现过程】
【案例分析】
案例14-15 插队(在容器中部插入元素)
【案例描述】
【实现过程】
【案例分析】
案例14-16 模拟文件资源管理系统
【案例描述】
【实现过程】
【案例分析】
案例14-17 反向输出
【案例描述】
【实现过程】
【案例分析】
案例14-18 获取列队头尾
【案例描述】
【实现过程】
【案例分析】
案例14-19 利用容器适配器实现栈功能
【案例描述】
【实现过程】
【案例分析】