来源:郑莉老师C++
vector 翻译为向量,但是这里使用“变长数组”的叫法更容易理解,也即“长度根据需要而自动改变的数组”。在考试题中,有时会碰到只用普通数组会超内存的情况,这种情况使用 vector 会让问题的解决便捷许多。另外, vector 还可以用来以邻接表的方式储存图,这对无法使用邻接矩阵的题目(结点数太多)、又害怕使用指针实现邻接表的读者是非常友好的,写法也非常简洁。
常见用途:
1. 储存数据
① vector 本身可以作为数组使用,而且在一些元素个数不确定的场合可以很好地节省空间。
② 有些场合需要根据一些条件把部分数据输出在同一行,数据中间用空格隔开。由于输出数据的个数是不确定的,为了更方便地处理最后一个满足条件的数据,后面不输出额外的空格,可以先用 vector 记录所有需要输出的数据,然后一次性输出。
2. 用邻接表存储图
使用 vector 实现邻接表可以让一些对指针不太熟悉的读者有一个比较方便的写法。具体见《算法笔记》10.2.2 节。
vector 常用函数概览:
#include
#include
using namespace std;
int main() {
vector vi;
// push_back():在vector后面添加一个元素,O(1)
for (int i = 0; i < 10; i++) {
vi.push_back(i);
}
//size():用于获取vector中元素个数,O(1)
for (int i = 0; i < vi.size(); i++) {
cout << vi[i] << " "; // 0 1 2 3 4 5 6 7 8 9
}
cout << endl;
// pop_back():删除vector尾部元素,O(1)
vi.pop_back(); // 删除vi尾元素 9
for (int i = 0; i < vi.size(); i++) {
cout << vi[i] << " ";// 0 1 2 3 4 5 6 7 8
}
cout<
set 翻译为集合,是一个内部自动有序(递增排序)且不含重复元素的容器。
需要注意的是,除了 vector 和 string 之外的 STL 容器都不支持*(it+i)的访问方式。
常见用途:
set 最主要的作用是自动去重并按升序排序,因此碰到需要去重但是却不方便直接开数组的情况,可以尝试用 set 解决。
延伸:set 中元素是唯一的,如果需要处理不唯一的情况,则需要使用 multiset。另外,C++11 标准中还增加了unordered_set,以散列代替 set 内部的红黑树( Red Black Tree,一种自平衡二叉査找树)实现,使其可以用来处理只去重但不排序的需求,速度比 set 要快得多。
set 常用函数概览:
#include
#include
using namespace std;
int main() {
set st;
// insert O(logN) N为set内元素个数
for (int i = 0; i < 10; i ++) {
st.insert(i);
}
//find O(logN)
// set::iterator it = st.find(2); // 在set中查找2,返回其迭代器
// cout << *it <::iterator it = st.begin(); it != st.end(); it++) { // 遍历
cout <<*(it)<< " "; // 0 1 2 5 6 7 8 9
}
cout<::iterator it = st.find(6);
st.erase(it, st.end()); // 删除6~9区间元素
for (set::iterator it = st.begin(); it != st.end(); it++) {
cout <<*(it)<< " "; // 0 1 2 5
}
cout<
在 C 语言中,一般使用字符数组 char strl 来存放字符串,但是使用字符数组有时会显得操作麻烦,而且容易因经验不足而产生一些错误。为了使编程者可以更方便地对字符串进行操作,C++在 STL 中加入了 string 类型,对字符串常用的需求功能进行了封装,使得操作起来更方便,且不易出错。
string 函数概述:
string 的定义
string 中内容的访问
(1)通过下标访问(可直接像字符数组那样去访问 string)
(2)通过迭代器访问
注意:若要读入和输出整个字符串,只能用 cin 和 cout
3. string 常用函数
(1)operator+= 这是 string 的加法,可将 string 直接拼接起来
(2)compare operator 两个 string 类型可使用== != < <= > >=比较大小 (字典序)
(3)length() / size() length()返回 string 长度,即存放的字符数,O(1),二者基本相同
(4)insert() 两个常用写法,均 O(N)
(5) erase() 删除单个元素 / 删除区间元素(两种方法)
(6)clear() 清空 string 中的数据,O(1)
(7)substr(pos, len) 返回从 pos 号位开始,长度为 len 的子串,O(len)
(8)string::npos 这是个常数,值为-1 但由于是 unsigned_int 类型 ,因此也可以认为是 unsigned_int 类型的最大值。string::npos 用以作为 find 函数失配时的返回值。
可以认为 string::npos 等于-1 或 4294967295
注:4294967295 是计算机程序设计里面的一个值,表示 32 位无符号整数的十进制最大值。如果是 16 进制,那么是 0xFFFFFFFF。也可以解释为一个 IP 地址(V4) 255.255.255.255
(9)find() O(nm) 其中 n 和 m 分别是 str 和 str2 的长度。
(10)replace() O(str.length())
#include
#include
using namespace std;
int main() {
// 1、string的定义
string str = "abcdefg";
// 2、string中内容的访问
// (1)通过下标访问(可直接像字符数组那样去访问string)
for (int i= 0; i < str.length(); i++) {
cout << str[i] << " ";
} // a b c d e f g
cout << endl;
// 注意:若要读入和输出整个字符串,只能用 cin 和 cout
//string str1;
//cin >> str1; // ahhh
//cout << str1 < >=比较大小
string s1 = "aa", s2 = "aaa", s3 = "abc", s4 = "xyz";
if (s1 < s2) cout << "ok1" << endl; // 如果s1字典序小于s2,输出ok1 // ok1
if (s1 != s3) cout << "ok2" < s3) cout << "ok3" <
1.map 的定义
单独定义一个 map:
map
mp;
map 和其他 STL 容器在定义上有点不一样,因为 map 需要确定映射前类型(键 key)和映射后类型(值 value),所以需要在< >内填写两个类型,其中第一个是键的类型,第二个是值的类型。如果是 int 型映射到 int 型,就相当于是普通的 int 型数组。
而如果是字符串到整型的映射,必须使用 string 而不能用 char 数组:
map
mp;
这是因为 char 数组作为数组,是不能被作为键值的。如果想用字符串做映射,必须用 string。
前面也说到,map 的键和值也可以是 STL 容器,例如可以将一个set 容器映射到一个字符串:
map
mp:
2.map 容器内元素的访问
map 一般有两种访问方式:通过下标访问或通过迭代器访问。下面分别讨论这两种访问方式。
(1) 通过下标访问
和访问普通的数组是一样的,例如对一个定义为 map
map
mp;
mp[‘c’]=20;
mp[‘c’]=30; // 20 被覆盖
(2)通过迭代器访问
map 迭代器的定义和其他 STL 容器迭代器定义的方式相同:
map
::iterator it;
typenamel 和 typename2 就是定义 map 时填写的类型,这样就得到了迭代器 it。
map 迭代器的使用方式和其他 STL 容器的迭代器不同,因为 map 的每一对映射都有两个 typename,这决定了必须能通过一个 it 来同时访问键和值。事实上,map 可以使用 it>first 来访问键,使用 it-> second 来访问值。
来看下面这个示例:
map
mp;
mp[‘m’]=20
mp[‘r’]=30;
mp[‘a’]=40;
for (map::iterator it = mp.begin>(); it != mp.end(); it++) {
cout<< it -> first << " " << it -> second << endl;
}
在上面这个例子中,it->first 是当前映射的键,it->second 是当前映射的值。程序输出如下
a 40
m 20
r 30
接下来似乎发现了一个很有意思的现象:map 会以键从小到大的顺序自动排序,即按 a < m < r 的顺序排列这三对映射。这是由于 map 内部是使用红黑树实现的(set 也是),在建立映射的过程中会自动实现从小到大的排序功能。
3. map 的常用函数
(1)删除单个元素有两种方法:
mp.erase(it) it 为需要删除的元素的迭代器,O(1)
mp.erase(key) key 为欲删除的映射的键。O(logN),N 为 map 内元素的个数。
(2)删除一个区间内的所有元素
mp.erase(first, last),其中 first 为需要删除的区间的起始迭代器,而 last 则为需要删除的区间的末尾迭代器的下一个地址,也即为删除左闭右开的区间[first, last) 时间复杂度为 O(last - first)
4.map 的常见用途
① 需要建立字符(或字符串)与整数之间映射的题目,使用 map 可以减少代码量
② 判断大整数或者其他类型数据是否存在的题目,可以把 map 当 bol 数组用
③ 字符串和字符串的映射也有可能会遇到。
延伸:map 的键和值是唯一的,而如果一个键需要对应多个值,就只能用 multimap。另外,C+11 标准中还增加了 unordered_map,以散列代替 map 内部的红黑树实现,使其可以用来处理只映射而不按 key 排序的需求,速度比 map 要快得多。
#include
#include
queue 翻译为队列,在 STL 中主要则是实现了一个先进先出的容器。
1.queue 的定义
添加头文件#include ,其定义的写法和其他 STL 容器相同, typename 可以是任意基本数据类型或容器:
queue< typename> name;
2.queue 容器内元素的访问
由于队列( queue)本身就是一种先进先出的限制性数据结构,因此在STL 中只能通过 front()来访问队首元素,或是通过 back()来访问队尾元素。
3.queue 常用函数
push(x)将 x 进行入队,时间复杂度 O(1)。
分别获得队首元素和队尾元素,时间复杂度 O(1)。
令队首元素出队,时间复杂度为 O(1)。
检测 queue 是否为空,返回 true 则空,返回 false 则非空。时间复杂度为 O(1)。
返回 queue 内元素的个数,时间复杂度为 O(1)。
#include
#include
using namespace std;
int main() {
queue q;
for (int i = 1; i <= 5; i++) {
q.push(i); // push(i)用以将i压入队列,因此依次入队1 2 3 4 5
}
cout << q.front() << " " << q.back() << endl; // 1 5
q.pop(); // 将队首元素1出队
cout << q.front() << endl; // 2
if(q.empty() == true) cout << "Empty!" <
4.queue 的常见用途
当需要实现广度优先搜索时,可以不用自己手动实现一个队列,而是用 queue 作为代替,以提高程序的准确性。
另外有一点注意的是,使用 front()和 pop()函数前,必须用 empty()判断队列是否为空,否则可能因为队空而出现错误。
延伸:STL 的容器中还有两种容器跟队列有关,分别是双端队列( deque)和优先队列( priority_queue),前者是首尾皆可插入和删除的队列,后者是使用堆实现的默认将当前队列最大元素置于队首的容器。
priority_queue 又称为优先队列,其底层是用堆来进行实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。例如在队列有如下元素,且定义好了优先级:
桃子(优先级 3)
梨子(优先级 4)
苹果(优先级 1)
那么出队的顺序为梨子(4) → 桃子(3) → 苹果(1)。
当然,可以在任何时候往优先队列里面加入(push)元素,而优先队列底层的数据结构堆(heap)会随时调整结构,使得每次的队首元素都是优先级最大的。
关于这里的优先级则是规定出来的。例如上面的例子中,也可以规定数字越小的优先级越大(在德国课程中,评分 1 分为优秀,5、6 分就是不及格了)。
1.priority_queue 的定义
添加头文件#include
priority_queue< typename > name;
其定义的写法和其他 STL 容器相同,typename 可以是任意基本数据类型或容器:
2. priority_queue 容器内元素的访问
和队列不一样的是,优先队列没有 font()函数与 back()函数,而只能通过 top()函数来访问队首元素(也可以称为堆顶元素),也就是优先级最高的元素。
priority_queue q;
q.push(3);
q.push(4);
q.push(1);
cout << q.top() << endl; // 4
3.priority_queue 常用函数
(1)push()
push(x)将令 x 入队,时间复杂度为 O(logN),其中 N 为当前优先队列中的元素个数。实例见“ priority_queue 容器内元素的访问 ”。
(2)top()
top()可以获得队首元素(即堆顶元素),时间复杂度为 O(1)。
(3)pop()
pop()令队首元素(即堆顶元素)出队,时间复杂度为 O(logN),其中 N 为当前优先队列中的元素个数。
(4)empty()
empty()检测优先队列是否为空,返回 true 则空,返回 false 则非空。时间复杂度为 O(1)。
(5) size
size()返回优先队列内元素的个数,时间复杂度为 O(1)。
#include
#include
using namespace std;
int main() {
priority_queue q;
q.push(3);
q.push(4);
q.push(1);
cout << q.top() << endl; // 4
q.pop();
cout << q.top() << endl; // 3
if (q.empty() == false) {
cout << "队列非空!" << endl; // 队列非空!
}
cout << q.size() << endl; // 2
return 0;
}
4.priority_queue 内元素优先级的设置
#include
#include
using namespace std;
int main() {
priority_queue, greater > q;
q.push(3);
q.push(4);
q.push(1);
cout << q.top() << endl; // 1
return 0;
}
事实上,即便是基本数据类型,也可以使用下面讲解的结构体的优先级设置方法,只不过第三个参数的写法不太一样了。下面来看结构体的优先级设置方法。
#include
#include
#include
using namespace std;
struct fruit {
string name;
int price;
friend bool operator < (fruit f1, fruit f2) {
return f1.price > f2.price; // 苹果 1 若改为 > 则输出:梨子 4
}
}f1, f2, f3;
int main() {
priority_queue q; // fruit类型!
f1.name = "桃子";
f1.price = 3;
f2.name = "梨子";
f2.price = 4;
f3.name = "苹果";
f3.price = 1;
q.push(f1);
q.push(f2);
q.push(f3);
cout << q.top().name << " " << q.top().price << endl;
return 0;
}
#include
#include
#include
using namespace std;
struct fruit {
string name;
int price;
}f1, f2, f3;
struct cmp {
bool operator() (fruit f1, fruit f2) {
return f1.price > f2.price; // 苹果 1 若改为 > 则输出:梨子 4
}
};
int main() {
priority_queue, cmp> q;
f1.name = "桃子";
f1.price = 3;
f2.name = "梨子";
f2.price = 4;
f3.name = "苹果";
f3.price = 1;
q.push(f1);
q.push(f2);
q.push(f3);
cout << q.top().name << " " << q.top().price << endl;
return 0;
}
示例如下:
#include
#include
using namespace std;
int main() {
stack st;
for (int i = 1; i <= 5; i++) {
st.push(i); // push(i)用以把i压入栈,故此处依次入栈 1 2 3 4 5
}
cout << st.top() << endl; // 5
return 0;
}
st.pop();
cout << st.top() << endl; // 4
if (st.empty() == false) {
cout << "栈不为空" << endl; // 栈不为空
}
cout << st.size() << endl; // 4
pair 中只有两个元素,分别是 first 和 second,只需要按正常结构体的方式去访问即可。
示例如下:
#include
#include
#include
using namespace std;
int main() {
pair p;
p.first = "haha";
p.second = 5;
cout << p.first << " " << p.second << endl; // haha 5
p = make_pair("xixi", 55);
cout << p.first << " " << p.second << endl; // xixi 55
p = pair("heihei", 555);
cout << p.first << " " << p.second << endl; // heihei 555
return 0;
}
pair p1(5, 10);
pair p2(5, 15);
pair p3(10, 5);
if (p1 < p3) cout << "p1 < p3" << endl; // p1 < p3
if (p1 <= p3) cout << "p1 <= p3" << endl; // p1 <= p3
if (p1 < p2) cout << "p1 < p2" << endl; // p1 < p2
map mp;
mp.insert(make_pair("heihei", 5));
mp.insert(pair("haha", 10));
for (map::iterator it = mp.begin(); it != mp.end(); it++) {
cout << it->first << " " << it->second << endl; // haha 10
} // heihei 5
概览:
1. max()、min()和abs()
2. swap()
3. reverse()
4. next_permutation()
5. fill()
6. sort()
7. lower_bound()和upper_bound()
int x = -1, y = -2;
cout << max(x, y) << " " << min(x, y) << endl; // -1 -2
cout << abs(x) << " " << abs(y) << endl; // 1 2
int x = 1, y = 2;
swap(x, y);
cout << x << " " << y << endl; // 2 1
int a[10] = {10, 11, 12, 13, 14, 15};
reverse(a, a + 3); // 将a[0]~a[2]反转
for (int i = 0; i < 6; i++) {
cout << a[i] << " "; // 12 11 10 13 14 15
}
如果是对容器中的元素(例如 string 字符串)进行反转,结果也是一样:
string str = "abcdefghi";
reverse(str.begin() + 1, str.end() - 1); // 对str[1]~str[7]反转
for (int i = 0; i < str.length(); i++) {
cout << str[i] << " "; // a h g f e d c b i
}
int a[10] = {1, 2, 3};
// a[0]~a[2]之间的序列需要求解 next_permutation
do {
cout << a[0] << a[1] << a[2] << endl;
} while (next_permutation(a, a + 3));
在上述代码中,使用循环是因为 next_permutation 在已经到达全排列的最后一个时会返回 false,这样会方便退出循环。而使用 do… while 语句而不使用 while 语句是因为序列 123 本身也需要输出,如果使用 while 会直接跳到下一个序列再输出,这样结果会少一个 123。
若 a[10] = {1, 5, 3};
输出结果中没有 135,因为这个函数给出一个序列在全排列中的下一个序列,135 是 153 的上一个序列,所以不会输出。
int a[5] = {1, 2, 3, 4, 5};
fill(a, a + 4, 233); // 将a[0]~a[3]均赋值为233
for (int i = 0; i < 5; i++) {
cout << a[i] << " "; // 233 233 233 233 5
}
int a[6] = {9, 4, 2, 5, 6, -1};
// 将a[0]~a[1]从小到大排序
sort(a, a + 2);
for (int i = 0; i < 6; i++) {
cout << a[i] << " "; // 4 9 2 5 6 -1
}
cout << endl;
// 将 a[0]~a[5] 从小到大排序
sort(a, a + 6);
for (int i = 0; i < 6; i++) {
cout << a[i] << " "; // -1 2 4 5 6 9
}
看运行结果,试着理解一下 ”尾元素地址的下一个地址“含义。
对 double、char(默认为字典序)同理,不再展开。
int a[5] = {3, 4, 1, 2};
sort(a, a + 4);
for (int i = 0; i < 4; i++) {
cout << a[i] << " "; // 1 2 3 4
}
如果想要从大到小来排序,则要使用比较函数 cmp 来”告诉“sort 何时要交换元素(让元素的大小比较关系反过来)。
示例如下:
#include
#include
using namespace std;
bool cmp(int a, int b) {
return a > b; // 可以理解为当 a > b时把a放在b前面
}
int main() {
int a[5] = {3, 4, 1, 2};
sort(a, a + 4, cmp);
for (int i = 0; i < 4; i++) {
cout << a[i] << " "; // 4 3 2 1
}
return 0;
}
其中,
return a > b;
可以理解为当 a > b 时把 a 放在 b 前面,妙啊!
这样就可以让数值较大的元素放在前面,也就达到了从大到小排序的要求。
对于 double、char 等同理,不再展开。
#include
#include
using namespace std;
struct node {
int x, y;
}ssd[10];
bool cmp(node a, node b) {
return a.x > b.x; // 按x值从大到小对结构体数组排序
}
int main() {
ssd[0].x = 2; // {2, 2}
ssd[0].y = 2;
ssd[1].x = 1; // {1, 3}
ssd[1].y = 3;
ssd[2].x = 3; // {3, 1}
ssd[2].y = 1;
sort(ssd, ssd + 3, cmp); // 排序
for (int i = 0; i < 3; i++) {
cout << ssd[i].x << ", " << ssd[i].y << " "; //3, 1 2, 2 1, 3
}
return 0;
}
下面以 vector 为例:
#include
#include
#include
using namespace std;
bool cmp(int a, int b) { // 因为vector中的元素为int型,因此仍然是int的比较
return a > b;
}
int main() {
vector vi;
vi.push_back(3);
vi.push_back(1);
vi.push_back(2);
sort(vi.begin(), vi.end(), cmp); // 对整个vector排序
for (int i = 0; i < 3; i++) {
cout << vi[i]<< " "; // 3 2 1
}
return 0;
}
再来看 string 的排序:
#include
#include
#include
using namespace std;
bool cmp(int a, int b) { // 因为vector中的元素为int型,因此仍然是int的比较
return a > b;
}
int main() {
string str[3] = {"bbbb", "ccc", "aaa"};
sort(str, str + 3); // 将string型数组按字典序从小到大输出
for (int i = 0; i < 3; i++) {
cout << str[i]<< " "; // aaa bbbb ccc
}
return 0;
}
如果上例中,需要按字符串长度从小到大排序,那么可以这么写:
#include
#include
#include
using namespace std;
bool cmp(string str1, string str2) {
return str1.length() < str2.length(); // 按string的长度从小到大排序
}
int main() {
string str[3] = {"bbbb", "cc", "aaa"};
sort(str, str + 3, cmp); // 将string型数组按字典序从小到大输出
for (int i = 0; i < 3; i++) {
cout << str[i]<< " "; // cc aaa bbbb
}
return 0;
}
示例如下:
#include
#include
using namespace std;
int main() {
int a[10] = {1, 2, 2, 3, 3, 3, 5, 5, 5, 5};
// 寻找-1
int* lowerPos = lower_bound(a, a + 10, -1);
int* upperPos = upper_bound(a, a + 10, -1);
cout << lowerPos - a << " " << upperPos - a << endl; // 0 0 返回的都是下标
// 寻找1
lowerPos = lower_bound(a, a + 10, 1);
upperPos = upper_bound(a, a + 10, 1);
cout << lowerPos - a << " " << upperPos - a << endl; // 0 1
// 寻找3
lowerPos = lower_bound(a, a + 10, 3);
upperPos = upper_bound(a, a + 10, 3);
cout << lowerPos - a << " " << upperPos - a << endl; // 3 6
// 寻找4
lowerPos = lower_bound(a, a + 10, 4);
upperPos = upper_bound(a, a + 10, 4);
cout << lowerPos - a << " " << upperPos - a << endl; // 6 6
//寻找6
lowerPos = lower_bound(a, a + 10, 6);
upperPos = upper_bound(a, a + 10, 6);
cout << lowerPos - a << " " << upperPos - a << endl; // 10 10 返回可插入该元素的位置的指针
return 0;
}
显然,如果只是想获得欲查元素的下标,就可以不使用临时指针,而直接令返回值减去数组首地址即可:
int a[10] = {1, 2, 2, 3, 3, 3, 5, 5, 5, 5};
// 寻找3
cout << lower_bound(a, a + 10, 3) - a << endl; // 3 所在位置下标
cout << upper_bound(a, a + 10, 3) - a << endl; // 6