标签:5.2 STL 元素 初步 int 集合 队列 include ID
5.2 STL初步
这一章节我们主要介绍C++的标准模板库。
5.2.1 排序与检索
关于sort的内容,我在4.2 函数调用与参数传递已经介绍的非常详细了。
事实上由于sort是一个模板函数,sort是可以对任意对象(不一定是内置类型)进行排序的。
5.2.2 不定长数组 vector
vector是一个封装了动态大小(不固定)数组的顺序容器,它能够存放任意类型的对象。不仅如此,它还将一些常用的操作封装在了vector类型的内部。
首先我们看一下怎么声明一个vector:
//vector<变量类型名>变量名
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
int main(){
vector<int> a;//这类似于int a[]
return 0;
}
需要注意到vector是一个模板类,所以需要这样的方式去声明。vector中封装的函数见此(具体的内容我们接下来一边做题一边分析):vector 容器浅析
木块问题 (UVA 101)
从左到右n个木块,编号为0~n-1,要求模拟以下四种操作:
move a onto b:将a和b上方的木块全部归位,然后把a放在b上
move a over b:把a上方的木块全部归位,然后将a放在b所在的木块堆的顶部
pile a onto b:把b上方的木块全部归位,然后把a及上面的木块整体放在b上
pile a over b:把a及a上方的木块全部放在b所有木块堆的顶部
输入一堆指令,如果a和b为同一堆的即为非法指令,即忽略。所有操作结束后输出每个位置的木块列表,按照从底部到顶部的顺序排列。
分析:显然又是一道模拟题,这里我们用vector去保存每个木块堆的高度进行处理:
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int maxn=30;
int n,a,b;
vector<int>pile[maxn];//表示多个木块堆
int main(){
cin>>n;
for (int i=0;i<n;i++) pile[i].push_back(i);
return 0;
}
这里push_back表示在这个vector的尾部加入一个数据。这里的意思是在每一个pile后面添加一个i的值。(pop_back()即为删除尾部元素的意思)
接下来我们看这个问题,首先我们需要一个函数得到当前指令的木块现在在这个木块堆的哪一个堆里:
void find_block(int a,int &p,int &q){//如果不明白这里为什么用引用的再好好看一看上一章
for (p=0;p<n;p++){
for (q=0;q<pile.size();q++) if (pile[p][q]==a) return;
}
}
这里用了一个.size(),即表示一个vector的大小。
查找得到了指令中木块的当前位置,p为所在堆,q为所在高度。如果两个木块的所在堆相同,即直接跳过这个指令。接下来观察这四个指令,我们可以发现,这四个指令其实是由以下两个简单命令合成的:
1.将x木块上方的木块全部归位
2.把x木块上的所有木块整体移动到另一个堆
接下来我们编译这两个简单命令的函数:
void clear_above(int p,int h){
for (int i=h+1;i<pile[p].size();i++){
int b=pile[p][i];
pile[b].push_back(b);//放回原堆
}
pile[p].resize(h+1); //pile只保留下标0~h的元素
}
需要注意的是,这里resize不是保留元素的意思,resize(int n,element)表示改变vector的的大小。如果说n小于当前的大小,即截取下标为n以后所有的元素(包括自身),如果n大于当前的大小,即进行扩容,扩充后的多出来的每个元素值为element。
同理:
// 把第p堆高度为h的木块上方的所有木块移动到p2的顶部
void pile_onto(int p,int h,int p2){
for (int i=h;i<pile.size();i++)
pile[p2].push_back(pile[p][i]);//移位
pile[p].resize(h);
}
于是指令的部分即为:
while (cin>>s1>>a>>s2>>b){//输入指令
int pa,ha,pb,hb;//两个木块当前所在的堆和高度
find_block(a,pa,ha);
find_block(b,pb,hb);
if (pa==ba) continue;
if (s2=="onto") clear_above(pb,hb)
if (s1=="move") clear_above(pa,ha)
pile_onto(pa,ha,pb);
}
其实这道题最重要的是要知道,当我们a上面的木块全部归位,然后把a放在别的木块上时,即为把a及a上面的木块放在别的木块上
5.2.3 集合 set
set声明的方式和vector是一样的:
#include<cstdio>
#include<iostream>
#include<set>
using namespace std;
int main(){
set<int> a;//这类似于int a[]
return 0;
}
这里我们还是看例题进行说明:
安迪的第一个字典 (UVA 10815)
输入一个文本,找出所有不同的单词(连续的字母序列,按字典序从小到大输出,单词全部转为小写)
分析:set作为容器正好契合这道题的属性,因为set中的每个元素最多出现过一次且无法更改,set还将这些元素自动进行了排序。于是我们只需要将这些字符串中的每一个单词全部转为小写然后添加到set中即可得到结果。
代码如下:
#include<cstdio>
#include<iostream>
#include<set>
#include<string>
using namespace std;
set<string>dict;
int main(){
string s;
while (cin>>s){
//将字符串里面所有的英文字母转化为小写,需要注意的是这个文本里面可能出现标点符号和单词连在一起
for (int i=0;i<s.size();i++) if (isalpha(s[i])) s[i]=tolower(s[i]); else s[i]=' ';
dict.insert(s);
}
return 0;
}
这是我自己写的代码,书上在这里用到了一些不必要的操作(大概)。
tolower(字符)表示将括号里的字母字符转为小写字母字符 (转化为大写字母字符为toupper)
.insert()即为向集合里插入一个值。
这道题比较关键的是输出的代码:
for (set<string>::iterator i=dict.begin();i!=dict.end();i++) cout<<*i<<endl;
这里的begin()和end()我们在第一章就有用到,那么这个函数到底表示什么含义,返回的又是什么呢?(你在这里直接输出i是会报错的)
begin()指向了变量的第一个元素,end()指向的是变量最后一个元素的下一个元素,注意是下一个元素,并且是指向。
我看有的博客上面介绍这两个函数返回的值是指针,事实上不是的(如果是指针你应该打印的下来)。这两个函数返回的是一个迭代器。
那么迭代器是什么呢?(python里面也有哦)
迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。(迭代器和指针的区别)
那我们如何定义一个迭代器和一个反向迭代器呢?
容器类名::iterator 迭代器名;//正向
容器类名::reverse_iterator 迭代器名;//反向
通过迭代器可以读取它指向的元素,*迭代器名就表示迭代器指向的元素。
迭代器都可以进行++和- -的操作。反向迭代器和正向迭代器的区别就在于:
对正向迭代器进行++操作时,迭代器会指向容器中的后一个元素;
而对反向迭代器进行++操作时,迭代器会指向容器中的前一个元素。
(- -同理。)
这里再介绍set里面内置的几个集合操作:这几个函数是在algorithm头文件里的
set_union:
set_union(A.begin(),A.end(),B.begin(),B.end(),inserter(C,C.begin()));
这里表示将A集合和B集合取合集放到C集合里。
set_intersection:
这个函数即代表两个集合(集合的部分)的交集放到另一个集合里。
set_difference:
这个是差集啦。
5.2.4 映射 map
map即为健(key)到值(value)的映射 (python里面的列表),定义的方法如下:
map<string,int>month_name
month_name["July"]=7这样的方式进行赋值
有木有感觉和python里面的列表非常相似啊。
继续看题:
反片语 (UVA 156)
这道题的输入和上一道题一样,需要按字典序输出满足如下条件的单词:该单词不能通过字母重排,得到输入的文本中的另外一个单词。在判断是否满足条件是,字母不分大小写,但在输出时不改变原本输入的大小写。
分析:首先我在这里介绍一下set和map比较重要的几个工具。
insert之前在set部分已经介绍过了。在map中插入一个元素:map变量名.insert(pair<int, string>(key, value))。
student_id.insert(pair<string,int>(s,i*10));
也可以用这样的方式进行插入:
student_id[string]=int;
student_id[s]=i*10;
但需要注意的是,用insert函数插入数据,在数据的插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的。( 不会报错,但语句不会生效)
可以明显发觉两种编译方式的差别:
注意这里还可以用一下的方式进行取值:
id_student.at(key);
id_student.at(12);
如果没有取到,不会报错,但会出现如下的情况:
那如果想要在指定的位置进行插入呢?
//在insert括号里的第一个加一个参数表示位置即可,注意这个参数的类型是迭代器哦
id_student.insert(id_student.begin(),pair<int,string>(12,"I love you"));
.empty(),表示这个容器是否为空(这个操作其他容器也通用):
id_student.empty()
如果为空,则返回1,否则返回0.
那我们如何对这个容器中原有的元素进行删除?
容器名.erase(迭代器)
//这是表示删除该迭代器指向的单个元素
容器名.erase(迭代器1,迭代器2)
//这是表示删除该迭代器1到迭代器2指向的多个元素
容器名.erase(key)
//表示删除该key值的键值对
这里我们还顺便测试了一下,insert默认将元素插入在了容器的什么位置。
添加和删除都有了,接下来我们介绍查找:
容器名.find(key)
注意这里返回的值,如果找到了则返回对应的迭代器,如果没找到则返回end()。
还有一种操作也可以做到查找的作用:
容器名.count(key)
这个操作表示得到key在这个容器里出现的次数(这个函数在其他容器也是通用的,由于map带有集合的性质,不会出现两个相同的key,于是count返回的值只会为1和0)
那我们该如何遍历map容器里的所有值呢?
for(map<int, string>::iterator iter = mapStudent.begin(); iter != mapStudent.end(); iter++)
cout<<iter->first<<" "<<iter->second<<endl;
这个和之前的vector是一样的。
那对于刚刚上面的题目,我们其实已经搞定了大半,只需要将每个输入进去的单词标准化(每个字母转化为小写字母后再从小到大进行排序)。然后将标准化的单词添加进map作为map的key,value即为出现的次数,然后通过value的值判断是否为我们需要的单词即可。
单词“标准化”:
string reps(const string& s){
string ans;
for (int i=0;i<s.size();i++) ans[i]=tolower[s[i]];
sort(ans.begin(),ans.end());
return ans;
}
建立映射
string s;
vector<string>words;
map<string,int>reps_num;
while (cin>>s){
words.push_back(s);//被检验的单词部分
string r=reps(s);
if (!reps_num.count(r)) reps_num[r]=0;//如果是第一次出现的字母格数,则添加一对映射
reps_num[r]++;
}
接下来,就只要遍历一遍vector中的单词,然后在map中num为1(只出现过一次)即可。剩下的代码大家自行编译完成啦。
5.2.5 栈
首先介绍一下什么是栈:
栈是一种符合“后进先出,先进后出”线性存储结构。(有的书上写的是先进后出,有的书上写的是先出后进,这两个本质是一样的)
允许元素插入与删除的一端称为栈顶,另一端称为栈底。限定只能在栈顶进行插入和删除的操作。
栈的常用操作分别为:pop(弹栈),push(压栈),栈的大小(size),判断是否为空(empty),和获取栈顶的值(top)
stack<int>s;
s.pop(); //弹出栈顶元素, 但不返回其值
s.push(); //将元素压入栈顶
s.size(); //返回栈中元素的个数
s.empty(); //如果栈为空则返回true, 否则返回false;
s.top(); //返回栈顶元素, 但不删除该元素
学习了上面的这些,我们就可以看书上的例题了 (大概)
集合栈计算机 (UVA 12096)
分析:这道题的集合不是整数或者说字符串集合,而是集合的集合(如果我没记错的话,这个集合的方法可以用来定义自然数)。然后我们得到的输出结果就是栈顶元素(集合)里的元素(集合)个数。于是这里考虑对于每个不同的集合我们给它分配一个ID,即用一个map做一个set到int的映射,然后用一个vector放集合:
#include<map>
#include<set>
#include<stack>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
typedef set<int> Set;//这个就理解为将set<int>用Set代替了即可
map<Set,int>Set_ID;//集合到ID的映射
vector<Set>SetX;//通过ID找集合
int main(){
stack<int>s;//题目中的栈,注意我们这里是用ID建立的栈
int n; cin>>n;
for (int i=1;i<=n;i++){
string op; cin>>op;
}
}
接下来考虑四种不同的指令:
PUSH:空集合“{ }”入栈。
DUP:把当前栈顶元素赋值一份再入栈。
UNION:出栈两个集合,然后把两者的并集入栈。
ADD:出栈两个集合,然后把先出栈的集合加入后出栈的集合中,把结果入栈。
其实光看这四个指令操作起来还是非常简单的,我们模拟一下:
string op; cin>>op;
if (op[0]=='P') s.push(ID(Set()));
//ID表示将集合转化为ID,Set()表示建立了一个空集合
else if (op[0]=='D') s.push(s.top());//复制栈顶的元素再入栈
else{
//出栈两个元素,并找到他们对应的集合
Set x1=SetX[s.top()];s.pop();
Set x2=SetX[s.top()];s.pop();
Set x;//x为入栈ID对应的新集合
//并集和交集
if (op[0]=='U') set_union(x1.begin(),x1.end(),x2.begin(),x2.end(),inserter(x,x.begin()));
if (op[0]=='I') set_intersection(x1.begin(),x1.end(),x2.begin(),x2.end(),inserter(x,x.begin()));
//将x1当作一个集合加入x2
if (op[0]=='A') {x=x2;x.insert(ID(x1));}
s.push(ID(x));
}
//需要输出当前栈顶元素的大小
cout<<SetX[s.top()].size()<<endl;
还差一个将集合转化为ID的函数:
//查找给定集合对应的ID,如果是一个新集合则分配一个新ID
int ID(Set x){
if (Set_ID.count(x)) return Set_ID[x];
SetX.push_back(x);//新集合
return Set_ID[x]=SetX.size()-1;//分配ID
}
完整代码如下:
#include<map>
#include<set>
#include<stack>
#include<string>
#include<vector>
#include<iostream>
#include <algorithm>
using namespace std;
typedef set<int> Set;//这个就理解为将set<int>用Set代替了即可
map<Set,int>Set_ID;//集合到ID的映射
vector<Set>SetX;//通过ID找集合
//查找给定集合对应的ID,如果是一个新集合则分配一个新ID
int ID(Set x){
if (Set_ID.count(x)) return Set_ID[x];
SetX.push_back(x);//新集合
return Set_ID[x]=SetX.size()-1;//分配ID
}
int main(){
stack<int>s;//题目中的栈,注意我们这里是用ID建立的栈
int n; cin>>n;
for (int i=1;i<=n;i++){
string op; cin>>op;
if (op[0]=='P') s.push(ID(Set()));
//ID表示将集合转化为ID,Set()表示建立了一个空集合
else if (op[0]=='D') s.push(s.top());//复制栈顶的元素再入栈
else{
//出栈两个元素,并找到他们对应的集合
Set x1=SetX[s.top()];s.pop();
Set x2=SetX[s.top()];s.pop();
Set x;//x为入栈ID对应的新集合
//并集和交集
if (op[0]=='U') set_union(x1.begin(),x1.end(),x2.begin(),x2.end(),inserter(x,x.begin()));
if (op[0]=='I') set_intersection(x1.begin(),x1.end(),x2.begin(),x2.end(),inserter(x,x.begin()));
//将x1当作一个集合加入x2
if (op[0]=='A') {x=x2;x.insert(ID(x1));}
s.push(ID(x));
}
//需要输出当前栈顶元素的大小
cout<<SetX[s.top()].size()<<endl;
}
return 0;
}
事实上这道题最重要的思想即是建立集合和ID之间的双射关系,能建立这个,这道题还是比较好解决的。(我瞎说的)
5.2.5 队列
队列(queue)是一种满足“先进先出”的线性储存结构,允许元素插入的一端称为队尾,允许元素删除的一端称为队头。
队列的基本操作如下:
q.empty() 如果队列为空返回true,否则返回false
q.size() 返回队列中元素的个数
q.pop() 删除队列首元素但不返回其值
q.front() 返回队首元素的值,但不删除该元素
q.push() 在队尾压入新元素
q.back() 返回队列尾元素的值,但不删除该元素
以上是关于队列最基本的知识,关于循环队列,链式队列的内容请参考:C++ 数据结构队列
团队队列 (UVA 540)
就是给你t个团队,团队中的每一个人有着属于自己独特的编号。t个团队的很多人一起在排个长队,这个排队遵循这样的规则:如果他有队友再排队,那么这个人可以插到最后一个队友的身边,如果没有,则他会排到长队的对位。
有三种指令:
ENQUEUE x:编号为x的进入长队
DEQUEUE:长队的队首出队(这个要输出对应编号的)
STOP 停止模拟
分析:这道题看起来就比前一道题和善太多了。每个团队为一个队列,然后团队整体又形成一个队列。我们将整个排队队列分解成三个团队的队列(因为我们可以比较简单的得到,每个团队的人在一个完整的排队队列里是成一个单独的队列),再用一个队列去纪律这三个团队的顺序。
代码如下:
#include<queue>
#include<iostream>
#include<map>
using namespace std;
int main(){
int t;
map<string,int>TEAM_ID;
cin>>t;
for (int i=0;i<t;i++){
int n; string x;
cin>>n;
for (int j=1;j<=n;j++){cin>>x;TEAM_ID[x]=i;}//编号为x的人所在的团队编号为i
}
queue<int>q;//q为团队的队列
queue<string>qq[20];//qq[i]为团队i的队列,多个队列拼接在一起就是一个完整的排队队列
while(true){
string ss,xx;
cin>>ss;
if (ss[0]='S') break;//STOP指令
else if (ss[0]=='D'){//DEQUEUE指令
int t=q.front();
cout<<qq[t].front()<<endl;//这里出第一个团队里的第一个人,即为整个队列的第一个人
qq[t].pop();//出队
if (qq[t].empty()) q.pop();//团队全体出队,下次来要重排啦
}
else if (ss[0]=='E'){
cin>>xx;
int id=TEAM_ID[xx];
if (qq[id].empty()) q.push(id);//表示某团队进入队列
qq[id].push(xx);//某团队的某个人进入了排队队列
}
}
return 0;
}
5.2.5 优先队列
优先队列(priority_queue)是一种类似于队列的抽象数据类型(Abstract Data Type,ADT)。
他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面优先出队。
基本操作如下:(优先队列的操作我都没有验证过,可能会出现意想不到的效果,建议大家实验过后使用):
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容
注意priority_queue的定义方式和之前的方法可能不太一样:
priority_queue<类型名,实现容器,cmp>
这里的cmp,可以用greater< 类型名 >或者less< 类型名 >,分别表示升序队列和降序队列,如果不传入cmp默认的为大顶堆(降序队列),不传入实现容器的话默认类型的是vector。(注意右边的两个箭头不要连在一起,最新的编译器好像已经修改过这个问题了)
//升序队列
priority_queue <int,vector<int>,greater<int> >q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;
这个cmp我们也是可以进行自定义的:
struct cmp{//C++中重载了“()”运算符的类和结构体被称作仿函数
bool operator() (const 变量1,const 变量2)const(书上加的){
return 规则;
}
}
书上在那里加了一个const,如果有知道为什么这样做的人麻烦告诉我一下。
丑数 (UVA 136)
丑数是指不能被除了2,3,5以外其他素数整除的数,把丑数从小到大进行排列,求第n个丑数。
分析:这道题的关键在于了解到对于一个丑数x,2x,3x,5x亦为丑数。我们这里用一个优先数列存放丑数,优先数列的队首为最小的丑数,我们用这个丑数去生成三个可能为丑数的数,然后放入集合进行判断,如果集合里没有这个数,就加入集合和优先队列。
代码如下:
#include<queue>
#include<iostream>
#include<vector>
#include<set>
using namespace std;
//下面的这两行代码其实没有什么特别的实际意义,不写这两行对代码实现没有实际的影响
typedef long long ll;
const int c[3]={2,3,5};
int main(){
priority_queue<ll,vector<ll>,greater<ll> >p;//升序数列p
set<ll>q;
p.push(1);
q.insert(1);
int n;cin>>n;
for (int i=1;;i++){
ll x=p.top();p.pop();//当前最小的丑数即为第i个丑数
if (i==n){cout<<x;break;}
for (int j=0;j<3;j++){
ll xx=x*c[j];
//如果没有出现过这个丑数,就进行添加
if (!q.count(xx)){q.insert(xx);p.push(xx);}
}
}
return 0;
}
5.2.6 测试stl
这一章节主要是介绍如何测试你正在使用的库是否存在bug,那怎么测试呢?就是随机一个数组参与测试,那随机数怎么生成呢?
#include<cstdlib>
rand()表示生成一个随机数
如果想要得到一定范围内的随机数,例如10-99
-->rand()%90+10
但是在随机数的生成前,我们一般加一行:
#include<ctime>
srand( unsigned( time(0) ) );
或者srand(time(NULL));
否则多次测试的为同一套随机数。
这一章的学习真的非常幸苦不过感觉还是比较有收获的(疲倦.jpg)
标签:5.2,STL,元素,初步,int,集合,队列,include,ID 来源: https://blog.csdn.net/weixin_55835175/article/details/115772030
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。