0x71 C++ STL

0x71 C++ STL

本节介绍STL中vector,queue,priority_queue,deque,set,multiset,map,bitset八种算法竞赛比较常用的容器。另外,我们也会介绍algorithm头文件中包含的部分函数。

1.vector

vector可理解为变长数组,它的内部实现基于倍增思想。按照下列思路可以大致实现一个vector:设 n , m n,m n,m为vector的实际长度和最大长度。向vector加入元素前,若 n = m n=m n=m,则在内存中申请 2 m 2m 2m的连续空间,并且把内容转移到新的地址上(同时释放旧的空间),再执行插入。从vector中删除元素后,若 n ≤ m / 4 n\leq m/4 nm/4,则释放一半的空间。

vector支持随机访问,即对于任意的下标 0 ≤ i < n 0\leq i<n 0i<n,可以像数组一样用 [ i ] [i] [i]取值。但它不是链表,不支持在任意位置 O ( 1 ) O(1) O(1)插入。为了保证效率,元素的增删一般应该在末尾进行。

声明

#include<vector>    //头文件
vector<int> a;      //相当于一个长度动态变化的int数组
vector<int> b[233]; //相当于第一维长为233,第二维长度动态变化的int数组
struct rec{...};  
vector<rec> c;      //自定义数据类型也可以保存在vector中

size/empty

size函数返回vector的实际长度(包含的元素个数),empty函数返回一个bool类型,表明vector是否为空。二者的时间复杂度都是 O ( 1 ) O(1) O(1)

所有的STL容器都支持这两个方法,含义也相同,之后便不再重复给出。

clear

clear函数把vector函数清空。

迭代器

迭代器就像STL容器的“指针”,可以用“*”操作符解除引用。

一个保存int的vector的迭代器声明方法为:

vector<int>::iterator it; 

vector的迭代器是“随机访问迭代器”,可以把vector的迭代器与一个整数相加减,其行为和指针的移动相似。可以把vector的两个迭代器相减,其结果也和指针相减类似,得到两个迭代器对应下标之间的距离。

begin/end

begin函数返回指向vector中第一个元素的迭代器。例如 a a a是一个非空的vector,则 ∗ a . b e g i n ( ) *a.begin() a.begin() a [ 0 ] a[0] a[0]的作用相同。

所有的容器都可以视作一个“前闭后开”的结构,end函数返回vector的尾部,即第 n n n个元素再往后的“边界”。 ∗ a . e n d ( ) *a.end() a.end() a [ n ] a[n] a[n]都是越界访问,其中 n = a . s i z e ( ) n=a.size() n=a.size()。下面两份代码都遍历了vector<int>a,并输出它的所有元素。

for(int i=0;i<a.size();++i)
	cout<<a[i]<<endl;
for(vector<int>::iterator it=a.begin();it!=a.end();++it)
    cout<<*it<<endl;

front/back

front函数返回vector的第一个元素,等价于 ∗ a . b e g i n ( ) *a.begin() a.begin() a [ 0 ] a[0] a[0]

back函数返回vector的最后一个元素,等价于 ∗ − − a . e n d ( ) *--a.end() a.end() a [ a . s i z e ( ) − 1 ] a[a.size()-1] a[a.size()1]

push_back/pop_back

a . p u s h _ b a c k ( x ) a.push\_back(x) a.push_back(x)把元素 x x x插入到vector a的尾部。

a . p o p _ b a c k ( ) a.pop\_back() a.pop_back()删除vector a的最后一个元素。

//用vector代替邻接表保存有向图
const int MAX_EDGES=100010;
vector<int> ver[MAX_EDGES],edge[MAX_EDGES];
//保存从x到y权值为z的有向边
void add(int x,int y,int z)
{
    ver[x].push_back(y);
    edge[x].push_back(z);
}
//遍历从x出发的所有边
for(int i=0;i<ver[x].size();++i)
{
    int y=ver[x][i],z=edge[x][i];
    //有向边(x,y,z)
}

2.queue

头文件queue主要包括循环队列queue和优先队列priority_queue两个容器。

声明

queue<int> q;
struct rec{...}; queue<rec> q;
priority_queue<int> q;
prioriry_queue<pair<int,int>> q;

循环队列queue

方法描述示例时间复杂度
push入队(从队尾)q.push(element);O(1)
pop出队(从队头)q.pop();O(1)
front队头元素int x=q.front();O(1)
back队尾元素int y=q.back();O(1)

可以回顾0x21节“树与图的遍历”的末尾“图的广度优先遍历”部分,或者0x61节“最短路”的“SPFA算法”部分,找到queue的运用实例。

优先队列priority_queue

priority_queue可以理解为一个大根二叉堆。

方法描述示例时间复杂度
push把元素插入堆q.push(x);O(logn)
pop删除堆顶元素q.pop();O(logn)
top查询堆顶元素(最大值)int x=q.top();O(1)

重载“<”运算符

priority_queue中存储的元素类型必须定义“小于号”,较大的元素会被放在堆顶。内置的 i n t , s t r i n g int,string int,string等类型本身就可以比较大小。若使用自定义的结构体类型,则需要重载“<”运算符。

例如下面的poi结构体保存了二维平面上点的编号和坐标,比较大小时,先比横坐标,再比纵坐标,并且考虑了精度误差:

struct poi{int id;double x,y;};
const double eps=1e-8;

bool operator<(const poi &a,const poi &b){
    return a.x+esp<b.x||a.x<b.x+eps&&a.y<b.y;
}

priority_queue实现小根堆

priority_queue实现小根二叉堆的方法一般有两种。

对于int等内置数值类型,可以把要插入的元素的相反数放入堆中。等从堆中取出元素时,再把它取相反数变回原来的元素。这就相当于把小的放在堆顶。

更为通用的方法是,建立自定义结构体类型,重载“小于号”,但是当做“大于号”来编写函数,例如:

struct rec{int id;double value;};
bool operator <(const rec &a,const res &b){
    return a.value>b.value;
}

这样priority_queue会认为“大”的更“小”,“小”的更“大”,从而实现大根堆,value较小的rec元素会被放在堆顶。

懒惰删除法

0x17节讲解的“二叉堆”支持删除操作,但STL的“优先队列”却不支持删除堆中任意元素,这给我们带来了很多不便。

懒惰删除法(又称延迟删除法)就是一种应对策略。当遇到删除操作时,仅在优先队列之外做一些特殊的记录(例如记录元素的最新值),用于辨别那些堆中尚未清除的“过时”元素。从堆顶取出最值时,再检查“最值元素”是不是“过时”的,若是,则重新取出下一个最值。换言之,元素的“删除”被延迟到堆顶进行。

我们仍然以0x61节“最短路”中“堆优化的Dijkstra算法”的参考程序为例:

当一个节点的距离值 d i s t [ y ] dist[y] dist[y]被更新时,暂时不从优先队列中删除 y y y对应的元素,而是直接把新的二元组 ( − d i s t [ y ] , y ) (-dist[y],y) (dist[y],y)插入优先队列。

当从堆顶取出二元组 ( v a l , x ) (val,x) (val,x)准备扩展时,再检查 d i s t [ x ] dist[x] dist[x]是否等于 v a l val val。如果不相等,那么说明 ( v a l , x ) (val,x) (val,x)这个元素是被删除过的, ( d i s t [ x ] , x ) (dist[x],x) (dist[x],x)才是最新插入的。此时直接继续循环,取出下一个堆顶。

当然,因为在无负权的图上,每一个节点第一次被取出时,就已经得到了它的最短路,所以每个节点只需要扩展一次。因此,我们没有执行上述检查,而是用一个bool数组进行了“是否扩展过”的标记,其本质还是懒惰删除法的应用。

3.deque

双端队列deque是一个支持在两端高效插入或删除元素的连续线性存储空间。它就像是vector和queue的结合。与vector相比,deque在头部增删元素仅需要 O ( 1 ) O(1) O(1)的时间;与queue相比,deque像数组一样支持随机访问。

方法描述示例时间复杂度
[]随机访问与vector类似O(1)
begin/enddeque的头/尾迭代器与vector迭代器类似O(1)
front/back队头/队尾元素与deque类似O(1)
push_ back从队尾入队q.push_back(x);O(1)
push_front从队头入队q.push_front(y);O(1)
pop_front从队头出队q.pop_front();O(1)
pop_back从队尾出队q.pop_back();O(1)
clear清空队列q.clear();O(n)

4.set

头文件set主要包括set和multiset两个容器,分别是“有序集合”和“有序多重集合”,即前者的元素不能重复,而后者可以包含若干个相等的元素。set和multiset的内部实现是一颗红黑树(平衡树的一种),它们支持的函数基本相同。

声明

set<int> s;
struct rec{...}; set<rec> s;
multiset<double> s;
//有优先队列一样,set和multiset存储的元素必须定义“小于号”运算符

size/empty/clear

与vector类似,分别为元素个数、是否为空、清空。前两者的时间复杂度为 O ( 1 ) O(1) O(1)

迭代器

set和multiset的迭代器称为“双向访问迭代器”,不支持“随机访问”,支持星号(*)解除引用,仅支持“++”和“- -”两个与算术相关的操作。

设it是一个迭代器,例如set<int>::iterator it;

若把it++,则it会指向“下一个”元素。这里的“下一个”是元素从小到大排序的结果中,排在it下一名的元素。同理,若把it- -,则it将会指向排在“上一个”的元素。

请注意,执行“++”和“- -”操作的时间复杂度都是 O ( l o g n ) O(logn) O(logn)。执行操作前后,务必仔细检查,避免迭代器指向的位置超出首、尾迭代器之间的范围。

begin/end

返回集合的首、尾迭代器,时间复杂度为 O ( 1 ) O(1) O(1)

s.begin()是指向集合中最小元素的迭代器。

s.end()是指向集合中最大元素的下一个位置的迭代器。换言之,就像vector一样,是一个“前闭后开”的形式。因此,- -s.end()是指向集合中最大元素的迭代器。

insert

s.insert(x)把一个元素 x x x插入到集合 s s s中,时间复杂度为 O ( l o g n ) O(logn) O(logn)

在set中,若元素已存在,则不会重复插入该元素,对集合的状态无影响。

下面的代码把 n n n个整数插入有序多重集multiset,并从小到大输出,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),相当于进行了一次排序。假设 n n n个整数目前储存在数组 a [ 1 ∼ n ] a[1\sim n] a[1n]中。

multiset<int> s;
for(int i=1;i<=n;++i) s.insert(a[i]);
for(multiset<int>::iterator it=s.begin();it!=s.end();++it)
    cout<<*it<<endl;

find

s.find(x)在集合 s s s中查找等于 x x x的元素,并返回指向该元素的迭代器。若不存在,则返回s.end()。时间复杂度为 O ( l o g n ) O(logn) O(logn)

low_bound/upper_bound

这两个函数的用法与find类似,但查找的条件略有不同,时间复杂度为 O ( l o g n ) O(logn) O(logn)

s.lower_bound(x)查找 ≥ x \geq x x的元素中最小的一个,并返回指向该元素的迭代器。

s.upper_bound(x)查找 > x > x >x的元素中最小的一个,并返回指向该元素的迭代器。

erase

设it是一个迭代器,s.erase(it)从s中删除迭代器it指向的元素,时间复杂度为 O ( l o g n ) O(logn) O(logn)

x x x是一个元素,s.erase(x)从s中删除所有等于 x x x的元素,时间复杂度为 O ( k + l o g n ) O(k+logn) O(k+logn),其中 k k k为被删除的元素个数。

如果想从multiset中删掉至多一个等于 x x x的元素,可以执行:

if((it=s.find(x))!=s.end()) s.erase(it);

count

s.count(x)返回集合s中等于 x x x的元素个数,时间复杂度为 O ( k + l o g n ) O(k+logn) O(k+logn),其中 k k k为元素 x x x的个数。

5.map

map容器是一个键值对key-value的映射。其内部实现是一颗以key为关键码的红黑树。map的key和value可以是任意类型,其中key必须定义“小于号”运算符,声明方法为:

//map<key_type,value_type> name;
map<long long,bool> vis;
map<string,int> hash;
map<pair<int,int>,vecor<int> > test;

在很多时候,map容器被当做Hash表使用,建立从复杂信息key(如字符串)到简单信息value(如一定范围内整数)的映射。

因为map基于平衡树实现,所以它的大部分操作的时间复杂度都在 O ( l o g n ) O(logn) O(logn)级别,略慢于使用Hash函数实现的传统Hash表。从C++11开始,STL中新增了unordered_map等基于Hash的容器。

size/empty/clear/begin/end

与set类似,分别为元素个数、是否为空、清空、首迭代器、尾迭代器。

迭代器

map的迭代器与set一样,也是“双向访迭代器”。对map的迭代器解除引用后,将得到一个二元组pair<key_type,value_type>

insert/erase

与set类似,分别为插入、删除。insert的参数是pair<key_type,value_type>,erase的参数可以是pair或者迭代器。

map<int,int> h;
h.insert(make_pair(1,2)),h.insert({2,3});
map<int,int>::iterator it=h.begin();
pair<int,int> p=*it;
h.erase(it),h.erase(make_pair(2,3));
cout<<p.first<<' '<<p.second<<endl;

find

h.find(x)在变量名为h的map中查找key为x的二元组,并返回指向该二元组的迭代器。若不存在,返回h.end()。时间复杂度为 O ( l o g n ) O(logn) O(logn)

[]操作符

h[key]返回key映射到value的引用,时间复杂度为 O ( l o g n ) O(logn) O(logn)

[]操作符是map最吸引人的地方。我们可以很方便地通过h[key]来得到key对应的value,还可以对h[key]进行赋值操作,改变key对应的value。

需要特别注意的是,若查找的key不存在,则执行h[key]后,h会自动新建一个二元组(key,zero),并返回zero的引用。这里的zero是一个广义“零值”,如整数0、空字符串等。如果查找之后不对h[key]进行赋值,那么时间一长,h会包含很多无用的“零值二元组”,白白地占用了空间,降低了程序运行效率。强烈建议在使用[]操作符查询之前,先用find方法检查key的存在性。

6.bitset

bitset可看做一个多位二进制数,每8位占用一个字节,相当于采用了状态压缩的二进制数组,并支持基本的位运算。在估算程序运行时间时,我们呢一般以32位整数的运算次数为基准,因此 n n n位bitset执行一次位运算的复杂度可视为 n / 32 n/32 n/32,效率较高。

声明

bitset<10000> s;
//表示一个10000位二进制数,<>中填写位数。下面把位数记为n。

位运算操作符

~s:返回对bitset s按位取反的结果。

&,|,^:返回对两个位数相同的bitset执行按位与、或、异或运算的结果。

>>,<<:返回把一个bitset右移、左移若干位的结果。

==,!=:比较两个bitset代表的二进制数是否相等。

[]操作符

s[k]表示s的第k位,既可以取值,也可以赋值。

在10000位二进制数中,最低位为 s [ 0 ] s[0] s[0],最高位为 s [ 9999 ] s[9999] s[9999]

count

s.count()返回有多少位为1。

any/none

若s所有位都为0,则s.any()返回false,s.none()返回true。

若s至少一位为1,则s.any()返回true,s.none()返回false。

s.any()返回是否存在一个1,s.none()返回是否不存在1。

set/reset/flip

s.set()把s所有位变为1。

s.set(k,v)把s的第k位改为v,即s[k]=v。

s.reset()把s所有位变为0。

s.reset(k)把s的第k位改为0,即s[k]=0。

s.flip()把s的所有位取反,即s=~s。

s.flip(k)把s的第k位取反,即s[k]^=1。

7.algorithm

下面介绍的几个函数都作用在序列上,接受两个迭代器(或指针) l , r l,r l,r,对下标处于前闭后开区间 [ l , r ) [l,r) [l,r)中的元素执行一系列操作。

reverse翻转

reverse(a.begin(),a.end()); //翻转一个vector
reverse(a+1,a+n+1);      //翻转一个数组,元素存放在下标1~n

unique去重

返回去重之后的尾迭代器(或指针),仍然为前闭后开,即这个尾迭代器是去重之后末尾的下一个位置。该函数常用于离散化,利用迭代器(或指针)的减法,可计算出去重后的元素个数 m m m

int m=unique(a.begin(),a.end())-a.begin(); //把一个vector去重
int m=unique(a+1,a+n+1)-(a+1); //把一个数组去重,元素放在下标1~n

random_shuffle随机打乱

用法与reverse相同

next_permutation下一个排列

把两个迭代器(或指针)指定的部分看做一个排列,求出这些元素构成的全排列中,字典序排在下一个的排列,并直接在序列上更新。另外,若不存在排名更靠后的排列,则返回false,否则返回true。同理,也用prev_permutation函数。

下面的程序按字典序输出1~n的n!种全排列:

for(int i=1;i<=n;++i) a[i]=i;
do{
    for(int i=1;i<n;++i) cout<<a[i]<<' ';
    cout<<a[n]<<endl;
}while(next_permutation(a+1,a+n+1));

sort快速排列

对两个迭代器(或指针)指定的部分进行快速排序。可以在第三个参数传入定义大小的函数,或者重载“小于号”运算符。

把一个int数组(元素存放在下标1~n)从大到小排序,传入比较函数:

int a[MAX_SIZE];
bool cmp(int a,int b){return a>b};
sort(a+1,a+n+1,cmp);

把自定义的结构体vector排序重载“小于号”运算符:

struct rec{int id,x,y;};
vector<rec> a;
bool operator <(const rec &a,const rec &b)
{
    return a.x<b.x||a.x==b.x&&a.y<b.y;
}
sort(a.begin(),a.end());

lower_bound/upper_bound二分

lower_bound的第三个参数传入一个元素x,在两个迭代器(或指针)指定的部分上执行二分查找,返回指向第一个大于等于x的元素的位置的迭代器(或指针)。

upper_bound的用法和lower_bound大致相同,唯一的区别是查找第一个大于x的元素。当然,两个迭代器(或指针)指定的部分应该是提前排好序的。

//在有序int数组(元素放在下标1~n)中查找大于等于x的最小整数的下标
int i=lower_bound(a+1,a+n+1,x)-a;
//在有序vector<int>中查找小于等于x的最大整数(假设一定存在)
int y=*--upper_bound(a.begin(),a.end(),x);

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷神星ceres

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值