主要内容参考的黑马STL课程,附有个人注解
目录
- 1. STL简介
- 2. STL三大组件
- 3. 常用容器(详情见STL基础教程)
- 4. 算法
- ————————随机补充修正————————
1. STL简介
1.1 STL基本概念
STL(Standard Template Library,标准模板库),是惠普实验室开发的一系列软件的统称。现在主要出现在c++中,但是在引入c++之前该技术已经存在很长时间了。STL从广义上分为:容器(container) 算法(algorithm) 迭代器(iterator),容器和算法之间通过迭代器进行无缝连接。STL几乎所有的代码都采用了模板类或者模板函数,这相比传统的由函数和类组成的库来说提供了更好的代码重用机会。STL(Standard Template Library)标准模板库,在我们c++标准程序库中隶属于STL的占到了80%以上。
注意,即使在使用时引入了对应的头文件,但实际上这些头文件的实体也都定义在std命名空间内,故依然要用std::
1.2 STL六大组件
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
1.容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器是一种class template。
2.算法:各种常用的算法,如sort、find、copy、for_each。从实现的角度来看,STL算法是一种function tempalte。
3.迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator* , operator-> , operator++,operator–等指针相关操作予以重载的class template。所有STL容器都附带有自己专属的多种功能的迭代器(因为通常里面装的数据类型都是用户自定义的),只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)就是一种迭代器(且是最强大的随机访问迭代器),迭代器就是用来访问容器里的元素,重载了指针的部分或全部功能,更广义的指针。
4.仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数就是一种重载了operator()的class 或者class template。
5.适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。就像现实中的各种适配器:电源适配器、蓝牙适配器,用来适配两端的通信,把原本不匹配的接口修饰成匹配的,如多合一,一分多。
6.空间配置器:负责(容器)空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte。
1.3 STL的优点
STL的一个重要特性是将数据和操作分离,在设计时两者互不干涉。数据由容器类别加以管理,操作则由可定制的算法定义,迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作。
2. STL三大组件
2.1 容器(container)
几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL容器就是把运用最广泛的一些数据结构实现出来。
常用的数据结构:数组(array),链表(list),树(tree),栈(stack),队列(queue),集合(set),映射表(map)。根据数据在容器中的排列特性,这些数据分为序列式容器和关联式容器两种。序列式容器强调固定的顺序,除非进行了增删改操作,否则数据取出时会和插入的顺序一致;关联式容器是非线性的(二叉)树结构,物理上没有严格顺序,故保存时也没有记录插入时的逻辑顺序,通常还会根据自身的特性将插入的数据排序,因此关联式容器还有另一个特点,就是由于需要有排序的规则,通常数据会有一个键值可以起到索引的作用,便于查找,为了保持该排序规则,不能用迭代器改变键值。
容器可以嵌套,学校>班级>小组。
2.2 算法(algorithm)
算法分为:质变算法和非质变算法。
质变算法:是指运算过程中会更改区间内的元素内容,例如拷贝,替换,删除。
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、求最值。
算法主要有头文件<algorithm>,包含常用算法;<functional>,包含仿函数的模板类;<numeric>,包含简单的小型算法。
2.3 迭代器(iterator)
迭代器(iterator)是一种抽象的设计概念,现实程序语言中并没有直接对应于这个概念的实物。其具体定义为:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。例如vector容器的方法begin()即称为起始迭代器,指向vector中第一个元素位置(注意不是指向元素,解引用*v.begin()才是第一个元素)。由于迭代器就相当于指针,故当容器出现空间重新分配时,之前接收迭代器的变量就失效了,要重新获取。
迭代器的种类:
输入迭代器 | 提供对数据的只读访问 | 只读,支持++、==、!= |
---|---|---|
输出迭代器 | 提供对数据的只写访问 | 只写,支持++ |
前向迭代器 | 提供读写操作,并能向前推进迭代器 | 读写,支持++、==、!= |
双向迭代器 | 提供读写操作,并能向前和向后操作 | 读写,支持++、–, |
随机访问迭代器 (相当于原生指针) | 提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器 | 读写,支持++、–、[n]、 +/-n、<、<=、>、>= |
主要使用双向和随机访问迭代器,如果迭代器支持+/-n不报错,就说明该容器提供的是随机访问迭代器,其他种类判断同理。若一个容器不提供随机访问迭代器,则其通常也不能使用algorithm库里的算法,但一般容器内会提供对应算法。
创建指定容器的迭代器对象来接收该容器的迭代器:
vector::iterator it = vec.begin();
在创建迭代器对象时常见有以下类名称规则:
iterator普通迭代器;
reverse_iterator 反转迭代器,可以接受特定迭代器,如v.rbegin(),v.rend();
const_iterator 只读迭代器,可以保证迭代器指向的元素无法修改,防止容器元素被更改;
等等
注意迭代器只是一种方法,会返回指针但并不是指针,即v.begin()++后无论接不接收,或接收后做改值操作,再次使用v.begin()时还是获取v的第一个元素位置。
3. 常用容器(详情见STL基础教程)
3.1 string容器
3.1.1 string容器基本概念
C风格字符串(以空字符结尾的字符数组)太过复杂难于掌握,不适合大程序的开发,所以C++标准库定义了一种string类,定义在头文件<string>。
String和c风格字符串对比:
1.Char是一个指针,String是一个类。string封装了char,管理这个字符串,是一个char*型的容器。
2.String封装了很多实用的成员方法及其重载版本。查找find,拷贝copy,删除delete 替换replace,插入insert
3.不用考虑内存释放和越界。string管理char*所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。
3.1.2 string容器常用操作
3.1.2.1 string构造函数
string();//默认构造,创建一个空的字符串 例如: string str;`
string(const string& str);//显示拷贝构造,使用一个string对象初始化另一个string对象 `
string(const char* s);//使用字符串s初始化 string(int n, char c);//使用n个字符c初始化`
3.1.2.2 string基本赋值操作
string& operator=(const char* s);//char*类型字符串 赋值给当前的字符串
string& operator=(const string &s);//把字符串s赋给当前的字符串
string& operator=(char c);//字符赋值给当前的字符串
string& assign(const char *s);//把字符串s赋给当前的字符串
string& assign(const char *s, int n);//把字符串s的前n个字符赋给当前的字符串
string& assign(const string &s);//把字符串s赋给当前字符串
string& assign(int n, char c);//用n个字符c赋给当前字符串
string& assign(const string &s, int start, int n);//将s从start开始n个字符赋值给字符串
3.1.2.3 string存取字符操作
char& operator[](int n);//通过[]方式取字符,如str[i]
char& at(int n);//通过at方法获取字符,如str.at(i)
//两者区别在于[]越界会直接挂掉,at越界会抛出out_of_range异常
3.1.2.4 string拼接操作
string& operator+=(const string& str);//重载+=操作符
string& operator+=(const char* str);//重载+=操作符
string& operator+=(const char c);//重载+=操作符
string& append(const char *s);//把字符串s连接到当前字符串结尾
string& append(const char *s, int n);//把字符串s的前n个字符连接到当前字符串结尾
string& append(const string &s);//同operator+=()
string& append(const string &s, int pos, int n);
//把字符串s中从pos开始的n个字符连接到当前字符串结尾
string& append(int n, char c);//在当前字符串结尾添加n个字符c
3.1.2.5 string查找和替换
int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos = 0) const; //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const; //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos = 0) const; //查找字符c第一次出现位置
int rfind(const string& str, int pos = npos) const;
//查找str最后一次位置,从pos开始查找,也就是从后往前找,r开头通常都是反向
int rfind(const char* s, int pos = npos) const;//查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const;//从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符c最后一次出现位置
string& replace(int pos, int n, const string& str);
//替换从pos开始n个字符为字符串str,是这n个字符整个替换为字符串str,原字符串可能会因此变长或变短
string& replace(int pos, int n, const char* s); //替换从pos开始的n个字符为字符串s
3.1.2.6 string比较操作
/* compare函数在>时返回 1,<时返回 -1,==时返回 0。
按照ASCII码比,故比较区分大小写,比较时参考字典顺序,排越前面的越小。
大写的A比小写的a小。 */
int compare(const string &s) const;//与字符串s比较
int compare(const char *s) const;//与字符串s比较
3.1.2.7 string子串
string substr(int pos = 0, int n = npos) const;//返回由pos开始的n个字符组成的字符串
//实际使用时n通常是一个变量,可以配合str.find()来找到标志符号的位置,如邮箱中的@
解析字符串时常用,注意该方法是返回一个新的string,不会改变原有string,故如果需要多次截取子串时,要改变pos值。如用.做分隔符来解析一个网址,每次pos应当是上个find(“.”)的值,具体示例如下:
string str = "www.itcast.com.cn";
vector<string> v;//将www itcast con cn截取到vector容器中
int start = 0;
int pos = -1;
while(true)
{
pos = str.find(".",start);
if(pos == -1)//全部找完了
{
//将最后剩的cn截取出来
string tempStr = str.substr(start,str.size()-start);
v.push_back(tempStr);
break;
}
string tempStr = str.substr(start, pos - start);
v.push_back(tempStr);
start = pos + 1;
}
3.1.2.8 string插入和删除操作
string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n, char c); //在指定位置插入n个字符c
string& erase(int pos, int n = npos); //删除从Pos开始的n个字符
3.1.2.9 string和c-style字符串转换
//string 转 char*
string str = "itcast";
const char* cstr = str.c_str();
//char* 转 string
char* s = "itcast";
string str(s);
提示:
在c++中存在一个从const char到string的隐式类型转换,却不存在从一个string对象到C_string的自动类型转换。对于string类型的字符串,可以通过c_str()函数返回string对象对应的C_string. 通常,程序员在整个程序中应坚持使用string类对象,直到必须将内容转化为char时才将其转换为C_string,如文件读写时. |
---|
3.1.3 将字符全部转为大/小写
int toupper(int c)和int tolower(int c)
原理很简单,就是判断输入进来的如果ascii码是小写字母就变成大写,反之亦然。这里有隐式转换,如果输入和输出要求是char型的话,则输入时把char型变成了int型,输出时则把int型变成了char型。
3.2 vector容器
3.2.1 vector容器基本概念
向量,就是万能类型的动态数组的封装,加上未雨绸缪的自动扩容。
vector的数据安排以及操作方式与数组array非常相似,唯一差别在于空间运用的灵活性。Array是静态空间,一旦配置了就不能改变,想再改变空间大小,一切琐碎得自己来:首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素,再也不必害怕空间不足而一开始就要求一个大块头的array了。
如图Vector是一个单端数组,本身结构只能在尾部插入删除,要在头部操作效率极低,提供了随机访问迭代器(Random Access Iterators),使用迭代器作参数可以随机访问操作。
3.2.2 vector实现原理
vector的动态空间实现技术,其实就是省去了像relloc()中先尝试在原有空间后延展的操作,直接进行”配置新空间-数据移动-释放旧空间”,同时加入未雨绸缪的考虑,降低空间配置时的速度成本,在需要配置新空间时根据以往扩容次数递增地预留更多的空间,这就是容量的概念。一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。
3.2.4 vector常用API操作
3.2.4.1 vector构造函数
vector<T> v; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end());//将v[begin(),end())这一前开后闭区间中的元素拷贝给本身,
//注意v.begin()和v.end()都是迭代器(指针),指向的是位置,解引用才是位置上的元素。
vector(n, elem);//构造函数将n个elem拷贝给本身。
vector(const vector &vec);//拷贝构造函数,新vector会根据vec的size来开辟容量。
//例子使用第二个构造函数 我们可以传入一个数组的两个位置的指针,来拷贝这两个指针之间的值
int arr[] = {2,3,4,1,9};
vector<int> v1(arr, arr + sizeof(arr) / sizeof(int));
3.2.4.2 vector常用赋值操作
assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身
assign(n, elem);//将n个elem拷贝赋值给本身
vector& operator=(const vector &vec);//重载等号操作符
swap(vec);// 将vec与本身的元素互换,即将两个容器内容互换
3.2.4.3 vector大小操作
size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(int num);//重新指定容器的长度为num,若容器变长,则以默认值0填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。注意只改变容器的元素长度,不改变最大容量
resize(int num, elem);//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。
capacity();//容器的容量,实际占用内存,会随着元素增加自动变大,但反之不会自动变小
//故有时要手动swap()来回收多余的内存空间。
reserve(int len);//指定容量,容器预留len个元素长度,预留位置不初始化,元素不可访问。
3.2.4.4 vector数据存取操作
at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。
operator[];//返回索引idx所指的数据,越界时,运行直接报错
front();//返回容器中第一个数据元素
back();//返回容器中最后一个数据元素
3.2.4.5 vector插入和删除操作
insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele.
push_back(ele);//尾部插入元素ele
pop_back();//删除最后一个元素
erase(const_iterator start, const_iterator end);//删除迭代器从start到end之间的元素
erase(const_iterator pos);//删除迭代器指向的元素
clear();//删除容器中所有元素
3.2.5 vector小技巧
用swap()收缩容量
由于容器的容量会随着元素增加满载而自动扩容,但反之不会自动收缩,可以利用与匿名对象swap()来回收v多余的内存空间。
v是一个已经扩容到1000的int型vector容器,然后将其长度缩小:
v.resize(10);//长度为10但容量还是1000,大量空间被浪费,实际占用着内存
vector<int>(v).swap(v);
即创建一个用v初始化的匿名对象,然后再和v做交换。匿名对象会根据v的size来开辟容量,然后狸猫换太子,v变成了容量为size的原匿名对象容器,而变臃肿的匿名对象则会自动销毁。
用reserve()预留容量
由于vector是自动扩容,有时在已知最大容量时也可以用v.reserve(1024)来达到array[1024]的效果来极大减少扩容次数,提高效率。
创建二维数组
利用多个容器组合嵌套可以创建多维数组,多维容器:
vector<vector<int>> matrix(3, vector<int>(4, 0));
即创建了一个3x4,初始值为0的二维数组。
或者先开辟3行vector,再给每行指定长度为4:
vector<vector<int> > dp(3); for(int i=0;i<n;i++){dp[i].resize(4);}
注意,如果行和列数是变量,则更推荐第二种,尤其是想作为全局变量,先开辟等确定了再用resize开辟空间,不然编译器运行到此为了保证容量够用会先开辟一个很大空间。
vector<vector<int> > G;
cin >> Nv >> Ne;
G.resize(Nv, vector<int>(Nv, 0));
而且这样做还更方便,G可以直接作为全局变量。一维同理,先定义个空的,等到需要了再resize大小,省空间。
vector<int> Path;
Path.resize(Nv,-1);
3.3 deque容器
3.3.1 deque容器基本概念
vector容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。两者最大的差异,一在于deque允许使用常数项时间(即固定有限的步骤)对头端进行元素的插入和删除操作;二在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,故deque没有必要提供空间保留(reserve)功能。
3.3.2 deque容器实现原理
deque容器在逻辑上是连续的空间,但物理上不一定,本质是由一段段定量的连续空间构成,一旦头段或尾段满载,就再开辟一段空间从后往前(头插)或从前往后(尾插)的存放数据。
deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间、复制、释放的轮回,代价就是复杂的迭代器架构。
deque采取一块所谓的map(注意不是STL的map容器)作为主控,这里的map是一小块连续的内存空间,其中每一个元素(此处称为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区,缓冲区才是deque的存储空间的主体。Deque数据结构的设计及迭代器的前进后退操作颇为繁琐,代码实现远比vector或list都多得多,只有需要在头部频繁操作时才可能使用。
3.4 stack容器
3.4.1 stack容器基本概念
就是万能类型的栈的模板封装。
stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,形式如图所示。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。将元素推入栈的操作称为:push,将元素推出栈的操作称为pop.
3.4.2 stack没有迭代器
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,同样也不提供迭代器。
3.4.3 stack常用API
3.4.3.1 stack构造函数
stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式:
stack(const stack &stk);//拷贝构造函数
3.4.3.2 stack赋值操作
stack& operator=(const stack &stk);//重载等号操作符
3.4.3.3 stack数据存取操作
push(elem);//向栈顶添加元素
pop();//从栈顶移除第一个元素
top();//返回栈顶元素
3.4.3.4 stack大小操作
empty();//判断堆栈是否为空
size();//返回堆栈的大小
3.5 queue容器
3.5.1 queue容器基本概念
就是万能类型的队列的模板封装。
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端(队尾)新增元素,从另一端(队头)移除元素。
3.5.2 queue没有迭代器
Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,同样也不提供迭代器。
3.5.3 queue常用API
3.5.3.1 queue构造函数
queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式:
queue(const queue &que);//拷贝构造函数
3.5.3.2 queue存取、插入和删除操作
push(elem);//往队尾添加元素
pop();//从队头移除第一个元素,没有返回值,需要配合front()使用
back();//返回最后一个元素
front();//返回第一个元素
3.5.3.3 queue赋值操作
queue& operator=(const queue &que);//重载等号操作符
3.5.3.4 queue大小操作
empty();//判断队列是否为空
size();//返回队列的大小
3.6 list容器
3.6.1 list容器基本概念
list容器是一个双向循环链表,相较于vector的连续线性空间,list每次插入或者删除一个元素,就是配置或者释放一个元素的空间,高效利用空间,不会浪费。对于任何位置的元素插入或元素的移除,list永远是常数时间,方便快捷,但是空间耗费较大,且遍历时间较慢。
3.6.2 list容器的迭代器
List容器的节点不能保证在同一块连续的内存空间上,故没有类似原生指针的随机访问迭代器。List迭代器至少要有能力指向list的节点,并能进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。
由于list是一个双向链表,迭代器必须同时具备前移、后移的能力,所以list容器提供的是Bidirectional Iterators(双向迭代器)。
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。其实就是链表的特性。
3.6.4 list常用API
3.6.4.1 list构造函数
list<T> lstT;//list采用采用模板类实现,对象的默认构造形式:
list(beg,end);//构造函数将[beg, end)区间中的元素拷贝给本身。
list(n,elem);//构造函数将n个elem拷贝给本身。
list(const list &lst);//拷贝构造函数。
3.6.4.2 list数据元素插入和删除操作
push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素
insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
remove(elem);//删除容器中所有与elem值匹配的元素,只支持匹配系统自带的数据类型,
//remove源码就是遍历如果相等==则删除,故要想删除自定义类型要在自定义类型中重载==。
3.6.4.3 list大小操作
size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(num);//重新指定容器的长度为num, 若容器变长,则以默认值填充新位置
resize(num, elem);//重新指定容器的长度为num, 若容器变长,则以elem值填充新位置
//如果resize后容器变短,则末尾超出容器长度的元素被删除。
3.6.4.4 list赋值操作
assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
list& operator=(const list &lst);//重载等号操作符
swap(lst);//将lst与本身的元素互换。
3.6.4.5 list数据的存取
front();//返回第一个元素。
back();//返回最后一个元素。
3.6.4.6 list反转和排序
reverse();//反转链表,比如lst包含1,3,5元素,运行此方法后,lst就包含5,3,1元素。
sort(); //list排序,默认从小到大。
由于list没有随机访问迭代器,故无法使用algorithm库里的sort算法,但一般容器本身会提供对应算法。这里的sort同样有重载版本,在()内可以传入回调函数或仿函数,来声明排序规则。例如按自定义类型的成员年龄大小排序,还做了个二级排序,若年龄相等则再按身高排序。
bool myComparePerson( Person &p1, Person &p2){
if(P1.m_Age == p2.m_Age) return p1.m_Height < p2.m_Height;
return p1.m_Age > p2,m_Age;
}
3.7 set/multiset容器
3.7.1 set/multiset容器基本概念
3.7.1.1 set容器基本概念
从这里开始都是关联式容器,即插入时就自动排序。Set的特性是所有元素都会根据元素的键值自动被排序,默认是从小到大。Set的元素不像map那样可以同时拥有实值和键值,set的元素即是键值又是实值。Set不允许两个元素有相同的键值,代码判断到已有的值则不会继续插入。由于要保证顺序,故不能通过set的迭代器改变set元素的值,因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set的iterator是一种const_iterator。
set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器例外。
3.7.1.2 multiset容器基本概念
multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。set和multiset的底层实现是红黑树,红黑树为平衡二叉树的一种。
3.7.2 set常用API
3.7.2.1 set构造函数
set<T> st;//set默认构造函数:
mulitset<T> mst; //multiset默认构造函数:
set(const set &st);//拷贝构造函数
3.7.2.2 set赋值操作
set& operator=(const set &st);//重载等号操作符
swap(st);//交换两个集合容器
3.7.2.3 set大小操作
size();//返回容器中元素的数目
empty();//判断容器是否为空
3.7.2.4 set插入和删除操作
insert(elem);//在容器中插入元素,返回一个对组pair<iterator,bool>,bool代表是否成功。
clear();//清除所有元素
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(elem);//删除容器中值为elem的元素。
3.7.2.5 set查找操作
find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
count(key);//查找键key的元素个数;
lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器;若不存在,返回set.end();
upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器;若不存在,返回set.end();
equal_range(keyElem);//返回容器中key与keyElem相等的上下限的两个迭代器,即返回上面lower和upper的对组(pair)。
3.7.3 对组(pair)
对组(pair)将一对值组合成一个值,这一对值可以具有不同的数据类型,两个值可以分别用pair的两个公有属性first和second访问。
类模板:template <class T1, class T2> struct pair,具体如下:
//第一种方法创建一个对组
pair<string, int> pair1(string("name"), 20);
cout << pair1.first << endl; //访问pair第一个值
cout << pair1.second << endl;//访问pair第二个值
//第二种,常用,很多类模板的创建方式都是make_xxx(),有严格检查,更加安全
pair<string, int> pair2 = make_pair("name", 30);
//pair=赋值
pair<string, int> pair3 = pair2;
3.7.4 更改set排序规则
关联式容器在插入自定义类型数据时都要用仿函数指定排序规则。
set<T,仿函数> st;
set在模板的数据类型参数处可以再输入一个参数来表示排序规则,默认是从小到大,由于在<>里应当是数据类型,故不能用回调函数,只能用仿函数,因为仿函数本质是一个重写了小括号()的类,此时容器类型已变成set<T,仿函数>,调用迭代器时注意,两个示例如下。
示例1:
class myCompareInt{
public::
bool operator()(int v1, int v2){
return v1 > v2;//改成从大到小
}
};
void test1(){
//set容器在插入前要指定排序规则
set<int,myCompareInt> s;
s.insert(10);
s.insert(50);
for(set<int,myCompareInt>::iterator it = s.begin(); it != s.end(); it++){
cout << *it << endl;
}
}
示例2:
class myComparePerson{
public:
bool operator()(const Person &p1, const Person &p2){
return p1.m_Age < p2.m_Age;
}
};
void test2(){
set<Person,myComparePerson s;
Person p1("aaa", 10);
Person p1("eee", 50);
s.insert(p1);
s.insert(p5);
for(set<int,myComparePerson>::iterator it = s.begin(); it != s.end(); it++){
cout << "姓名: " << (*it).m_Name << "年龄: " << it->m_Age << endl;//(*p).和->都可
}
3.8 map/multimap容器
3.8.1 map/multimap基本概念
Map的特性是,所有元素都会根据元素的键值自动排序。Map所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值。同样地,map的键值也不能用迭代器改变,但元素的实值是可以改的。
Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。
Multimap和map的操作类似,唯一区别multimap键值可重复。
Map和multimap都是以红黑树为底层实现机制。
3.8.2 map/multimap常用API
3.8.2.1 map构造函数
map<T1, T2> mapTT;//map默认构造函数:
map(const map &mp);//拷贝构造函数
3.8.2.2 map赋值操作
map& operator=(const map &mp);//重载等号操作符
swap(mp);//交换两个集合容器
3.8.2.3 map大小操作
size();//返回容器中元素的数目
empty();//判断容器是否为空
3.8.2.4 map插入数据元素操作
map.insert(...); //往容器插入元素,返回pair<iterator,bool>,iterator是指向插入元素的迭代器,bool表示是否成功插入
// 第一种 通过pair的方式插入对象,插入的pair中第一个元素作为key
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过pair的方式插入对象,推荐
mapStu.insert(make_pair(-1, "校长")); // make_pair(key, val);
// 第三种 通过value_type的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值,重载了[],map并不提供随机访问迭代器
//由于map不允许键值重复,上面的方法如果插入时key值已存在会直接忽略,而这种方法可以修改已有键key的val
mapStu[3] = "小刘"; // mapStu[key]=val;
mapStu[5] = "小王";
3.8.2.5 map删除操作
clear();//删除所有元素
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg,end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(keyElem);//删除容器中key为keyElem的对组。
3.8.2.6 map查找操作
find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回map.end();
count(keyElem);//返回容器中key为keyElem的对组个数。对map来说,要么是0,要么是1。对multimap来说,值可能大于1。
lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器。
upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器。
equal_range(keyElem);//返回容器中key与keyElem相等的上下限的两个迭代器。
3.9 STL容器使用时机
vector | deque | list | set | multiset | map | multimap | |
---|---|---|---|---|---|---|---|
典型内存结构 | 单端数组 | 双端数组 | 双向链表 | 二叉树 | 二叉树 | 二叉树 | 二叉树 |
可随机存取 | 是 | 是 | 否 | 否 | 否 | 对key而言:不是 | 否 |
元素搜寻速度 | 慢 | 慢 | 非常慢 | 快 | 快 | 对key而言:快 | 对key而言:快 |
元素安插移除 | 尾端 | 头尾两端 | 任何位置 | - | - | - | - |
vector的使用场景:比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录。
deque的使用场景:比如排队购票系统,对排队者的存储可以采用deque,支持头端的快速移除,尾端的快速添加。如果采用vector,则头端移除时,会移动大量的数据,麻烦还速度慢。
vector与deque的比较:
1、vector.at()比deque.at()效率高,比如vector.at(0)是固定的,deque的开始位置却是不固定的。
2、如果有大量释放操作的话,vector花的时间更少,这跟二者的内部实现有关。
3、deque支持头部的快速插入与快速移除,这是deque的(唯一)优点。
list的使用场景:比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。
set的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。
map的使用场景:比如按ID号存储十万个用户,想要快速通过ID查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector容器,最坏的情况下可能要遍历完整个容器才能找到该用户。
vector,list使用得最多,关联式容器中map使用得更多,一般都是简单高效,泛用性好。
4. 算法
4.1 函数对象(仿函数)
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。重载()中需要几个参数,就称其为几元仿函数。
STL提供的算法往往都有两个版本,其中一个版本表现出最常用的某种运算,另一版本则允许用户通过传入回调函数或仿函数来指定所要采取的策略,由于有些参数是template类型,如set<T,仿函数> st;回调函数(指针)不能当作template参数,故函数对象更通用。
总结:
1、函数对象(仿函数)是一个类,不是一个函数,使用时可以简单地用匿名对象,也可创建一个对象,能进行类的其他操作。
2、函数对象(仿函数)重载了”() ”操作符使得它可以像函数一样调用,故传入仿函数时要把()带上,回调函数只需要传函数名(指针)。
3、函数对象通常不定义构造函数和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用的运行时问题。
4、函数对象超出普通函数的概念,函数对象可以有自己的状态。
5、函数对象可内联编译,不需要入栈出栈,性能好。
6、模版函数对象使函数对象具有通用性,这也是它的优势之一。
4.2 谓词
谓词是指普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。
class MyCompare{
public:
bool operator()(int num1, int num2){//二元谓词,()比较需要两个参数
return num1 > num2;
}
};
//调用:
sort(v.begin(), v.end(),MyCompare());
4.3 内建函数对象
STL内建了一些函数对象,即提供了一些仿函数的模板类,可以直接创建函数对象去使用,很少用临时的匿名对象,因为模板类匿名反而更麻烦。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。这些仿函数所产生的对象,用法和一般函数完全相同。使用内建函数对象,需要引入头文件 #include<functional>。
- 6个算数类函数对象,除了negate是一元运算,其他都是二元运算。
template<class T> T plus<T>//加法仿函数
template<class T> T minus<T>//减法仿函数
template<class T> T multiplies<T>//乘法仿函数
template<class T> T divides<T>//除法仿函数
template<class T> T modulus<T>//取模仿函数
template<class T> T negate<T>//取反仿函数
加法示例:
plus<int>p;
cout << p(10, 10) << endl;
- 6个关系运算类函数对象,每一种都是二元运算。
template<class T> bool equal_to<T>//等于
template<class T> bool not_equal_to<T>//不等于
template<class T> bool greater<T>//大于
template<class T> bool greater_equal<T>//大于等于
template<class T> bool less<T>//小于
template<class T> bool less_equal<T>//小于等于
- 逻辑运算类运算函数,not为一元运算,其余为二元运算,很少用。
template<class T> bool logical_and<T>//逻辑与
template<class T> bool logical_or<T>//逻辑或
template<class T> bool logical_not<T>//逻辑非
4.4 适配器
4.4.1函数对象适配器
如果我想让仿函数传入额外的参数,但不符合该算法的形式,如for_each底层是只把参数1传入仿函数中,我想再传一个偏移变量num,让myprint打印时加上,显然只改写myprint()是没用的,因为没有这个偏移量的接口供用户去输入,for_each也无法处理。
for_each(v.begin(), v.end(),MyPrint());//显然不行,没有接口,也无法运行
所以要用适配器,把参数绑在一起,具体有以下三步:
1、先用函数适配器绑参数。bind2nd(参数1,参数2)本身也是一个仿函数,参数1是原仿函数,额外参数2会被绑进去,提供接口和对外算法的运行。一元仿函数,如取反函数要用not1(myfunction()),对myfunction的规则取反,如果myfunction是二元仿函数想取反要改成not2()。
for_each(v.begin(), v.end(), bind2nd( MyPrint(), num ));
find_if(v.begin(), v.end(), not1( GreaterThanFive()));
2、继承仿函数基类。在写myprint时要继承binary_function<>。binary_function<参数1类型,参数2类型,返回值类型>是二元仿函数时要继承的类,一元仿函数要继承unary_function<参数1类型,返回值类型>。
class MyPrint :public binary_function< int, int, void >{
public:
void operator()( int val, int start ) const{
cout<<"val = "<<val<<"start = "<<start<<"sum = "<<<val+start<<endl;
}
};
3、加const。由于binary_function类里重载()是用了常函数(即在函数的()后加了const),故myprint里重写该重载()也还要加上const改成只读。
注意,STL内建函数对象已经继承了基类,重载()也加过了const,故只需使用适配器,且由于适配器基本都需要使用和继承模板类来声明函数对象,故也需要引入头文件#include<functiona\l>。
示例详解:
not1(GreaterThanFive())是把自己写的返回大于5的仿函数取反,变成返回小于5,可以把GreaterThanFive()改成STL提供的greater<int>(),再用适配器绑上5:
find_if(v.begin(), v.end(), not1( bind2nd( greater<int>() , 5 )));//本质还是一元取反,里面套了一个二元仿函数
sort(v.begin(), v.end(), not2 (less<int>()));//二元取反,因为less调用了两个参数
4.4.2函数适配器
上文是函数对象(仿函数)的适配器,如果使用回调函数想加额外参数时,由于回调函数就是普通的函数(指针),没有继承和const可写,故只需要在第一步用适配器时,先把myFunction适配成函数对象,用ptr_fun(myFunction)即可,当然myFunction要改写成需要的。
for_each(v.begin(), v.end(), bind2nd(ptr_fun(myPrint3), 1000));
4.4.3成员函数适配器
这里的适配器是先将成员函数适配成回调函数,不涉及前文的增加额外参数,可以和前文组合。
首先把成员函数的作用域加上,然后加上&找到其地址,最后再用mem_fun_ref()适配成回调函数。
for_each(v.begin(), v.end(), mem_fun_ref(&Person::showPerson));
4.4.4函数适配器个人补充
bind2nd(),bind1st(),not1(),not2(),ptr_fun(),mem_fun_ref()等都是函数适配器。其中bind2nd原版通用是bind(func(),arg1,arg2,…),但一般不超过二元,即bind2nd(func(),arg1)即可。在无需使用适配器,也就是func()本身只有一个参数时,传入的那个参数通常是其算法固定要传的那个,比如for_each(beg,end, func())就是传入当前遍历到的那个迭代器,底层为初值=beg的_First。那么当把func()改成两个参数,并加了bind2nd(func(),arg1)后,新func()的第一个参数还是其算法要传的那个,第二个参数则是arg1,而bind1st()只是把顺序调了以下,一般就用bind2nd(),因为顺序一致。
4.5 常用遍历算法
for_each() 遍历
for_each(iterator beg, iterator end, _Pred);
从beg开始迭代器遍历到end结束迭代器,_Pred为函数回调或函数对象,指定遍历时对每个元素要做的操作和条件,比如(按条件)打印、计算。
void myPrint (int val){cout << val << " ";}//定义一个有参函数
for_each(V.begin(), V.end(), myPrint);//写入函数名(指针),会自动遍历V并传入该函数
for_each()会返回传入函数对象的最终状态,故即使传入的是匿名函数对象,for_each也会一直保留到遍历至结束,并返回此时的函数对象。
for_each本身是拷贝操作,不会改变原数组,除非是引用传递。
transform() 搬运
transform(iterator beg1, iterator end1, iterator beg2, _Pred);
将源容器指定区间元素搬运到目标容器中,beg1为源容器开始迭代器,beg2为源容器结束迭代器,beg2为目标容器开启迭代器,_Pred为回调函数或函数对象,指定搬运时对每个元素要做的操作和条件,比如(按条件)打印,计算,什么都不做也要返回自己,不然做不了搬运。
transform()本身不会给目标容器扩容,故要v2.resize(v1.size())保证长度足够装下。
这里的参数都是灵活的,比如是要把v1的部分内容拼接在v2之后,那beg1和beg2可以不是v1.begin()和v1.end(),beg2则是v2.end(),且有v2.resize(v2.size()+v1.size());
4.6 常用查找算法
find() 查找
find(iterator beg, iterator end, value);
按值查找,返回第一个符合的元素位置(迭代器),找不到则返回end迭代器。
如果value是自定义数据类型,还需在该类中重载==
bool operator==(const Person &p){
return this->m_Name == p.m_Name && this->m_Age == p.m_Age;
}
该例中&无所谓,因为本身也没有改动原变量。
find_if() 按条件查找(详解)
find_if(iterator beg, iterator end, _callback);
按条件查找,回第一个符合的元素位置(迭代器),找不到则返回end迭代器。
_callback必须是谓词,即返回bool类型的函数,因为要以此判断是否满足条件。
find()的按值查找其实就是看==的返回值是1还是0,find_if()由于查找条件成了函数,更加灵活。尤其对于自定义类型,find(beg, end,value)的value不能是指针,因为find()判断依据是重载的==,只能比较到指针本身的值是否相等,而find_if()则可以通过仿函数重载()做到,因为find_if()判断依据是重载()的仿函数的bool返回值。
示例如下:因为是比较,重载()要有两个参数,由4.4.4适配器补充可知,其中p1是当前遍历的迭代器,p2是要比较的迭代器,故要用适配器把两个参数绑起来
class MyComparePersin :public binary_function< Person *, Person *, bool >{
public:
bool operator()( Person *p1, Person *p2 ) const{
return p1->m_Name == p2->m_Name && p1->m_Age == p2->m_Age;
}
};
int test(){
vector<Person*> v;
Person *p = new Person("bbb", 20);
vector<Person *>::iterator pos = find_if(v.begin(), v.end(), bind2nd( MyComparePerson(), p) );
}
adjacent_find() 查找相邻的重复元素
adjacent_find(iterator beg, iterator end, _callback);//返回第一个相邻的重复元素,如1,2,3,3,4会返回第一个3的迭代器。
_callback是自定义相等规则,不写默认就是值相等即算作重复元素。
binary_search() 二分查找
bool binary_search(iterator beg, iterator end, value);//找到返回1,不会返回迭代器,必须是有序序列才能用二分查找。
count() / count_if() (按条件)计数
int count(iterator beg, iterator end, value);//返回值为value元素个数
int count_if(iterator beg, iterator end, _callback);//类似find_if()的区别,返回满足条件的元素个数
4.7 常用排序算法
merge() (有序)合并
merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest)
//将容器1和容器2里的指定区间元素合并到目标容器,前提是容器1和容器2内元素是有序且顺序一致。如1,2,3和2,3,4合并成1,2,2,3,3,4。
注意要保证目标容器长度足够,可以先resize()一下。
sort() 排序算法
sort(iterator beg, iterator end, _callback);//默认为从小到大升序排列。
random_shuffle() 随机打乱
random_shuffle(iterator beg, iterator end);//将指定区间内元素随机打乱,要搭配随机树种子。
reverse() 逆转
reverse(iterator beg, iterator end);//将指定区间内元素逆转。
4.8 常用拷贝和替换算法
copy() 拷贝
copy(iterator beg, iterator end, iterator dest);//将beg到end区间元素拷贝到目标迭代器
copy(v2.begin(), v2.end(), ostream_iterator<int>(cout, " "));//小技巧:打印,比for_each打印简单点
ostream_iterator属于I/O流STL适配器(流迭代器),使用时要引入头文件<iterator>,用于获取一个元素,同时保存在缓冲器中,供后面的cout输出。这里就是把流迭代器作为copy的目标迭代器,而这个流迭代器还会把值给到缓冲区中。
replace() / replace_if() (按条件)替换
replace(iterator beg, iterator end, oldvalue, newvalue);//将指定区间内的指定元素全部替换为新元素
replace_if(iterator beg, iterator end, _callback, newvalue);//将指定区间内满足条件的元素全部替换为新元素
swap() 交换
swap(container c1, container c2);//交换两个容器的元素。
4.9 常用算数生成算法
accumulate() 累加
accumulate(iterator beg, iterator end, value);//求指定区间的元素之和,value为起始(偏移)值,0即为累计和。
该算法在<numeric>小型算法库中。
fill() 填充
fill(iterator beg, iterator end, value);//给范围内所有位置赋值,包括未赋值的空元素。
4.10 常用集合算法
set_intersection() 求交集(详解)
set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
vTarget.resize(min(v1.size(), v2.size()));
取出容器1和容器2的交集元素并放到目标容器的开始迭代器,集合算法要求容器1和容器2必须是有序序列,返回目标迭代器最后一个元素的下一个迭代器。
由于要放到一个目标容器中,需要保证目标容器size足够大,考虑交集的最大情况,即小集合全部被大集合包括vTarget.resize(min(v1.size(), v2.size()));也可用三目运算符,但麻烦。
如果需要遍历或使用目标容器的元素,要使用该算法返回的迭代器,而不能用目标容器的end():
vector<int>::iterator itEnd = set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), vTarget.begin());
for_each(vTarget.begin(), itEnd, [](int val){cout << val << " "; });
因为end()是容器大小size的最后一个位置,而目标容器的size设计是考虑到最大情况,大部分情况会有冗余,用默认值0填充,所以如果用vTarget.end()则会把最后的一串0也遍历到。
为啥类似目标容器设计操作都是用size而不是容量?因为只有size才是可访问的位置,会初始化,容量只是预留空间,无法访问也不初始化。
set_union() 求并集
set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
//同上,区别主要在开辟目标容器size,为并集的最大情况,即两者完全不相交而相加
vTarget.resize(v1.size() + v2.size());
set_difference() 求差集
set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
//同上,差集的最大情况为两者中的大集合
vTarget.resize(max(v1.size(), v2.size()));