STL基础知识
写在前面:
什么是STL? Standard Template Library
。 C++ STL从广义来讲包括了三类:算法,容器和迭代器。
算法包括排序,复制等常用算法,以及不同容器特定的算法。 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。
这部分内容主要参考 岁与禾 的简书博客 C++进阶-STL容器,你看我就够了。其次,还有参考C++ string类(C++字符串)完全攻略。一些补充内容的参考来源也会在文中注明。
-
容器中的一般节点类,需要提供拷贝构造函数( 避免出现浅拷贝问题,导致崩溃 ),并重载等号操作符(用来赋值)
-
每个容器都提供了一个默认构造函数和一个默认拷贝构造函数
string
- 创建string对象,首先要
#include <string>
string s1(); // si = ""
//通过const char * 初始化,这两个效果一样
string s1 = "aaaa";
string s2("bbbbb");
//还可以选取部分字符串内容
string s3("12345", 1, 3); //s3 = "234",即 "12345" 的从下标 1 开始,长度为 3 的子串
//用10个'a'字符来初始化字符串
string s4(10, 'a');
//通过拷贝构造函数来初始化对象s5
string s5 = s2;
此外, swap 成员函数可以交换两个 string 对象的内容。例如:
string s1("West”), s2("East");
s1.swap(s2); // s1 = "East",s2 = "West"
- 遍历:
string str("abcdefg");
//数组形式遍历(不会抛出异常),程序直接终端。
for (int i = 0; i < str.length(); i++) {
cout<< str[i] << endl;
}
//at方法遍历,根据index取值 (会抛出异常,可以捕获处理)
for (int i = 0; i < str.length(); i++) {
cout << str.at(i) << endl;
}
//迭代器遍历
for (string::iterator it = str.begin(); it != str.end(); it++) {
cout << *it << endl;
}
测试[]和at是否抛出异常:
//创建字符串对象
string str("abcdefg");
// 这个异常可以捕获
try{
cout<<str.at(10)<<endl;
}catch(...){
cout<<"使用at"<<endl;
}
// 这个异常不可以捕获
try{
cout<<str[10]<<endl;
}catch(...){
cout<<"使用[]"<<endl;
}
输出结果:
使用at
[此处无输出]
- 与char *的转换
//1 string转const char *
string str("aaaaaa");
const char *c_str = str.c_str();
//2 string 转char*
const int len = str.length();
c = new char[len+1];
strcpy(c,str.c_str());
//3 string拷贝到buf[]中
char buf[128] = {0};
str.copy(buf, 3, 0); //从0开始,拷贝3个字符到buf指向的内存空间
string提供了copy(buf,size,begin)
成员函数来讲string从begin
位置开始的size
个字符拷贝到buf
中。如果buf空间容纳不下,会越界;拷贝过去时,不会给buf末尾添加\0
。
- 拼接,使用
+
或成员函数append
.
string s1 = "123456";
string s2 = "abcdef";
//直接使用加号运算符拼接
string s3 = s1 + s2;
//使用成员函数拼接
string s4 = s1.append(s2); // 此时s4和s1都是123456abcdef
与拼接相反的,就是子串了。参考C++ string类(C++字符串)完全攻略。substr 成员函数可以用于求子串 (n, m),原型如下:
string substr(int n = 0, int m = string::npos) const;
其中,看两个都是默认参数。注意,第二个参数m
指的是子串长度。
string s1 = "this is ok";
string s2 = s1.substr(2, 4); // s2 = "is i"
s2 = s1.substr(2); // s2 = "is is ok"
- 查找和替换
find
函数,用来查找字符串中指定的字符。第2个参数用于指定查找开始的位置。
string s1 = "hello hello 012345";
//从0位置开始查找第一个hello出现的首位位置
size_t index1 = s1.find("hello",0);
cout << index1 << endl; // 0
//查找第一个hello出现时的首位位置
size_t index2 = s1.find_first_of("hello");
cout << index2 << endl; // 0
//查找最后一个hello出现时的末尾位置
size_t index3 = s1.find_last_of("hello");
cout << index3 << endl; // 10
另外参考C++ string类(C++字符串)完全攻略,查找时:
与find相对的是
rfind
,从后往前查找子串或字符出现的位置。此外,还有
find_first_not_of
:从前往后查找何处出现另一个字符串中没有包含的字符。
find_last_not_of
:从后往前查找何处出现另一个字符串中没有包含的字符。
replace
函数,用来替换字符串中指定位置的字符串。replace
函数先删除指定位置开始的指定长度歌字符,然后在当前指定位置插入新的字符。
// 子串(2,4)的含义是:从位置2开始的4个字符.
string s1("Real Steel");
s1.replace(1, 3, "123456", 2, 4); //用 "123456" 的子串(2,4) 替换 s1 的子串(1,3)
cout << s1 << endl; //输出 R3456 Steel
string s2("Harry Potter");
s2.replace(2, 3, 5, '0'); //用 5 个 '0' 替换子串(2,3)
cout << s2 << endl; //输出 HaOOOOO Potter
int n = s2.find("OOOOO"); //查找子串 "00000" 的位置,n=2
s2.replace(n, 5, "XXX"); //将子串(n,5)替换为"XXX"
cout << s2 < < endl; //输出 HaXXX Potter
- 区间删除和插入
string提供了insert
和erase
分别实现插入和删除操作。下面的操作中, 使用int指示位置的,会返回新的string; 使用指针指示位置的,会返回操作后的当前指针位置。
string s1 = "Hello2World!";
// 删除字符串中的'2',
// 通过find函数,查找'2'所在的迭代器位置(find函数需要引入#include<algorithm>)
// 然后删除位置,会返回当前迭代器位置
string::iterator it = find(s1.begin(), s1.end(), '2');
if (it != s1.end()) {
s1.erase(it);
}
cout << s1 << endl; // HelloWorld!
// 删除两个迭代器之间的字符,左闭右开区间
s1.erase(s1.begin(), s1.begin() + 5);
cout << s1 << endl; // World!
// 在0位置插入"AAA"
s1.insert(0, "AAA");
cout << s1 << endl; // AAAWorld!
// 也可以用string对象来插入
string s2("BBB");
s1.insert(3,s2);
cout<< s1 << endl; // AAABBBWorld!
// 位置6插入3个字符'C'
s1.insert(6,3,'C');
cout << s1 << endl; // AAABBBCCCWorld!
// 删除从位置6开始的3个字符
s1.erase(6,3);
cout << s1 << endl; // AAABBBWorld!
- 大小写转换
参考1. 使用C++的transform函数实现string的大小写转换,2. string类中字符的大小写转换.
string str = "abcdefgHIJKLMN";
//转小写
transform(str.begin(),str.end(),str.begin(),::tolower);
cout<<str<<endl;
//转大写
transform(str.begin(),str.end(),str.begin(),::toupper);
cout<<str<<endl;
string str = "ab123cdefg456,. HIJKLMN";
时,上面的代码会将str中的字符进行相应的转化,其余的字符不会发生变化。
模板函数transform在#include<algorithm>
中引入。原型如下:
template <class InputIterator, class OutputIterator, class UnaryOperator>
OutputIterator transform (InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperator op)
{
while (first1 != last1) {
*result = op(*first1); // or: *result=binary_op(*first1,*first2++);
++result; ++first1;
}
return result;
}
vector
vector
是将元素放到动态数组中加以管理的容器。
- 首先介绍基本的初始化方法,以及 front() , back() , push_back() , pop_back() 四个方法的用法。
push_back()添加元素时会进行拷贝一份。对于类对象,由于默认生成的拷贝构造函数执行浅拷贝,这样的话对于指针类型的成员变量,可能会存在原对象和副本对象指向同一块内存区域,在删除对象时两次释放这块区域的错误。stl中push_back和浅拷贝和深拷贝的问题.
vector的多种初始化方式,参考C++:vector 六种初始化方法.
//默认构造函数定义一个vector容器
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3); // 1 2 3
//修改头部元素的值(front()返回是引用,可以当左值)
v1.front() = 44; // 44 2 3
//修改尾部的值(back()返回是引用,可以当左值)
v1.back() = 99; // 44 2 99
//删除元素(从尾部删除)
v1.pop_back(); // 44 2
//迭代器遍历打印
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
cout << *it << " ";
}
cout << endl; // 44 2
// 其他初始化方法
//通过拷贝构造函数初始化
vector<int> v2 = v1;
vector<int> v2(v1);
//使用部分元素来构造
vector<int> v3(v1.begin(), v1.begin() + 2); // 1 2
vector<int> v4(v1.begin(), v1.end());
//指定元素个数
vector<int> v5(10); // v5中有10个元素,都是0
vector<int> v6(3,9); //v6中有3个元素,都是9
// 其他的初始化方式,已经运行过代码.这里可以查漏补缺
// 使用数组
vector<int> v7 = {3,4,6,3,7};
vector<int> v8{3,4,6,3,7};
vector
容器可以随机存取元素,也就是说支持[]
运算符和at
方式存取。这与string类的用法一致。 此外,迭代器提供了逆向遍历,可以通过迭代器来实现逆向遍历。
vector<int> v1;
for (int i = 0; i < 10; i++) {
v1.push_back(i);
}
//遍历-迭代器逆向遍历
for (vector<int>::reverse_iterator it = v1.rbegin(); it != v1.rend(); it++) {
cout << *it << " ";
}
cout << endl; // 9 8 7 6 5 4 3 2 1 0
- 删除/插入元素, 需要根据元素的迭代器位置进行。
// 接2测试,v1开始是 0 1 2 3 4 5 6 7 8 9
// 删除前3个元素
v1.erase(v1.begin(), v1.begin() + 3); // 3 4 5 6 7 8 9
// 删除指定位置的元素
v1.erase(v1.begin() +3); // 3 4 5 7 8 9
//在第3个位置插入数字600
v1.insert(v1.begin() + 3, 600); // 3 4 5 600 7 8 9
//在最后的位置插入3个元素10的拷贝
v1.insert(v1.end(), 3, 10); // 3 4 5 600 7 8 9 10 10 10
- 排序
greater()的用法[用sort+greater()实现降序快排].可以使用<algorithm>
中的sort
函数对vector进行排序。
#include <vector>
#include <algorithm>
// 默认从小到大排序
vector<int> vi;
vi.push_back(3);
vi.push_back(35);
vi.push_back(22);
sort(vi.begin(), vi.end());
// 这样,可以直接从大到小排序
sort(vi.begin(),vi.end(),greater<int>());
resize的原型:void resize (size_type n, value_type val = value_type());
resize()
的作用是改变vector中元素的数目。如果n
比当前的vector元素数目要小,vector的容量要缩减到n
大小,并移除那些超出n的元素。如果n
比当前vector元素数目要大,会在vector的末尾扩展需要的元素数目,如果第二个参数val
指定了,扩展的新元素初始化为val的副本,否则按类型默认初始化。
注意:
如果n大于当前的vector的容量(是容量capacity,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。
指定一维vector的大小
如果我们声明了一个vector:
vector v1;
此时我们不能使用v1[i]
来访问它。但是如果调用v1.resize(5);
我们就把v1的大小设置成了5,就可以使用v[i]
来访问它。
指定二维vector的大小
类似的,在对二维vector如
vector< <vector> vec;
如果使用vec[i].push_back()
会出现下标越界。这是因为此时的vec为空。解决办法是在push_back之前先定义vec的长度,如vec.resize(5)。
但是此时使用vec[i][j]
仍然会出现下标越界。如果想使用vec[i][j]
,需要同时指定vec
和vec[i]
的大小,此时用到一个临时一维vector:vector <vector<int> > vec; vector<int> temp(4) vec.resize(5,temp)
- resize和reserve的区别:
首先必须弄清楚两个概念:
capacity:指容器在分配新的存储空间之前能存储的元素总数。
size:指当前容器所存储的元素个数
在弄清这两个概念以后,很容易懂resize和reserve的区别:
reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。resize既分配了空间,也创建了对象。
reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。
两者的形参个数不一样。
resize带两个参数,一个表示容器大小,一个表示初始值(默认为0)
reserve只带一个参数,表示容器预留的大小。
deque
根据stl容器的统一迭代器访问方式,我们可以定义一个可以打印任何stl容器的模板函数(参考:使用模板打印任何容器的所有数据)。下面就只需要调用该函数就可以打印容器中的内容了~
template <typename T>
void print_stl(const T &obj){
for(typename T::const_iterator it=obj.begin();it!= obj.end();++it){
std::cout<<*it<<" ";
}
std::cout<<std::endl;
}
deque是一个双端数组容器:可以在头部和尾部操作元素。 使用deque,首先要引入头文件: #include<deque>"
.
//定义deque对象
deque<int> d;
//尾部插入元素
d.push_back(10);
d.push_back(20);
d.push_back(30);
print_stl(d); // 10 20 30
//头部插入元素
d.push_front(1);
d.push_front(2);
d.push_front(3);
print_stl(d); // 3 2 1 10 20 30
//尾部删除元素
d.pop_back();
print_stl(d); // 3 2 1 10 20
//头部删除元素
d.pop_front();
print_stl(d); // 2 1 10 20
//修改头部和尾部的值
d.front() = 111;
d.back() = 222;
print_stl(d); // 111 1 10 222
//查找元素为10的下标
//首先查找指向10的迭代器位置,然后通过distance求该迭代器距开始的距离。
for(deque<int>::iterator it = d.begin();it != d.end(); it++){
if (*it == 10) {
cout << "下标:" << distance(d.begin(), it) << endl;
}
} // 下标:2
queue
队列是一种先入先出的容器,首先引入 #include<queue>
queue<int> q;
q.push(10);
q.push(20);
q.push(30);
q.push(40);
while (!q.empty()){
//取出栈顶元素
cout << "当前队首元素" << q.front() << ", ";
//获取栈的大小
cout << "当前队列的大小" << q.size() << endl;
q.pop();
}
输出:
当前队首元素10, 当前队列的大小4
当前队首元素20, 当前队列的大小3
当前队首元素30, 当前队列的大小2
当前队首元素40, 当前队列的大小1
stack
栈是一种先入后出的容器,首先引入 #include<stack>
//定义stack对象
stack<int> s1;
//入栈
s1.push(10);
s1.push(20);
s1.push(30);
s1.push(40);
//打印栈顶元素,并出栈
while (!s1.empty()) {
//取出栈顶元素
cout << "当前栈顶元素" << s1.top() << ", ";
//获取栈的大小
cout << "当前栈的大小" << s1.size() << endl;
//出栈
s1.pop();
}
输出:
当前栈顶元素40, 当前栈的大小4
当前栈顶元素30, 当前栈的大小3
当前栈顶元素20, 当前栈的大小2
当前栈顶元素10, 当前栈的大小1
注意在queue和stack中:
print_stl(s1); 无法使用
cout << “弹出元素:” << s1.pop() << endl; 无法使用,因为pop()不会返回值。
list
list 是一个双向链表。 list节点的结构见如下源码:
template <class T>
struct __list_node{
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
}
首先引入#include<list>
//创建list对象
list<int> l;
//尾部添加元素
for (int i = 0; i < 5; i++) {
l.push_back(i+3);
}
print_stl(l); // 3 4 5 6 7
//头部添加元素
l.push_front(111); // 111 3 4 5 6 7
//list不能随机访问,要改变指针位置只能++或--
list<int>::iterator it = l.begin();
it++;
it++;
it++;
cout << *it <<endl; // 5
it--;
cout << *it <<endl; // 4
// it = it + 1; //编译报错,不能随机访问
// it = it + 5; //编译报错,不能随机访问
// erase删除元素通过指针来实现
// 删除区间内的,it此时指向4
l.erase(l.begin(), it);
print_stl(l); // 4 5 6 7
// 删除单个
l.erase(l.begin());
print_stl(l); // 5 6 7
//romove按移除值为5的所有元素
l.push_back(5);
print_stl(l); // 5 6 7 5
l.remove(5);
print_stl(l); // 6 7
此外,其他方法(参考C++ list,STL list(双向链表)详解。 除了顺序容器都有的成员函数外, 还有:
void pop_front() | 删除链表最前面的元素 |
---|---|
void sort() | 将链表从小到大排序 |
remove_if | 删除符合某种条件的元素 |
void unique() | 删除所有和前一个元素相等的元素 |
void merge(list & x) | 将链表 x 合并进来并清空 x。要求链表自身和 x 都是有序的 |
void splice(iterator i, list & x, iterator first, iterator last) | 在位置 i 前面插入链表 x 中的区间 [first, last),并在链表 x 中删除该区间。链表自身和链表 x 可以是同一个链表,只要 i 不在 [first, last) 中即可 |
由于list的双向特性,其支持在头部(front)和尾部(back)两个方向进行push和pop操作,当然还支持 erase,splice,sort,merge,reverse,sort等操作,这里不再详细阐述。
priority_queue
首先需要包含头文件#include<queue>
。
priority_queue<int>
默认定义int类型的最大值队列,等价于priority_queue<int, vector<int>, less<int>>
定义int型的最大值优先队列。而priority_queue<int, vector<int>, greater<int>>
定义int型的最小值队列。 上面的定义中,less
和greater
相当于谓词,是预定义好的排序函数,我们称之为“仿函数”。
/定义优先级队列(默认是最大值优先级队列)
priority_queue<int> p1;
//队列入队
p1.push(5);
p1.push(3);
p1.push(7);
p1.push(1);
p1.push(9);
// 打印最大优先级的队头元素
cout<<"最大值优先级,队列的大小:"<< p1.size() <<endl;
while (!p1.empty()){
//取出队首元素
cout << p1.top() << " ";
p1.pop();
}
cout<<endl;
// 与上相反,值越小优先级越大
priority_queue<int, vector<int>, greater<int>> p2;
//给最小值优先级队列入栈
p2.push(5);
p2.push(3);
p2.push(7);
p2.push(1);
p2.push(9);
//打印最小优先级队列的对头元素
cout<<"最小值优先级,队列的大小:"<< p2.size() <<endl;
while (!p2.empty()){
//取出队首元素
cout << p2.top() << " ";
p2.pop();
}
cout<<endl;
除了仿函数,对于自定义的数据类型,还可以使用一种轻便的比较函数定义方法。以pair为例,定义如下:
typedef pair<int, int> P;
auto cmp = [](const P& a, const P& b){ return a.second < b.second; };
priority_queue<P, vector<P>, decltype(cmp)> que(cmp);
其中,decltype(cmp)
可以推测得到cmp的类型。关于仿函数的自定义,在后面set部分会讲到。
set
set中元素唯一且有序。其结构是红黑二叉树,插入数据的效率比vector快 。
- 插入元素,删除元素
int rand(void);产生0~RAND_MAX(32767)的伪随机整数。C++ 随机数。
set<int> s;
//插入元素
for (int i = 0; i<5; i++) {
int tmp = rand();
s.insert(tmp);
}
print_stl(s); // 41 6334 18467 19169 26500
//重复插入元素(只会保留一个)
s.insert(100);
s.insert(100);
print_stl(s); // 41 100 6334 18467 19169 26500
// 删除元素
// 此时,s.erase(100)也是可以的.
cout <<"删除前两个元素"<< endl;
set<int>::iterator it = s.begin();
it ++;
it ++;
s.erase(s.begin(),it);
print_stl(s); // 6334 18467 19169 26500
//依次删除集合中的元素
while(!s.empty())
{
set<int>::iterator it = s.begin();
cout <<"删除"<< *it << endl;
//删除头部元素
s.erase(s.begin());
print_stl(s); // 集合一次次在缩短
}
那么,对于上面重复插入100,最后只保留了一个结果的情况,如何知道,是否插入成功了呢?
可以根据set的insert操作的返回值得到。insert的声明为:
pair<iterator,bool> insert(const value_type& __v)
其中,pair是一个能盛放两个值的结构体,而且这两个值可以是不同的类型。它的定义是个模板:
template <class _T1, class _T2>
struct pair
{
typedef _T1 first_type;
typedef _T2 second_type;
_T1 first;
_T2 second;
}
那么,返回的pair<iterator, bool>
类型中,pair的第一个属性表示当前插入的迭代器的位置,第二个属性表示插入是否成功的bool值。所以,我们可以通过第二个属性来判断元素是否插入成功。
set<int> s;
//重复插入元素
for(int i = 0;i<3;i++){
cout<< "第"<<i+1<<"次";
pair<set<int>::iterator, bool> ps = s.insert(100);
if(ps.second == true){
cout << "插入100成功" <<endl;
}else{
cout << "插入失败" <<endl;
}
print_stl(s);
}
输出结果:
第1次插入100成功
100
第2次插入失败
100
第3次插入失败
100
- 改变set中的元素排列顺序: 设置从大道小排序
//从大到小
set<int, greater<int>> s2;
//添加元素
for (int i = 0; i < 5; i++) {
s2.insert(rand());
}
//遍历
cout<<"从大到小s2: "<<endl;
print_stl(s2); // 输出: 26500 19169 18467 6334 41
为什么上面的可以呢?因为greater<int>
是一个仿函数,可以用于设置集合元素的排序规则。
参考C++:greater和less: greater表示内置类型从大到小排序,less表示内置类型从小到大排序。
参考关联容器也可使用greater<int>来改变大小排序顺序: greater配合sort使用, 配合容器使用。
// 去除模板定义的简化版本,int类型变量的比较函数定义
struct greater
{
//重载了括号操作符,用来比较大小
bool operator()(const int &left, const int &right) const
{
//如果左值>右值,为真。从大到小的排列
return left > right;
}
};
// 完整版本(模板), 调用函数时:greater<int>,int换为其他内置类型也都可以
template<class _Ty = void>
struct greater
{ // functor for operator>
typedef _Ty first_argument_type;
typedef _Ty second_argument_type;
typedef bool result_type;
constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
{ // apply operator> to operands
return (_Left > _Right);
}
};
那么,对于自定义的类,在定义了类似的比较函数后,也可以加入到set中,使之唯一且有序排列。下面定义一个Student类,并将它放进set中。
- 自定义类,放进set中。
首先,定义类,定义类的用于比较的仿函数:
class Student {
public:
char name[64]; // 变量定义为private将不能在外部访问
int age;
public:
Student(const char *name, int age)
{
strcpy(this->name, name);
this->age = age;
}
};
// 仿函数
struct Scomp
{
bool operator() (const Student &left, const Student &right)
{
// 从小到大按照年龄排序
return left.age < right.age;
}
};
然后,放入set中,遍历:
set<Student, Scomp> ss;
//插入数据
ss.insert(Student("s1",55));
ss.insert(Student("s2",33));
ss.insert(Student("s3",99));
ss.insert(Student("s4",11));
ss.insert(Student("s5",77));
ss.insert(Student("s5",77));
//遍历
for (set<Student>::iterator it = ss.begin(); it != ss.end(); it++) {
cout << it->age << "\t" << it->name <<endl;
}
输出结果:
11 s4
33 s2
55 s1
77 s5
99 s3
另外,上面第一部分,定义类+定义仿函数,等价于直接下面这样定义。下面这样定义后,可以直接set<Student> ss;
来定义集合。这样定义的最大优势在于,即使数据设置为private,比较也能进行,因为类的成员函数可以访问到它们。
class Student {
public:
char name[64];
int age;
public:
Student(const char *name, int age)
{
strcpy(this->name, name);
this->age = age;
}
bool operator<(const Student &other)const
{
return this->age < other.age;
}
}
- 查找元素
iterator find(const key_type& __k)
查找元素为k的迭代器位置;
iterator lower_bound(const key_type& __k)
函数查找大于等于元素k的迭代器位置;
iterator upper_bound(const key_type& __k)
函数查找大于元素k的迭代器位置;
pair<iterator,iterator> equal_range(const key_type& __k)
返回一个pair类型,第一个属性表示大于等于k的迭代器位置,第二个是大于k的迭代器位置.
set<int> s;
for (int i = 0; i < 5; i++) {
s.insert(i+3);
}
print_stl(s); //3 4 5 6 7
// find: 用于查找元素是5的迭代器位置
set<int>::iterator it0 = s.find(5);
cout << "s.find(5):" << *it0 <<endl; // s.find(5):5
//lower_bound: 查找不小于5的元素迭代器位置
set<int>::iterator it1 = s.lower_bound(5);
cout << "不小于5:" << *it1 <<endl; // 5
//upper_bound: 查找大于5的元素迭代器位置
set<int>::iterator it2 = s.upper_bound(5);
cout << "大于5:" << *it2 <<endl; // 6
//equal_range: 返回的pair第一个迭代器是>=5,另一个是>5
pair<set<int>::iterator, set<int>::iterator> mypair = s.equal_range(5);
set<int>::iterator it3 = mypair.first;
set<int>::iterator it4 = mypair.second;
cout << "it3:" << *it3 <<endl; // 5
cout << "it4:" << *it4 <<endl; // 6
int num1 = s.count(5);
cout << "num1:" << num1 << endl; // 1
// 当待查找的元素不存在时
s.erase(5);
print_stl(s); // 3 4 6 7
it0 = s.find(5);
cout << "s.find(5):" << *it0 <<endl; // 4 find的处的迭代器位置不可靠
//查找不小于5的迭代器
it1 = s.lower_bound(5);
cout << "不小于5:" << *it1 <<endl; // 6
//查找大于5的迭代器
it2 = s.upper_bound(5);
cout << "大于5:" << *it2 <<endl; // 6
//返回的pair第一个迭代器是>=5,另一个是>5
mypair = s.equal_range(5);
it3 = mypair.first;
it4 = mypair.second;
cout << "it3:" << *it3 <<endl; // 6
cout << "it4:" << *it4 <<endl; // 6
num1 = s.count(5);
cout << "num1:" << num1 << endl; // 0
- multiset容器
multiset容器,与set容器相似,但是multiset容器中的元素可以重复。另外,他也是自动排序的,容器内部的值不能随便修改,因为有顺序的。
map
引入#include<map>
。 map中的键值对都是唯一的,即不能出现同名键,而且一个键对应一个值(multimap中一个键可以对应多个值 )。
- 首先介绍基本的插入、删除操作。map中的元素都是键值对,以pair的结构存在。为了方便打印map中的内容,定义一个模板函数:
// 打印map类型的数据
template <typename T>
void print_map(const T& m){
for (typename T::const_iterator it = m.begin(); it != m.end(); it++ ){
if (it != m.begin()){std::cout<<",";}
std::cout << it->first << ":" << it->second;
}
std::cout<<std::endl;
}
map元素的插入,有两种方式:
调用
insert
函数插入pair类型的键值对直接使用
[]
来插入键并插入值;如果要插入的键已经存在,则修改值就完事了。
map<int, string> m;
//insert方法插入
//--1 通过pair<int, string>(1,"One") 构造pair元素
m.insert(pair<int, string>(1,"One"));
//--2 通过make_pair构造pair元素
m.insert(make_pair(2,"Two"));
//--3 通过value_type构造pair元素
m.insert(map<int, string>::value_type(3,"Three"));
//[]直接插入
m[4] = "Four";
print_map(m); //遍历
//重复插入(插入会不成功)
pair<map<int, string>::iterator, bool> pair1 = m.insert(make_pair(2, "haha"));
if (pair1.second) {
cout << "重复插入成功!" << endl;
}else{
cout << "重复插入失败!" << endl;
}
//元素的修改(成功)
// m[2] = "haha";的方式,如果不存在键则插入,存在键则修改
m[2] = "haha";
print_map(m); //遍历
//元素的删除
//--删除值为"haha"的元素
cout<<"删除值为haha的键值对:"<<endl;
for (map<int, string>::iterator it = m.begin(); it != m.end(); it++) {
if (it->second.compare("haha") == 0) {
m.erase(it);
}
}
print_map(m); //遍历
输出:
1:One,2:Two,3:Three,4:Four
重复插入失败!
1:One,2:haha,3:Three,4:Four
删除值为haha的键值对:
1:One,3:Three,4:Four
-
查找
find,lower_bound,upper_bound,equal_range。
首先, 用find函数来定位数据出现位置,它返回的一个迭代器,当数据出现时,它返回数据所在位置的迭代器,如果map中没有要查找的数据,它返回的迭代器等于end函数返回的迭代器。 c++ 中map 的find 用法.
// 接上面的测试,m中的值为 1:One,3:Three,4:Four
map<int, string>::iterator it = m.find(3);
cout << it->first << "\t" << it->second << endl;
//查找key=100
it = m.find(100);
if (it != m.end()) {
cout << "存在key=100的键值对";
}else{
cout << "不存在" << endl;
}
输出:
3 Three
不存在
其次,看lower_bound,upper_bound。参考stl map中的lower_bound和 upper_bound:map中的lower_bound和upper_bound的意思其实很简单,就两句话:
map::lower_bound(key):返回map中第一个大于或等于key的迭代器指针
map::upper_bound(key):返回map中第一个大于key的迭代器指针
所以,理解这两个函数请不要按照字面意义思考太复杂,因为仅仅是不小于(lower_bound)和大于(upper_bound)这么简单。
// 接上面的测试,m中的值为 1:One,3:Three,4:Four
//lower_bound
map<int, string>::iterator it1 = m.lower_bound(2);
cout << it1->first << "\t" << it1->second << endl; // 3 Three
map<int, string>::iterator it2 = m.lower_bound(3);
cout << it2->first << "\t" << it2->second << endl; // 3 Three
//upper_bound
map<int, string>::iterator it3 = m.upper_bound(2);
cout << it3->first << "\t" << it3->second << endl; // 3 Three
map<int, string>::iterator it4 = m.upper_bound(3);
cout << it4->first << "\t" << it4->second << endl; // 4 Four
// 不存在时
map<int, string>::iterator it5 = m.lower_bound(5);
if(it5 == m.end()){
cout<< "大于等于5的值不存在"<<endl;
}else{
cout << it5->first << "\t" << it5->second << endl;
} // 大于等于5的值不存在
最后,equal_range。返回两个指针组成的pair,其中第一个指向的键不小于,第二个指向的键大于。
// 接上面的测试,m中的值为 1:One,3:Three,4:Four
// equal_range
pair<map<int, string>::iterator, map<int, string>::iterator> mypair2 = m.equal_range(2);
cout << "equal_range(2): " << mypair2.first->first << "\t" << mypair2.second->first << endl;
auto mypair3 = m.equal_range(3);
cout << "equal_range(3): " << mypair3.first->first << "\t" << mypair3.second->first << endl;
// 边缘元素的返回值
auto mypair = m.equal_range(4);
if (mypair.first == m.end()) {
cout << "大于等于4的位置不存在" << endl;
}else{
cout << mypair.first->first << "\t" << mypair.first->second << endl;
}
if (mypair.second == m.end()) {
cout << "大于4的位置不存在" << endl;
}else{
cout << mypair.second->first << "\t" << mypair.second->second << endl;
}
// 元素不存在时
auto mypair5 = m.equal_range(5);
if (mypair5.first == m.end()) {
cout << "大于等于5的位置不存在" << endl;
}else{
cout << mypair5.first->first << "\t" << mypair5.first->second << endl;
}
if (mypair5.second == m.end()) {
cout << "大于5的位置不存在" << endl;
}else{
cout << mypair5.second->first << "\t" << mypair5.second->second << endl;
}
输出:
equal_range(2): 3 3
equal_range(3): 3 4
4 Four
大于4的位置不存在
大于等于5的位置不存在
大于5的位置不存在
- map是有默认值的
参考C++——自定义map的value默认值:当取一个不存在的key值的value时:如果value为内置类型,其值将被初始化为0;如果value为自定义数据结构且用户定义了默认值则初始化为默认值,否则初始化为0。
// 内置类型
map<int,int> emptyMap{};
int i = emptyMap[100]; // i = 0
// 自定义了默认值
struct IntDefaultedToOne {
int num = 1;
};
map<int,IntDefaultToOne> emptyMap{};
int i = map[0].num; // i = 1
multimap
multimap容器,与map容器的区别是:
multimap支持多个键值。 由于支持多个键值,multimap提供了
count
函数来计算同一个key的元素个数。不能用
[]
插入键值对。
multimap<int, string> m2;
//sale部门
m2.insert(make_pair(1, "one"));
m2.insert(make_pair(1, "Jan"));
m2.insert(make_pair(1, "first"));
// m2[2] = "two"; 无法使用[]运算符
// m2[2] = "Feb";
// m2[2] = "second";
m2.insert(pair<int, string>(3,"three"));
m2.insert(pair<int, string>(3,"March"));
m2.insert(pair<int, string>(3,"third"));
//遍历
for (multimap<int, string>::iterator it = m2.begin(); it != m2.end(); it++) {
cout << it->first << "\t" << it->second << endl;
}
cout << "键为3的键值对个数:" << (int)m2.count(3) << endl;
// 使用equal_range查看这些
auto range3 = m2.equal_range(3);
for (multimap<int,string>::iterator it = range3.first; it != range3.second; it++){
cout << it->first << "\t" << it->second<<endl;
}
输出结果:
1 one
1 Jan
1 first
3 three
3 March
3 third
键为3的键值对个数:3
3 three
3 March
3 third
unordered_map
参考C++ unordered_map的使用: 它允许根据键快速检索单个元素。在内部,unordered_map
使用散列表来组织它们的元素,以比map容器能更快地通过它们的键访问单个元素,但是它们通常对于元素子集的范围迭代效率较低。
template < class Key, // 键,um::key_type可得
class T, // 值,um::mapped_type
class Hash = hash<Key>, // 返回一个散列值的一元函数,um::hasher
class Pred = equal_to<Key>, // 判断两个键是否相同的函数,um::key_equal
class Alloc = allocator< pair<const Key,T> > // 容器,um::allocator_type
> class unordered_map;
无序映射实现了直接访问操作符(operator[]),使用键值作为参数直接访问映射值。 容器中的任何两个元素都不能具有相同的键。
容器中的迭代器至少是前向迭代器forward iterators。
查找一个元素是否在map中C++学习之map与unordered_map的使用与注意事项:
unordered_map<key, value> mp; //或map<key,value> mp;
mp.find(x)!=mp.end()
mp.count(x)!=0
C++中unordered_map的使用: unordered_map
是类似于map的关联容器,其中存储的是键值对pair。哈希表的搜索、插入和元素移除拥有平均常数时间复杂度,元素在内部的存储是没有顺序的,而是通过键的哈希来确定元素具体放在具体的某个位置。
函数名 | 函数作用 |
---|---|
empty | 判定容器是否为空 |
size | 返回容器的元素 |
max_size | 返回可容纳的最大元素数 |
clear | 清除内容 |
insert | 插入元素或者结点 |
emplace | 原位构造元素 |
erase | 擦除元素 |
swap | 交换内容 |
at | 访问指定位置的元素,并且进行越界检查 |
count | 返回匹配特定键的元素数量 |
find | 返回特定键的元素 |
疑难点
++it、it++
++
前置会返回一个对象本身。后置++
则必须产生临时对象以保留原来的值并返回。 临时对象会导致效率降低 。
// ++i实现代码为:
int& operator++()
{
*this += 1;
return *this;
}
//i++实现代码为:
int operator++(int)
{
int temp = *this;
++*this;
return temp;
}
list删除元素
使用erase删除一个元素,会返回下一个元素的有效迭代器。
//创建list对象
list<int> l;
//尾部添加元素
for (int i = 0; i < 9; i++) {
l.push_back(i+3);
}
print_stl(l); // 3 4 5 6 7 8 9 10 11
// erase删除元素通过指针来实现
// 删除区间内的
list<int>::iterator it = l.begin();
it++;
it++;
auto it2 = l.erase(it);
print_stl(l); // 3 4 6 7 8 9 10 11
cout<< "删除第3个元素*it"<<endl;
cout<< "*it " << *it << endl; // 5
cout<< "*it2 " << *it2 << endl; // 6
cout<< "*(++it) " << *(++it) << endl; // 2003788910 it已失效
cout<< "*(++it2) " << *(++it2) << endl; // 7
// 删除一个区间的
auto it3 = l.erase(l.begin(),it2);
print_stl(l); // 7 8 9 10 11
cout<< "删除前3个元素begin(),it2"<<endl;
cout<< "*it2 " << *it2 << endl; // 7
cout<< "*it3 " << *it3 << endl; // 7
cout<< "*(++it2) " << *(++it2) << endl; // 8
cout<< "*(++it3) " << *(++it3) << endl; // 8
// romove删除元素通过值来实现
cout<< "remove 9"<<endl;
l.remove(9); // 7 8 10 11
print_stl(l);
3 4 5 6 7 8 9 10 11
3 4 6 7 8 9 10 11
删除第3个元素*it
*it 5
*it2 6
*(++it) 2003788910
*(++it2) 7
7 8 9 10 11
删除前3个元素begin(),it2
*it2 7
*it3 7
*(++it2) 8
*(++it3) 8
remove 9
7 8 10 11
slist(forward_list)
(single linked list)是单向链表, 迭代器属于单向的Forward iterator。list是双向链表。 虽然slist的很多功能不如list灵活,但是其所耗用的空间更小,操作更快。
根据STL的习惯,插入操作会将新元素插入到指定位置之前,而非之后,然而slist是不能回头的,只能往后走,因此slist特别提供了insert_after()和erase_after供灵活应用。在slist开头是可取的,slist只提供push_front()操作,元素插入到slist后,存储的次序和输入的次序是相反的。
forward_list<int> fl;
fl.push_front(1);
fl.push_front(3);
fl.push_front(2);
fl.push_front(6);
fl.push_front(5);
// 链表的实际存放顺序与插入相反。
for(forward_list<int>::iterator it1 = fl.begin();it1 != fl.end(); ++it1)
{
cout << *it1 <<" "; // 5 6 2 3 1
}
cout << endl;
// 在2的位置后插入99
forward_list<int>::iterator it1 = find(fl.begin(), fl.end(), 2); //寻找2的位置
if (it1 != fl.end())
fl.insert_after(it1, 99);
for (auto it : fl)
{
cout << it << " "; //5 6 2 99 3 1
}
cout << endl;
// 删除6后面的元素
it1 = find(fl.begin(), fl.end(), 6); //寻找6的位置
if (it1 != fl.end())
fl.erase_after(it1);
print_stl(fl); // 5 6 99 3 1
// 删除头部元素
fl.pop_front();
print_stl(fl); // 6 99 3 1
// 取头部元素,并改变头部元素
cout<< fl.front() << endl; // 6
fl.front() = 20;
print_stl(fl); // 20 99 3 1
stack的实现
stack(栈)是一种先进后出(First In Last Out)的数据结构,只有一个入口和出口,那就是栈顶,除了获取栈顶元素外,没有其他方法可以获取到内部的其他元素。
template <class T, class Sequence = deque<T> >
class stack
{
...
protected:
Sequence c;
public:
bool empty(){return c.empty();}
size_type size() const{return c.size();}
reference top() const {return c.back();}
const_reference top() const{return c.back();}
void push(const value_type& x){c.push_back(x);}
void pop(){c.pop_back();}
};
从stack的数据结构可以看出,其修改了deque数据结构,关闭一些访问接口成为adapter(配接器)( 常将其归类为container adapter而非container) 。 stack除了默认使用deque作为其底层容器之外,也可以使用双向开口的list,只需要在初始化stack时,将 list作为第二个参数即可。
stack<int,list<int>> sls;
for(int i=0;i<5;++i){
sls.push(i*2+1);
}
while (!sls.empty()) {
cout << "当前栈顶元素" << sls.top() << ", ";
cout << "当前栈的大小" << sls.size() << endl;
sls.pop();
}
输出:
当前栈顶元素9, 当前栈的大小5
当前栈顶元素7, 当前栈的大小4
当前栈顶元素5, 当前栈的大小3
当前栈顶元素3, 当前栈的大小2
当前栈顶元素1, 当前栈的大小1
queue的实现
queue(队列)是一种先进先出(First In First Out)的数据结构,只有一个入口和一个出口。默认也是基于deque实现,同样也可以由list改写:
template <class T, class Sequence = deque<T> >
class queue
{
...
protected:
Sequence c;
public:
bool empty(){return c.empty();}
size_type size() const{return c.size();}
reference front() const {return c.front();}
const_reference front() const{return c.front();}
void push(const value_type& x){c.push_back(x);}
void pop(){c.pop_front();}
};
priority_queue实现
默认情况下,priority_queue使用一个max-heap完成,底层容器使用的是一般为vector,堆heap 为处理规则来管理底层容器实现 。priority_queue的这种实现机制导致其不被归为容器,而是一种容器配接器。关键的源码如下:
template <class T, class Squence = vector<T>,class Compare = less<typename Sequence::value_type> >
class priority_queue{
...
protected:
Sequence c; // 底层容器
Compare comp; // 元素大小比较标准
public:
bool empty() const {return c.empty();}
size_type size() const {return c.size();}
const_reference top() const {return c.front()}
void push(const value_type& x)
{
c.push_heap(x);
push_heap(c.begin(), c.end(),comp);
}
void pop()
{
pop_heap(c.begin(), c.end(),comp);
c.pop_back();
}
};
heap
heap(堆)并不是STL的容器组件,是priority queue(优先队列)的底层实现机制。binary heap本质是一种complete binary tree(完全二叉树),binary max heap(大根堆)总是最大值位于堆的根部,优先级最高。
整棵binary tree除了最底层的叶节点之外,都是填满的,但是叶节点从左到右不会出现空隙, 因此可以使用array或vector来存储所有的节点。当其中某个节点位于 i i i处,其左节点必定位于 2 i 2i 2i处,右节点位于 2 i + 1 2i+1 2i+1处,父节点位于 i / 2 i/2 i/2(向 下取整)处。这种以array表示tree的方式称为隐式表述法。
那heap算法有哪些?常见有的插入、弹出、排序和构造算法。
为了方便打印array或vector中的堆元素,先定义一个打印函数,如下:
template <typename T>
void print_ele(const T & h){
for (auto v : h)
{
std::cout << v << " "; // 6 4 5 3 1 0 2
}
std::cout << std::endl;
}
然后,进行插入、弹出、排序和构造算法。
// 注意下面的操作,v.XX是vector容器的成员函数操作,函数调用是stl算法操作。
// 将vector建立为堆
vector<int> v = { 0,1,2,3,4,5,6 };
make_heap(v.begin(), v.end()); //以vector为底层容器
print_ele(v); // 6 4 5 3 1 0 2
// 添加一个元素,并使之仍然是堆,2步完成.
v.push_back(7); // 先后面插入
push_heap(v.begin(), v.end()); // 该函数将最后一个元素进行排序
print_ele(v); // 7 6 5 4 1 0 2 3
// pop_heap弹出根节点,实际上是移到了vector的最后一个位置
pop_heap(v.begin(), v.end());
cout << v.back() << endl; // 7
// vector的成员方法pop_back可以删除元素,早就知道了~~
v.pop_back();
print_ele(v); // 6 4 5 3 1 0 2
// sort得到一个递增序列,调用pop_heap每次确定一个元素位置.
sort_heap(v.begin(), v.end());
print_ele(v); // 0 1 2 3 4 5 6
- 默认是大根堆,排序后将是升序排列
参考make_heap(), pop_heap(), push_heap()用法.观察上面的make_heap,push_heap,pop_heap等方法,格式都是一样的。自定义排序,只需要添加一个比较函数即可。需要注意的是,添加时,上面三个函数就都需要添加一样的比较函数。
vector<int> nums({8, 3, 4, 8, 9, 2, 3, 4, 10};);
make_heap(nums.begin(), nums.end(), less<int> ());
make_heap(nums.begin(), nums.end(), greater<int> ());
或者
make_heap(nums.begin(), nums.end(),[](const int& a,const int& b){return a>b;});