C++标准模板库(STL)介绍
C++中为使用者提供了标准模板库 (Standard Template Library,STL),其中封装了很多相当实用的容器(可以先把容器理解成能够实现很多功能的东西),不需要费力去实现它们的细节而直接调用函数来实现很多功能,十分方便。下面介绍几个常用的容器,读者应当动手实现其中的例子,这对掌握它们的用法大有帮助(其中 queue、priority_queue、stack 可以等阅读到下一章节关于队列和栈的内容时再看)。
vector的常见用法详解
vector翻译为向量,但是这里使用“变长数组”的叫法更容易理解,也即“长度根据需要而自动改变的数组”。在考试题中,有时会碰到只用普通数组会超内存的情况,这种情况使用vector会让问题的解决便捷许多。另外,vector 还可以用来以邻接表的方式储存图,这对无法使用邻接矩阵的题目(结点数太多)、又害怕使用指针实现邻接表的读者是非常友好的,写法也非常简洁。
如果要使用vector,则需要添加vector 头文件,即#include 。除此之外,还需要在头文件下面加上一句“using namespace std;",这样就可以在代码中使用vector了。下面来看vector的一些常用用法。
1.vector的定义
单独定义一个 vector:
vector <typename> name
上面这个定义其实相当于是一维数组name[SIZE],只不过,其长度可以根据需要进行变化,比较节省空间,说通俗了就是“变长数组”。
和一维数组一样,这里的 typename 可以是任何基本类型,例如 int、double、char、结构体等,也可以是STL标准容器,例如vector、set、queue等。需要注意的是,如果typename也是一个STL容器,定义的时候要记得在 >> 符号之间加上空格,因为一些使用C++11之前标准的编译器会把它视为移位操作,导致编译错误。下面是一些简单的例子.
vector<int> name;
vector<double> name;
veetor<char> name;
vector<node> name; //node 是结构体的类型
如果typename 是vector,就是下面这样定义:
vector<vector<int>> name; //>>之间要加空格
可以很容易联想到二维数组的定义,即其中一维是一个数组的数组。那么vector数组也是一样,即 Arrayname[] 中的每一个元素都是一个vector。初学者可以把vector数组当作两个维都可变长的二维数组理解。
然后来看定义vector数组的方法:
vector<typename> Arrayname[arraySize];
例如
vectoz<int> vi[100];
这样 Arrayname[0]-Arrayname[arraySize -1]中每一个都是一个vector 容器。
与vector<vector> name不同的是,这种写法的一维长度已经固定为 arraySize,另一维才是“变长”的(注意体会这两种写法的区别)。
2.vector容器内元素的访问
vector一般有两种访问方式:通过下标访问或通过迭代器访问。下面分别讨论这两种访问方式。
(1) 通过下标访问
和访问普通的数组是一样,对一个定义为vector vi的vector容器来说,直接访问vi[index]即可(如vi[0]、vi[1])。当然,这里下标是从0到vi.size()-1,访问这个范围外的元素可能会运行出错。
(2)通过迭代器访问
迭代器(iterator)可以理解为一种类似指针的东西,其定义是:
vector<typename>::iterator it;
这样it就是一个vector::iterator型的变量 (虽然这个类型看起来很长),其中typename 就是定义vector 时填写的类型。下面是typename为int和double的举例:
vector<int>::iterator it;
vector<double>::iterator it;
这样就得到了迭代器it,并且可以通过*it来访问vector里的元素。
例如,有这样定义的一个vector容器:
vector<int> vi;
for (int i = 1 ; i <=5; i++) { //循环完毕后vi中元素为1 2 34 5
vi.push_beck(i); //push_back(i)在vi.的末尾添加元素1,即依次添加1 23 4 5
}
可以通过类似下标和指针访问数组的方式来访问容器内的元素:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1; i <= 5; i++) {
vi.push_back(i);
} //vi.begin()为取vi 的首元素地址,而it指向这个地址
vector<int>::iterator it = vi.begin();
for (int i = 0; i < 5; i++) {
printf("%d ", *(it + i)); //输出vi[i]
}
return 0;
}
输出结果:
1 2 3 4 5
从这里可以看出vi[i]和*(vi.begin()+i)是等价的。
既然上面说到了 begin()函数的作用为取vi的首元素地址,那么这里还要提到end()函数。和 begin()不同的是,end()并不是取vi的尾元素地址,而是取尾元素地址的下一个地址。end()作为迭代器末尾标志,不储存任何元素。美国人思维比较习惯左闭右开,在这里begin()和end()也是如此。
除此之外,迭代器还实现了两种自加操作:++i和i++(自减操作同理),于是有了另一种遍历vector中元素的写法:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1; i <= 5; i++) {
vi.push_back(i);
}
//vector 的迭代器不支持it < vi.end()写法,因此循环条件只能用it != vi.end()
for (vector<int>::iterator it = vi.begin(); it != vi.end(); it++) {
printf("%d ", *it);
}
return 0;
}
输出结果:
1 2 3 4 5
最后需要指出,在常用STL容器中,只有在vector和string中,才允许使用vi.begin()+3这种迭代器加上整数的写法。
3. vector常用函数实例解析
(1) push_back()
顾名思义,push back(x)就是在vector后面添加一个元素X,时间复杂度为O(1)。
示例如下
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1 ; i <= 3 ; i++) {
vi.push_back(i); //将1、2、3依次插入vi末尾
}
for (int i = 0; i < vi.size(); i++) { //size()函数会给出vi 中元素的个数
printf("%d ", vi[i]);
}
return 0;
}
输出结果:
123
(2) pop_back()
有添加就会有删除,pop_back()用以删除vector的尾元素,时间复杂度为O(1)。
示例如下:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1 ; i <= 3 ; i++) {
vi.push_back(i); //将1 2 3依次插入vi末尾
}
vi.pop_back(); //删除vi的尾元素3
for (int i = 0; i < vi.size(); i++) { //size()函数会给出v1中元素的个数
printf("%d ", vi[i]);
}
return 0;
}
输出结果:
12
(3) size()
size()用来获得vector中元素的个数,时间复杂度为O(1)。size()返回的是unsigned类型,不过一般来说用%d不会出很大问题,这一点对所有STL容器都是一样的。
示例如下:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1; i <= 3; i++) {
vi.push_back(i); //将1 2 3依次插入vi末尾
}
printf("%d\n", vi.size());
return 0;
}
输出结果:
3
(4) clear()
clear()用来清空vector中的所有元素,时间复杂度为 O(N),其中N为 vector中元素的个数。
示例如下:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1; i <= 3; i++) {
vi.push_back(1); //将1 2 3依次插入vi末尾
}
vi.clear();
printf("%d\n", vi.size());
return 0;
}
输出结果:
0
(5) insert()
insert(it.x)用来向 vector的任意迭代器it处插入一个元素X,时间复杂度O(N)。
示例如下:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 1; i <= 5; i++) {
vi.push_back(i); //此时为1 2 3 4 5
}
vi.insert(vi.begin() + 2, -1); //将-1插入vi[2]的位置
for (int i = 0; i < vi.size() ; i++) {
printf("%d", vi[i]); //1 2 -1 3 4 5
}
return 0;
}
输出结果:
1 2 -1 3 4 5
(6) erase()
erase()有两种用法:删除单个元素、删除一个区间内的所有元素。时间复杂度均为O()。
- 删除单个元素。
erase(it) 即删除迭代器为 it 处的元素。
示例如下:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 5; i <= 9; i++) {
vi.push_back(i); //插入5 6 7 8 9
}
//删除8 (因为vi.begin()对应的是vi[0],所以8不是对应vi.begin() + 4)
vi.erase(vi.begin() + 3);
for (int i = 0; i < vi.size(); i++) {
printf("%d ", vi[i]); //输出56 79
}
return 0;
}
输出结果:
5 6 7 9
- 删除一个区间内的所有元素。
erase(first,last)即删除[first, last)内的所有元素。
示例如下:
#include <stdio.h>
#include <vector>
using namespace std;
int main() {
vector<int> vi;
for (int i = 5; i <= 9; i++) {
vi.push_back(i); //插入5 6 7 8 9
}
vi.erase(vi.begin() + 1, vi.begin() + 4); //删除v1[1]、vi[2]、vi[3]
for (int i = 0; i < vi.size(); i++) {
printf("%d", vi[i]); //输出 5 9
}
return 0;
}
输出结果:
5 9
由上面的说法可以知道,如果要删除这个 vector内的所有元素,正确的写法应该是
vi.erase(vi.begin(), vi.end()),这正如前面说过,vi.end()就是尾元素地址的下一个地址。(当然更方便的清空vector的方法是使用vi.clear())。
4 vector的常见用途
(1) 储存数据
- vector本身可以作为数组使用,而且在一些元素个数不确定的场合可以很好地节省空间。
- 有些场合需要根据一些条件把部分数据输出在同一行,数据中间用空格隔开。由于输出数据的个数是不确定的,为了更方便地处理最后一个满足条件的数据后面不输出额外的空格,可以先用vector记录所有需要输出的数据,然后一次性输出。
(2)用邻接表存储图
使用vector 实现邻接表可以让一些对指针不太熟悉的读者有一个比较方便的写法。
set的常见用法详解
set翻译为集合,是一个内部自动有序且不含重复元素的容器。在考试中,有可能出现需要去掉重复元素的情况,而且有可能因这些元素比较大或者类型不是int型而不能直接开散列表,在这种情况下就可以用set来保留元素本身而不考虑它的个数。当然,上面说的情况也可以通过再开一个数组进行下标和元素的对应来解决,但是set提供了更为直观的接口,并且加入set之后可以实现自动排序,因此熟练使用set可以在做某些题时减少思维量。
如果要使用set,需要添加set头文件,即 #include 。除此之外,还需要在头文件下面加上一句:“using namespace std;”,这样就可以在代码中使用set了。
1.set的定义
单独定义一个set:
set<typename> name;
其定义的写法其实和vector基本是一样的,或者说其实大部分STL都是这样定义的。这里的typename依然可以是任何基本类型,例如int、double、char、结构体等,或者是STL标准容器,例如vector、set、queue等。
和前面vector中提到的一样,如果typename是一个STL容器,那么定义时要记得在>>符号之间加上空格,因为一些使用C++11之前标准的编译器会把它视为移位操作,导致编译错误。下面是一些简单的例子:
set<int> name;
set<double> name;
set<char> name;
set<node> name; //node是结构体的类型
set数组的定义和vector相同:
set<typename> Arrayname[arraysize];
例如
set<int> a[100];
这样Arrayname[0]~Arrayname[arraySize-1]中的每一个都是一个set容器。
2.set容器内元素的访问
set只能通过迭代器(iterator)访问:
set<typename>::iterator it;
typename就是定义set时填写的类型,下面是typename为int和char的举例:
set<int>::iterator it;
set<char>::iterator it;
这样就得到了迭代器it,并且可以通过*it来访问set里的元素。
由于*除开vector和string之外的STL容器都不支持 (it + i)的访问方式,因此只能按如下方式枚举:
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int> st;
st.insert(3); //insert(x)将x插入set中
st.insert(5);
st.insert(2);
st.insert(3);
//注意,不支持it<st.end()的写法
for (set<int>::iterator it = st.begin(); it != st.end(); it++) {
printf("%d", *it);
}
return 0;
}
输出结果:
2 3 5
可以发现,set内的元素自动递增排序,且自动去除了重复元素。
3.set常用函数实例解析
(1)insert()
insert(x)可将x插入set容器中,并自动递增排序和去重,时间复杂度O(logN),其中N为set内的元素个数,示例参见“set容器内元素的访问”。
(2)find()
find(value)返回set中对应值为value的迭代器,时间复杂度为O(logN),N为set内的元素个数。
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int> st;
for (int i = 1; i <= 3; i++) {
st.insert(i);
}
set<int>::iterator it = st.find(2); //在set中查找2,返回其迭代器
printf("%d\n", *it);
//以上两句也可以直接写成printf("冬d\n",*(st.find(2));
return 0;
}
输出结果:
2
(3) erase()
erase()有两种用法:删除单个元素、删除一个区间内的所有元素。
①删除单个元素。
删除单个元素有两种方法:
- st.erase(it),it为所需要删除元素的迭代器。时间复杂度为O(1)。可以结合find()函数来使用。示例如下:
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int>st;
st.insert(100);
st.insert(200);
st.insert(100);
st.insert(300);
st.erase(st.find(100));//利用find()函数找到100,然后用erase删除它
st.erase(st.find(200));//利用find()函数找到200,然后用erase删除它
for (set<int>::iterator it = st.begin(); it != st.end(); it++) {
printf("%d\n", *it);
}
return 0;
}
输出结果:
300
- st.erase(value),value为所需要删除元素的值。时间复杂度为O(logN),N为set内的元素个数。示例如下:
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int> st;
st.insert(100);
st.insert(200);
st.erase(100); //删除set中值为100的元素
for (set<int>::iterator it = st.begin(); it != st.end(); it++) {
printf("%d\n", *it); //只会输出200
}
return 0;
}
输出结果:
200
②.删除一个区间内的所有元素。
st.erase(first,last)可以删除一个区间内的所有元素,其中first为所需要删除区间的起始迭代器,而last则为所需要删除区间的末尾迭代器的下一个地址,也即为删除(first,last)。时间复杂度为O(last - first)。示例如下:
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int> st;
st.insert(20);
st.insert(10);
st.insert (40);
st.insert(30);
set<int>::iterator it = st.find(30);
st.erase(it, st.end()); //删除元素30至set末尾之间的元素,即30和40
for (it = st.begin(); it != st.end(); it++) {
printf("%d", *it);
// 输出10 20
}
return 0;
}
输出结果:
10 20
(4)size()
size0用来获得set内元素的个数,时间复杂度为O(1)。
示例如下:
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int>st;
st.insert(2);//插入254
st.insert(5);
st.insert(4);
printf("%d\n", st.size()); //输出set内元素的个数
return 0;
}
输出结果:
3
(5)clear()
clear()用来清空set中的所有元素,复杂度为O(N),其中N为set内元素的个数。
示例如下:
#include <stdio.h>
#include <set>
using namespace std;
int main() {
set<int>st;
st.insert(2);
st.insert(5);
st.insert(4);
st.clear(); //清空set
printf("%d\n", st.size());
return 0;
}
输出结果:
0
4.set的常见用途
set最主要的作用是自动去重并按升序排序,因此碰到需要去重但是却不方便直接开数组的情况,可以尝试用set解决。
延伸:set中元素是唯一的,如果需要处理不唯一的情况,则需要使用multiset。另外, C++11标准中还增加了unordered set,以散列代替set内部的红黑树(Red Black Tree,一种自平衡二叉查找树)实现,使其可以用来处理只去重但不排序的需求,速度比set要快得多,有兴趣的读者可以自行了解,此处不多作说明。
string的常见用法详解
在C语言中,一般使用字符数组char str[]来存放字符串,但是使用字符数组有时会显得操作麻烦,而且容易因经验不足而产生一些错误。为了使编程者可以更方便地对字符串进行操作,C++在STL中加入了string类型,对字符串常用的需求功能进行了封装,使得操作起来更方便,且不易出错。
如果要使用string,需要添加string头文件,即#include (注意string.h和string是不一样的头文件)。除此之外,还需要在头文件下面加上一句:“using namespace std;”,这样就可以在代码中使用string了。下面来看string的一些常用用法。
1.string的定义
定义string的方式跟基本数据类型相同,只需要在string后跟上变量名即可:
string str;
如果要初始化,可以直接给string类型的变量进行赋值:
string str = "abcd";
2.string中内容的访问
(1)通过下标访问
一般来说,可以直接像字符数组那样去访问string:
#include <stdio.h>
#include <string>
using namespace std;
int main() {
string str = "abcd";
for (int i = 0; i < str.length(); i++) {
printf("%c", str[i]); //输出abcd
}
return 0;
}
输出结果:
abcd
如果要读入和输出整个字符串,则只能用cin和cout:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str;
cin >> str;
cout << str;
return 0;
}
上面的代码对任意的字符串输入,都会输出同样的字符串。
那么,真的没有办法用printf来输出string吗?其实是有的,即用c_str()将string类型转换为字符数组进行输出,示例如下:
#include <stdio.h>
#include <string>
using namespace std;
int main() {
string str = "abcd";
printf("%s\n", str.c_str()); //将string型str用c_str()变为字符串数组
return 0;
}
输出结果:
abcd
(2)通过迭代器访问
一般仅通过(1)即可满足访问的要求,但是有些函数比如insert()与erase()则要求以迭代器为参数,因此还是需要学习一下string迭代器的用法。
由于string不像其他STL容器那样需要参数,因此可以直接如下定义:
string::iterator it;
这样就得到了迭代器it,并且可以通过*it来访问string里的每一位:
#include <stdio.h>
#include <string>
using namespace std;
int main() {
string str = "abcd";
for (string::iterator it = str.begin(); it != str.end(); it++) {
printf("%c", *it);
}
return 0;
}
输出结果:
abcd
最后指出,string和vector一样,支持直接对迭代器进行加减某个数字,如str.begin()+3的写法是可行的。
3.string常用函数实例解析
事实上,string的函数有很多,但是有些函数并不常用,因此下面就几个常用的函数举例。
(1)operator+=
这是string的加法,可以将两个string直接拼接起来。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str1 = "abc", str2 = "xyz", str3;
str3 = str1 + str2; //将str1和str2拼接,赋值给str3
str1 += str2; //将str2直接拼接到str1上
cout << str1 << endl;
cout << str3 << endl;
return 0;
}
输出结果
abcxyz
abcxyz
(2)compare operator
两个string类型可以直接使用==、!=、<、<=、>、>=比较大小,比较规则是字典序。
示例如下:
#include <stdio.h>
#include <string>
using namespace std;
int main() {
string str1 = "aa", str2 = "aaa", str3 = "abc", str4 = "xyz";
if (str1 < str2)
printf("ok1\n");//如果str1字典序小于str2,输出ok1
if (str1 != str3)
printf("ok2\n");//如果str1和str3不等,输出ok2
if (str4 >= str3)
printf("ok3\n");//如果str4字典序大于等于str3,输出ok3
return 0;
}
输出结果:
ok1
ok2
ok3
(3)length()/size()
length()返回string的长度,即存放的字符数,时间复杂度为O(1)。size()与length()基本相同。
示例如下:
string str = "abcxyz";
printf("%d %d\n",str.length(),str.size());
输出结果:
66
(4)insert()
string的insert()函数有很多种写法,这里给出几个常用的写法,时间复杂度为O(N)。
①insert(pos,string),在pos号位置插入字符串string。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "abcxyz", str2 = "opq";
str.insert(3, str2); //往str[3]处插入opg,这里str2的位置直接写"opg"也是可以的
cout << str << endl;
return 0;
}
输出结果:
abcopqxyz
②inserte(it,it2,t3),it为原字符串的欲插入位置,it2和it3为待插字符串的首尾迭代器,用来表示串[t2,it3)将被插在it的位置上。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "abcxyz", str2 = "opg";
// str是原字符串,str2是待插字符串
// 在str的3号位(即c和x之间)插入str2
str.insert(str.begin() + 3, str2.begin(), str2.end());
cout << str << endl;
return 0;
}
输出结果:
abcopqxyz
(5)erase()
erase()有两种用法:删除单个元素、删除一个区间内的所有元素。时间复杂度均为O)。
①删除单个元素。
str.erase(it)用于删除单个元素,it为需要删除的元素的迭代器。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "abcdefg";
str.erase(str.begin() + 4); //删除4号位(即e)
cout << str << endl;
return 0;
}
输出结果:
abcdfg
②删除一个区间内的所有元素。
删除一个区间内的所有元素有两种方法:
- str.erase(first,last),其中first为需要删除的区间的起始迭代器,而last则为需要删除的区间的末尾迭代器的下一个地址,也即为删除[frst,last)
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "abcdefg";
// 删除在[str, begin() + 2, str.end() - 1)内的元素,即cdef
str.erase(str.begin() + 2, str.end() - 1);
cout << str << endl;
return 0;
}
输出结果:
abg
- str.erase(pos,length),其中pos为需要开始删除的起始位置,length为删除的字符个数。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "abcdefg";
str.erase(3, 2); //删除从3号位开始的2个字符,即de
cout << str << endl;
return 0;
}
输出结果:
abcfg
(6)clear()
clear()用以清空string中的数据,时间复杂度一般为O(1)。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "abcd";
str.clear();//清空字符串
printf("%d\n", str.length());
return 0;
}
输出结果:
0
(7)substr()
substr(pos,len)返回从pos号位开始、长度为len的子串,时间复杂度为O(Ien)。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Thank you for your smile.";
cout << str.substr(0, 5) << endl;
cout << str.substr(14, 4) << endl;
cout << str.substr(19, 5) << endl;
return 0;
}
输出结果:
Thank
your
smile
(8)string::npos
string::npos 是一个常数,其本身的值为-1,但由于是unsigned_int类型,因此实际上也可以认为是unsigned_int类型的最大值。string::npos用以作为find函数失配时的返回值。例如在下面的实例中可以认为string::npos等于-1或者4294967295。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
if (string::npos == -1) {
cout << "-1 is true." << endl;
}
if (string::npos == 4294967295) {
cout << "4294967295 is also true." << endl;
}
return 0;
}
输出结果:
-1 is true.
4294967295 is also true.
(9)find()
str.find(str2),当str2是sr的子串时,返回其在str中第一次出现的位置;如果str2不是str的子串,那么返回string::npos。
str.find(str2,pos),从str的pos号位开始匹配str2,返回值与上相同。
时间复杂度为O(nm),其中n和m分别为str和str2的长度。
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Thank you for your smile.";
string str2 = "you";
string str3 = "me";
if (str.find(str2) != string:: npos) {
cout << str.find(str2) << endl;
}
if (str.find(str2, 7) != string:: npos) {
cout << str.find(str2, 7) << endl;
}
if (str.find(str3) != string::npos) {
cout << str.find(str3) << endl;
} else {
cout << "I know there is no position for me." << endl;
}
return 0;
}
输出结果:
6
14
I know there is no position for me.
(10)replace()
str.replace(pos,len,str2)把str从pos号位开始、长度为len的子串替换为str2。
str.replace(it1,it2,str2)把str的迭代器[it1,it2)范围的子串替换为str2。
时间复杂度为O(str.length()).
示例如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Maybe you will turn around.";
string str2 = "will not";
string str3 = "surely";
cout << str.replace(10, 4, str2) << endl;
cout << str.replace(str.begin(), str.begin() + 5, str3) << endl;
return 0;
}
输出结果:
Maybe you will not turn around.
surely you will not turn around.