目录
上节学习了C++中的强制类型转换、异常操作和文件操作,本节开始学习STL!
这部分是C++里面很重要的内容!
之前我们学习用C语言学习数据结构的时候,我们要自己写链表、队列、栈的增删等操作。
在C++里面这些操作都封装好了,我们直接调用就行,我们不需要知道它的底层是什么,直接用就可以了,这个东西就是STL。
说明:本篇博文比较长,理论部分可以不用看,但是代码部分必看!
STL概述
STL(Standard Template Library,标准模板库)是惠普实验室开发的一系列软件的统称。现然主要出现在C++中,但在被引入C++之前该技术就已经存在了很长的一段时间。
STL的从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。在C++标准中,STL被组织为下面的13个头文 件:<algorithm>算法、<deque>双端数组、<functional>仿函数(类似于C语言中的回调函数)、<iterator>迭代器(这个头文件一般不写也不会报错)、<vector>向量(其实是数组)、<list>列表、<map>映射(其实是二叉树)、<memory>内存(用的不多)、<numeric>(跟数学相关的一些算法,很少用)、<queue>队列、<set>集合(二叉树)、<stack>栈 和<utility>(这个很少用)。
algorithm(算法):比如遍历链表就属于一种算法。
container(容器):比如我们学习的栈、队列、二叉树、链表等这些数据结构里面的概念都是用来存储数据的,就可以视为一种“容器”。
iterator(迭代器):通俗来讲本质上它就是指针。
STL六大组件
1、容器(Container)
2、算法(Algorithm)
3、迭代器(Iterator)
4、仿函数(Function object):函数对象(这个在前面学习函数调用运算符()重载的时候讲过)
5、适配器(Adaptor)
6、空间配制器(allocator)
使用STL优势
1、STL是C++的一部分,因此不用额外安装什么,它被内建在C++的编译器之内。
2、STL的一个重要特点是数据结构和算法的分离。
3、程序员可以不用思考STL具体的实现过程,只要能够熟练使用STL就OK了,因此提高了开发的效率。
4、STL具有高可重用性,高性能,高移植性,跨平台的优点。
了解到STL的这些好处,我们知道STL无疑是最值得C++程序员骄傲的一部分。只有能够熟练使用STL的程序员,才是好的C++程序员。招聘工作中,经常遇到C++程序员对STL不是非常了解。大多是有一个大致的映像,而对于在什么情况下应该使用哪个容器和算法都感到比较茫然。STL是C++程序员的一项不可或缺的基本技能,掌握它对提升C++编程大有裨益。
容器
容器:用来存放数据的空间。
经典的数据结构数量有限,但是我们常常重复着一些为了实现向量、链表等结构而编写的代码,这些代码都十分相似,只是为了适应不同数据的变化而在细节上有所出入。STL容器就为我们提供了这样的方便,它允许我们重复利用已有的实现特定类型下的数据结构,通过设置一些模板,STL容器对最常用的数据结构提供了支持,这些模板的参数允许我们指定容器中元素的数据类型,可以将我们许多重复而乏味的工作简化。
容器部分主要由头文 件<vector>,<list>,<deque>,<set>,<map>,<stack> 和<queue>组成。
容器的分类
序列式容器(Sequence containers)
每个元素都有固定位置--取决于插入时机和地点,和元素值无关。
vector、deque、list (比如说数组,可以通过下标指定放到哪)
关联式容器(Associated containers)
元素位置取决于特定的排序准则,和插入顺序无关
set、multiset、map、multimap (这种容器不好指定放到哪,可以实现排序,我们只负责放,放到哪由这个容器自己决定)
容器的种类
迭代器
迭代器从作用上来说是最基本的部分,但是理解起来比前两者都要费力一些。软件设计有一个基本原则,所有的问题都可以通过引进一个间接层来简化, 这种简化在STL中就是用迭代器来完成的。概括来说,迭代器在STL中用来将算法和容器联系起来,起着一种黏和剂的作用。几乎STL提供的所有算法都是通过迭代器存取元素序列进行工作的,每一个容器都定义了其本身所专有的迭代器,用以存取容器中的元素。
迭代器部分主要由头文件<utility>、<iterator>和<memory>组 成。<utility>是一个很小的头文件,它包括了贯穿使用在STL中的几个模板的声明,<iterator>中提供了迭代器 使用的许多方法,而对于<memory>的描述则十分的困难,它以不同寻常的方式为容器中的元素分配存储空间,同时也为某些算法执行期间产生 的临时对象提供机制,<memory>中的主要部分是模板类allocator,它负责产生所有容器中的默认分配器。
算法
STL提供了大约100个实现算法的模版函数,比如算法for_each将为指定序列中的每一个元素调用指定的函数,stable_sort以你所指定的规则对序列进行稳定性排序等等。这样一来,只要熟悉了STL之后,许多代码可以被大大的化简,只需要通过调用一两个算法模板,就可以完成所需要的功能并大大地提升效率。
算法部分主要由头文件<algorithm>,<numeric>和<functional>组成。<algorithm>是所有STL头文件中最大的一个(尽管它很好理解),它是由一大堆模版函数组成的,可以认为每个函数在很大程度上都是独立的,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。<numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。<functional>中则定义了一些模板类, 用以声明函数对象。
接下来开始直接上代码讲解
组件1:容器
容器1:string(处理字符串的类)
string是STL中处理字符串的类。而在使用string之前,字符串通常是用char *表示的。string与char *都可以用来表示字符串,那么二者有什么区别呢?
string和char*的比较
string是一个类, char *是一个指向字符的指针。
string封装了char *,管理这个字符串,是一个char *型的容器。
string不用考虑内存释放和越界。
string管理char *所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。
string提供了一系列的字符串操作函数。
string的构造
默认构造函数:
string();
拷贝构造函数(用一个对象创造另一个对象):
string(const string &str);
带参数的构造函数
string(const char *s);
string(int n, char c);
string操作
1、存取字符(string这个类型重载了[ ])
[ ]或者at(),区别就是at()成员函数在越界时会抛出异常;用at()更安全一些
2、string转换成char *(因为strcpy只能拷贝char*型,所以要将string转换成char*)
const char *c_str() const;
3、拷贝
int copy(char *s, int n, int pos=0) const;
4、string长度
int length() const; 或者 bool empty() const;
5、string的赋值(string这个类里面重载了=)
=或者assign()函数,重载形式有:
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); //从下标为start的字符开始,向后n个字节赋值给引用的对象里面
6、字符串连接
+= 或者 append()成员函数,重载形式有:
string &append(const char *s);//把s指向的字符串接在当前对象的后面
string &append(const char *s,int n);//把s指向的字符串的前n个字节接在当前对象的后面
string &append(const string &s);//将一个对象(字符串)接在当前对象的后面
string &append(const string &s,int pos, int n);//将s引用的对象从pos开始向后的n个字节接在当前对象的后面
string &append(int n, char c);//将n个字符c接在当前对象的后面
7、字符串比较
int compare(const string &s) const; //将s引用的字符串和当前对象比较
int compare(const char *s) const;//将s指向的字符串和当前对象比较
8、string的子串
string substr(int pos=0, int n=npos) const;//获得当前对象的子串(从当前对象的pos位置开始向后n个字节)
9、string查找
int find(char c,int pos=0) const;//从pos这个位置开始查找字符c,返回c所在下标
int find(const char *s, int pos=0) const;//从下标为pos的地方开始往后找字符串s
int find(const string &s, int pos=0) const; //从下标为pos的地方开始往后找对象s(字符串)
find函数如果查找不到,就返回-1
rfind是从后往前找(以上find是从头往后找)
int rfind(char c, int pos=npos) const;
int rfind(const char *s, int pos=npos) const;//从pos开始往后找字符串s
int rfind(const string &s, int pos=npos) const;//从pos开始往后找对象(字符串)s
9、替换
string &replace(int pos, int n, const char *s);//将当前对象从pos这个位置开始往后的n个字节替换成字符串s
string &replace(int pos, int n, const string &s);
void swap(string &s2);
10、string的插入
string &insert(int pos, const char *s);//从pos这个位置开始插入字符串s
string &insert(int pos, const string &s);
string &insert(int pos, int n, char c);
11、string的删除
string &erase(int pos=0, int n=npos); //将下标为0的位置往后n个字节删掉
接下来看第二个容器
容器2:vector(向量,类似动态数组)
(翻译过来是向量,本质上数组或者顺序表,但它类似动态数组)
vector是将元素置于一个动态数组中加以管理的容器。
vector可以随机存取元素(支持索引值直接存取, 用[]操作符或at()方法)。
vector尾部添加或移除元素非常快速。但是在中部或头部插入元素或移除元素比较费时。
vector默认的构造函数
vector采用模板类实现(要包含头文件<vector>),
vector对象的默认构造形式:vector<T> vecT;
vector<int> vecInt; //一个存放int的vector容器。
vector<float> vecFloat; //一个存放float的vector容器。
vector<string> vecString; //一个存放string的vector容器。
... //尖括号内还可以设置指针类型或自定义类型。
vector带参的构造函数
vector(beg,end); //构造函数将[beg, end)区间中的元素拷贝给本身。注意该区间是左闭右开的区间。
vector(n,elem); //构造函数将n个elem拷贝给本身。
vector(const vector &vec); //拷贝构造函数
vector的赋值
vector.assign(beg,end); //将[beg, end)区间中的数据拷贝赋值给本身。注意该区间是左闭右开的区间。
vector.assign(n,elem); //将n个elem拷贝赋值给本身。
vector& operator=(const vector &vec); //重载等号操作符
vector.swap(vec); // 将vec与本身的元素互换。
vector的大小
vector.size(); //返回容器中元素的个数
vector.empty(); //判断容器是否为空
vector.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
vector.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
vector数据的存取
vec.at(idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。
vec[idx]; //返回索引idx所指的数据,越界时,运行直接报错
除了以上遍历方法,也可以使用迭代器遍历vector。
组件2:迭代器(类似指针)
迭代器是一个“可遍历STL容器内全部或部分元素”的对象。
迭代器指出容器中的一个特定位置。
迭代器就如同一个指针(可以通过指针遍历数组)。
迭代器提供对一个容器中的对象的访问方法,并且可以定义了容器中对象的范围。
vector和迭代器
通过正向迭代器来遍历vector
用反向迭代器遍历,可以倒序遍历
如果访问的过程中不想修改数据或者防止误操作还可以用只读迭代器
如果想要通过这个只读迭代器修改数据是不允许的
我们一般都是正向迭代器进行遍历。
迭代器的种类
输入迭代器:也有叫法称之为“只读迭代器”,它从容器中读取元素,只能一次读入一个元素向前移动,只支持一遍算法,同一个输入迭代器不能两遍遍历一个序列。
输出迭代器:也有叫法称之为“只写迭代器”,它往容器中写入元素,只能一次写入一个元素向前移动,只支持一遍算法,同一个输出迭代器不能两遍遍历一个序列。
正向迭代器:组合输入迭代器和输出迭代器的功能,还可以多次解析一个迭代器指定的位置,可以对一个值进行多次读/写。
双向迭代器:组合正向迭代器的功能,还可以通过--操作符向后移动位置。
随机访问迭代器:组合双向迭代器的功能,还可以向前向后跳过任意个位置,可以直接访问容器中任何位置的元素。
双向迭代器和随机访问迭代器
双向迭代器支持的操作:
it++, ++it, it--, --it,*it, itA = itB,itA == itB,itA != itB
其中list、set、multiset、map、multimap支持双向迭代器(不连续的存储空间)。
随机访问迭代器支持的操作(在双向迭代器的操作基础上添加):
it+=i,it-=i,it+i(或it=it+i),it[i],itA<itB,itA<=itB,itA>itB,itA>=itB
其中vector,deque支持随机访问迭代器(连续的存储空间)。
Vector的插入操作除了刚刚我们用的push_back,还有一个可以插入的函数insert()
vector插入
vector.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
vector.insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
vector.insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值
pos指迭代器
(用的是正向迭代器)
vector删除
vector.clear(); //移除容器的所有数据
vec.erase(beg,end); //删除[beg,end)区间的数据,返回下一个数据的位置。
vec.erase(pos); //删除pos位置的数据,返回下一个数据的位置。
容器3:deque(双端数组)
(翻译过来是双端数组)
deque是“double-ended queue”的缩写,和vector一样都是STL的容器,deque是双端数组(头不固定,插入时会修改第一个元素的地址),而vector是单端的(头是固定的)。
deque在接口上和vector非常相似,在许多操作的地方可以直接替换。
deque可以随机存取元素(支持索引值直接存取, 用[]操作符或at()方法。
deque头部和尾部添加或移除元素都非常快速。但是在中部安插元素或移除元素比较费时。
#include <deque>
deque的默认构造
deque采用模板类实现,deque对象的默认构造形式:deque<T> deqT;
deque <int> deqInt; //一个存放int的deque容器。
deque <float> deq Float; //一个存放float的deque容器。
deque <string> deq String; //一个存放string的deque容器。
deque的有参构造函数
deque(beg, end); //构造函数将[beg, end)区间中的元素拷贝给本身。注意该区间是左闭右开的区间。
deque(n, elem); //构造函数将n个elem拷贝给本身。
deque(const deque &deq); //拷贝构造函数。
deque的添加和删除
deque.push_back(elem); //在容器尾部添加一个数据
deque.push_front(elem); //在容器头部插入一个数据
deque.pop_back(); //删除容器最后一个数据
deque.pop_front(); //删除容器第一个数据
deque数据的存取
deque.at(idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range。
deque[idx]; //返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
deque.front(); //返回第一个数据。
deque.back(); //返回最后一个数据
deque和迭代器
deque.begin(); //返回容器中第一个元素的迭代器。
deque.end(); //返回容器中最后一个元素之后的迭代器。
deque.rbegin(); //返回容器中倒数第一个元素的迭代器。
deque.rend(); //返回容器中倒数最后一个元素之后的迭代器。
deque的赋值
deque.assign(beg,end); //将[beg, end)区间中的数据拷贝赋值给本身。注意该区间是左闭右开的区间。
deque.assign(n,elem); //将n个elem拷贝赋值给本身。
deque& operator=(const deque &deq); //重载等号操作符
deque.swap(deq); // 将vec与本身的元素互换
deque的大小
deque.size(); //返回容器中元素的个数
deque.empty(); //判断容器是否为空
deque.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque的插入
deque.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
deque.insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
deque.insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值。
deque的删除
deque.clear(); //移除容器的所有数据
deque.erase(beg,end); //删除[beg,end)区间的数据,返回下一个数据的位置。
deque.erase(pos); //删除pos位置的数据,返回下一个数据的位置(即返回下一个元素的迭代器)。
deque实战
假设 deqInt 包含1,3,2,3,3,3,4,3,5,3,删除容器中等于3的元素。
注意:删除元素的返回值。
为什么这样写出现了段错误?
一开始it指向开头位置
*it没有等于3,所以直接it++
此时*it等于3,则把it指向的3删掉,
注意:在erase()里面删掉后,双端数组两端哪端移动的数少就让哪端移动。比如此时删掉3后,如果让头部往后移动则只需要移动一次,而如果让尾部向前移动则需要移动很多次,所以erase()就让头部移动。这也是deque的一个特性。
然后1、2多不等于3则继续it++
后面三个3都等于3,则依次循环删除
此时让头部移动还是比让尾部移动的次数更少,所以还是让头部移动
然后2、4都等于3所以都it++
然后后面的3等于3,需要删除
注意!此时让尾部向前移动比让头部向后移动的次数少,所以erase()就让尾部向前移动
然后5不等于3,继续it++,指向3,则把3给删除
此时让end往前移动比让头部往后移动的次数要少,所以erase()就让end往前移动
然后it++,指向了一个未知的空间
而此时it也不等于end(),所以for循环还在继续执行,it继续++,形成了死循环,所以程序就死了。
所以我们的问题出现在了这里的it++,我们要想办法让这个it++不要执行
所以应该该改成这样:
注:基本上我们所接触的容器基本上都支持迭代器,也就是都有begin(),end(),rbegin(),rend()这些函数,这些函数返回的就是对应的位置。
但是栈和队列是没有迭代器的,因为栈和队列不支持遍历,栈只能看到栈顶(类似水桶),队列只能看到队头和队尾(类似水管)。
容器4:stack(栈)
stack是堆栈容器,是一种“先进后出”的容器。
stack是简单地装饰deque容器而成为另外的一种容器。
#include <stack>
stack的默认构造
stack采用模板类实现, stack对象的默认构造形式: stack <T> stkT;
stack <int> stkInt; //一个存放int的stack容器。
stack <float> stkFloat; //一个存放float的stack容器。
stack <string> stkString; //一个存放string的stack容器。
stack进栈和出栈
stack.push(elem); //往栈头添加元素
stack.pop(); //从栈头移除第一个元素
stack拷贝构造和赋值
stack(const stack &stk); //拷贝构造函数
stack& operator=(const stack &stk); //重载等号操作符
stack.top(); //返回栈顶元素
注意:在STL里面出栈的时候并没有返回出栈的元素。如果想要这个元素是多少,只能用stack.top();这个函数获取。
stack大小
stack.empty(); //判断堆栈是否为空
stack.size(); //返回堆栈的大小
如果非要遍历栈的话,就只能让它们出栈。
容器5:queue(队列)
queue是队列容器,是一种“先进先出”的容器。
queue是简单地装饰deque容器而成为另外的一种容器。
#include <queue>
queue默认构造
queue采用模板类实现,queue对象的默认构造形式:queue<T> queT; 如:
queue<int> queInt; //一个存放int的queue容器。
queue<float> queFloat; //一个存放float的queue容器。
queue<string> queString; //一个存放string的queue容器。
queue操作
queue.push(elem); //往队尾添加元素
queue.pop(); //从队头移除第一个元素
queue(const queue &que); //拷贝构造函数
queue& operator=(const queue &que); //重载等号操作符
queue.back(); //返回最后一个元素
queue.front(); //返回第一个元素
queue.empty(); //判断队列是否为空
queue.size(); //返回队列的大小
容器6:list(双向链表)
list是一个双向链表容器,可高效地进行插入删除元素。
list不可以随机存取元素,所以不支持at.(pos)函数与[]操作符(因为链表的元素地址不是连续的)。例如:It++(ok支持这种写法,一次只能走一个节点);it+5(err不支持这种写法,一次不能跨越多个节点)
#include <list>
list默认构造
list采用采用模板类实现,对象的默认构造形式:list<T> lstT; 如:
list<int> lstInt; //定义一个存放int的list容器。
list<float> lstFloat; //定义一个存放float的list容器。
list<string> lstString; //定义一个存放string的list容器。
list头插和尾插
list.push_back(elem); //在容器尾部加入一个元素
list.pop_back(); //删除容器中最后一个元素
list.push_front(elem); //在容器开头插入一个元素
list.pop_front(); //从容器开头移除第一个元素
list操作
list.front(); //返回第一个元素。
list.back(); //返回最后一个元素。
list.begin(); //返回容器中第一个元素的迭代器。
list.end(); //返回容器中最后一个元素之后的迭代器。
list.rbegin(); //返回容器中倒数第一个元素的迭代器。
list.rend(); //返回容器中倒数最后一个元素的后面的迭代器。
list(beg,end); //构造函数将[beg, end)区间中的元素拷贝给本身(删除两个迭代器之间的元素)。注意该区间是左闭右开的区间。
list(n,elem); //构造函数将n个elem拷贝给本身。
list(const list &lst); //拷贝构造函数。
list.assign(beg,end); //将[beg, end)区间中的数据拷贝赋值给本身。注意该区间是左闭右开的区间。
list.assign(n,elem); //将n个elem拷贝赋值给本身。
list& operator=(const list &lst); //重载等号操作符
list.swap(lst); // 将lst与本身的元素互换。
list.size(); //返回容器中元素的个数
list.empty(); //判断容器是否为空
list.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
list.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
list.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
list.insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
list.insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值。
list.clear(); //移除容器的所有数据
list.erase(beg,end); //删除[beg,end)区间的数据,返回下一个数据的位置。
list.erase(pos); //删除pos位置的数据,返回下一个数据的位置。
lst.remove(elem); //删除容器中所有与elem值匹配的元素。
lst.reverse(); //反转链表,比如lst包含1,3,5元素,运行此方法后,lst就包含5,3,1元素。
如果这样写的话程序就出错了
由于这里it指向的是一个对象,而不是一个数值,所以不能直接用输出运算符输出,所以我们要重载输出运算符<<
现在我们根据给定的对象(字符串)来删除学生
为什么程序会报错?
因为remove()这个函数是要将对象s的字符串和每一个学生对象的name比较的,比较就用到==这个符号,两个对象相比较,那就要重载运算符==
这样它就能把s2给删除了
容器7:priority_queue(优先队列)
包含头文件<queue>
操作基本和队列差不多;
最大值优先级队列、最小值优先级队列
优先级队列适配器 STL priority_queue
数据被放入priority_queue容器中,不需要指定位置,会自动按顺序排列好。
priority_queue相关操作
pq.empty() //判断优先队列是否为空
pq.size() //返回优先队列的长度
pq.top() //获取优先队列对头元素
pq.pop() //出队操作
pq.push(item) //进队操作
优先队列是线性结构中唯一可以排序的。
如何指定排序规则
优先队列其实不是用队列来实现的,它真正用来存放数据的容器默认是用vector实现(也可以用deque来实现)。
priority_queue<int> p1; //默认是 最大值优先级队列,等价于
priority_queue<int, vector<int>, less<int> > p1;
priority_queue<int, vector<int>, greater<int>> p2; //最小值优先级队列
注:less<int>和 greater<int>在这里和int并列是一个类型,在头文件里面有一个less的模板类,并且这个这个类里面重载了函数调用运算符,该重载函数的类型是个bool类型,greater同理。所以排序到底是按从小到大排序还是从大到小排序就是由less和greater这两个类决定的。
注意这里必须加一个空格,否则编译器会把>>当成输入运算符
也可以把这里改成deque,只是默认是vector
less<>与greater<>是什么?
这里涉及到一个概念:伪函数(或者仿函数),英文名叫functor。
尽管函数指针被广泛用于实现函数回调,但C++还提供了一个重要的实现回调函数的方法,那就是函数对象。functor,翻译成函数对象,伪函数,是重载了“()”操作符的普通类对象。从语法上讲,它与普通函数行为类似。greater<>与less<>就是函数对象。
因为他们不知道我们的容器里要放什么类型,所以greater<>与less<>写成了模板类
其实greater<>与less<>的功能我们也可以自己模拟:
如果把我们模拟的也改成模板类,就也变成显示调用了,而用myless<int>创建的对象就叫做函数对象:
如果priority_queue不包含int类型,而是包含自定义类型,容器如何排序?
比如我们把一个类的对象放到这个优先队列中,如果直接放的话就会报错:
因为在less<>这个函数对象中重载了函数调用运算符(),类似我们模拟的这样:
但是该重载函数中需要比较大小的运算符没有重载,我们往容器中存放的数据不再是int,而是一个类的对象,所以return a<b或者return a>b这里的比较大小符号需要重载。如果需要从小到大排序就重载>,反之则要重载<
注:谁需要运算就在谁的类里面重载运算符。因为这里是学生对象要比较运算,所以就在学生对象的类里面重载比较运算符
这样编译就通过了,如果想要让这些学生对象出队的话还需要重载输出运算符<<
重载<<
前面讲这些容器基本都是线性结构,接下来讲的容器是属于树形结构。树形结构比较复杂,但是它存在的意义就是提高查找的效率。当我们要处理的数据比较多,要求的效率比较高的时候就可以用set和map这两种容器。
容器8:set(集合,自动排序,元素唯一)
set是一个集合容器,其中所包含的元素是唯一的,集合中的元素按一定的顺序排列。元素插入过程是按排序规则插入,所以不能指定插入位置。
set采用红黑树变体的数据结构实现,红黑树属于平衡二叉树。在插入操作和删除操作上比vector快。
set不可以直接存取元素。(不可以使用at.(pos)与[ ]操作符,因为树形结构的结点不是连续的,链表也是不可以直接使用at.(pos)与[]操作符)。
multiset与set的区别:set支持唯一键值,每个元素值只能出现一次;而multiset中同一值可以出现多次。
不可以直接修改set或multiset容器中的元素值,因为该类容器是自动排序的。如果希望修改一个元素值,必须先删除原有的元素,再插入新的元素。
#include <set>
set的默认构造
set<int> setInt; //一个存放int的set容器。
set<float> setFloat; //一个存放float的set容器。
set<string> setString; //一个存放string的set容器。
multiset<int> mulsetInt; //一个存放int的multi set容器。
multi set<float> multisetFloat; //一个存放float的multi set容器。
multi set<string> multisetString; //一个存放string的multi set容器。
set的插入和迭代器
set.insert(elem); //在容器中插入元素,不需要指定位置
set.begin(); //返回容器中第一个数据的迭代器。
set.end(); //返回容器中最后一个数据之后的迭代器。
set.rbegin(); //返回容器中倒数第一个元素的迭代器。
set.rend(); //返回容器中倒数最后一个元素的后面的迭代器。
set的操作
set(const set &st); //拷贝构造函数
set& operator=(const set &st); //重载等号操作符
set.swap(st); //交换两个集合容器
set.size(); //返回容器中元素的数目
set.empty(); //判断容器是否为空
set.clear(); //清除所有元素
set.erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
set.erase(beg,end); //删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
set.erase(elem); //删除容器中值为elem的元素。
set.find(elem); //查找elem元素,返回指向elem元素的迭代器。
set.count(elem); //返回容器中值为elem的元素个数。对set来说,要么是0,要么是1。对multiset来说,值可能大于1。
set.lower_bound(elem); //返回第一个>=elem元素的迭代器。
set.upper_bound(elem); // 返回第一个>elem元素的迭代器。
set.equal_range(elem); //返回容器中与elem相等的上下限的两个迭代器。上限是闭区间,下限是开区间,如[beg,end)。
以上函数返回两个迭代器,而这两个迭代器被封装在名为pair的类里面。
pair
pair译为对组,可以将两个值视为一个单元。
pair<T1,T2>存放的两个值的类型,可以不一样,如T1为int,T2为float。T1,T2也可以是自定义类型。
pair.first是pair里面的第一个值,是T1类型。
pair.second是pair里面的第二个值,是T2类型。
以下这段代码直接这样将一个类的对象插入到set容器中的话编译就会报错
因为一个学生对象的属性有学号和名字等等,这样直接插入对象到容器中时,在容器内部不知道是按什么排序的。
所以我们需要在学生的类里面重载比较运算符并指定按照什么属性排序
接下来可以遍历一下这个容器,这个容器是支持迭代器的。
但是我们每次都要写一个这么长的类型比较麻烦,
我们可以用上C语言中最没用的关键字auto(比如我们平时在main函数中定义变量int a; 这个int a前面其实省略了一个auto,a叫自动变量,auto修饰局部变量,但是我们一般都省略不写),而在C++中,这个auto关键字非常有用。
不管多么复杂的类型,直接auto就行了,编译器就会根据给对象it赋值的结果,自动把it定义成对应的类型。
但是要注意,如果使用auto关键字编译器报错的话,编译的时候需要加上: -std=c++11
问题又来了,想要输出对象就要运算符重载<<
重载<<
结果可以看到它就自动按名字排序好了顺序
接下来演示删除操作
查找操作
前面学习的set是一个结点一个结点的集合容器,
接下来学习的最后一个容器map就将一个结点又分成了两块,里面保存了一个key和value,key和value的类型不固定。排序的时候是按照key来排序的。
容器9:map(映射,键值唯一)
翻译过来是映射
map是标准的关联式容器,一个map是一个键值对序列,即(key,value)对。它提供基于key的快速检索能力。
map中key值是唯一的。集合中的元素按一定的顺序排列。元素插入过程是按排序规则插入,所以不能指定插入位置。
map的具体实现采用红黑树变体的平衡二叉树的数据结构。在插入操作和删除操作上比vector快。
map可以直接存取key所对应的value,支持[ ]操作符,如map[key]=value。
multimap与map的区别:map支持唯一键值,每个键只能出现一次;而multimap中相同键可以出现多次。multimap不支持[]操作符。
#include <map>
map的构造
map/multimap采用模板类实现,对象的默认构造形式:
map<T1,T2> mapTT;
multimap<T1,T2> multimapTT;
如:
map<int, char> mapA;
map<string,float> mapB;
map的插入
map.insert(...); //往容器插入元素,参数是创建pair的对象,返回一个键值对pair<iterator,bool>
假设 map<int, string> mapStu; 在map中插入元素的三种方式:
一、通过pair的方式插入对象
mapStu.insert( pair<int,string>(3,"小张") );
二、通过pair的方式插入对象
mapStu.inset(make_pair(-1, “校长-1”));
三、通过value_type的方式插入对象
mapStu.insert( map<int,string>::value_type(1,"小李") );
注:value_type() 是map<int,string>这个类里面的一个静态成员函数,可以通过类名调用
- 通过数组的方式插入值
注:在map<int,string>这个类里面重载了下标运算符[ ]
mapStu[3] = “小刘";
mapStu[5] = “小王";
注意:用insert插入和下标运算符[ ]插入有什么区别?
我们知道map里面的key是不能重复的,用insert()插入如果重复的话会返回错误,如果用下标运算符[ ]插入的话,即使插入重复也不会返回错误,即使重复它会覆盖。
前三种方法,采用的是insert()方法,该方法返回值为pair<iterator,bool> ,通过判断这个bool类型我们就知道插入成功还是失败了。
第四种方法非常直观,但存在一个性能的问题。插入3时,先在mapStu中查找主键为3的项,若没发现,则将一个键为3,值为初始化值的对组插入到mapStu中,然后再将值修改成“小刘”。若发现已存在3这个键,则修改这个键对应的value。
string strName = mapStu[2]; //取操作或插入操作
只有当mapStu存在2这个键时才是正确的取操作,否则会自动插入一个实例,键为2,值为初始化值。
map操作
map(const map &mp); //拷贝构造函数
map& operator=(const map &mp); //重载等号操作符
map.swap(mp); //交换两个集合容器
map.size(); //返回容器中元素的数目
map.empty();//判断容器是否为空
map.clear(); //删除所有元素
map.erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
map.erase(beg,end); //删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
map.erase(keyElem); //删除容器中key为keyElem的对组。
map.find(key); 查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回map.end();
map.count(keyElem); //返回容器中key为keyElem的对组个数。对map来说,要么是0,要么是1。对multimap来说,值可能大于1。
容器10:multimap(键值可重复)
(头文件包含<map>)
multimap 案例
1个key值可以对应多个valude =è分组
公司有销售部 sale (员工2名)、技术研发部 development (1人)、财务部 Financial (2人)
人员信息有:姓名,年龄,电话、工资等组成
通过 multimap进行 信息的插入、保存、显示
分部门显示员工信息
要是这样直接输出的话会出问题
因为pair里面的second我们放的是一个员工对象,对象不能直接输出,需要重载输出运算符
容器的共同能力
所有容器提供的都是值(value)语意,而非引用(reference)语意。容器执行插入元素的操作时,内部实施拷贝动作。所以STL容器内存储的元素必须能够被拷贝(必须提供拷贝构造函数)。
除了queue与stack外,每个容器都提供可返回迭代器的函数,运用返回的迭代器就可以访问元素。
通常STL不会丢出异常。要求使用者确保传入正确的参数。
每个容器都提供了一个默认构造函数跟一个默认拷贝构造函数。
如已有容器vecIntA。
vector<int> vecIntB(vecIntA); //调用拷贝构造函数,复制vecIntA到vecIntB中。
与大小相关的操作方法(c代表容器):
c.size(); //返回容器中元素的个数
c.empty(); //判断容器是否为空
比较操作(c1,c2代表容器):
c1 == c2 判断c1是否等于c2
c1 != c2 判断c1是否不等于c2
c1 = c2 把c2的所有元素指派给c1
如何正确选择容器
deque的使用场景:比如排队购票系统,对排队者的存储可以采用deque,支持头端的快速移除,尾端的快速添加。如果采用vector,则头端移除时,会移动大量的数据,速度慢。
vector与deque的比较:
一:vector.at()比deque.at()效率高,比如vector.at(0)是固定的,deque的开始位置却是不固定的。
二:如果有大量释放操作的话,vector花的时间更少,这跟二者的内部实现有关。
三:deque支持头部的快速插入与快速移除,这是deque的优点。
list的使用场景:比如公交车乘客,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。
数据非常多的时候考虑用set和map:
set的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。
map的使用场景:比如按ID号存储十万个用户,想要快速要通过ID查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector容器,最坏的情况下可能要遍历完整个容器才能找到该用户。
组件3:算法
算法概述
算法部分主要由头文件<algorithm>,<numeric>和<functional>组成。
<algorithm>是所有STL头文件中最大的一个,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、反转、排序、合并等等。
<numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。
<functional>中则定义了一些模板类,用以声明函数对象。
STL提供了大量实现算法的模版函数,只要我们熟悉了STL之后,许多代码可以被大大的化简,只需要通过调用一两个算法模板,就可以完成所需要的功能,从而大大地提升效率。
#include <algorithm>
#include <numeric>
#include <functional>
算法分类
按操作对象分类:
直接改变容器的内容
将原容器的内容复制一份,修改其副本,然后传回该副本
按功能分类:
非可变序列算法 指不直接修改其所操作的容器内容的算法
计数算法 count、count_if
搜索算法 search、find、find_if、find_first_of、…
比较算法 equal、mismatch、lexicographical_compare
可变序列算法 指可以修改它们所操作的容器内容的算法
删除算法 remove、remove_if、remove_copy、…
修改算法 for_each、transform
排序算法 sort、stable_sort、partial_sort、
排序算法 包括对序列进行排序和合并
数值算法 对容器内容进行数值计算(比如并集或者交集)
常见算法
常用的查找算法:
adjacent_find()( adjacent 是邻近的意思),binary_search(),count(),
count_if()、equal_range()、find()、find_if()。
常用的排序算法:
merge()、sort()、random_shuffle()(shuffle是洗牌的意思) ,reverse()。
常用的拷贝和替换算法:
copy(),、replace(),
replace_if()、swap()
常用的算术和生成算法:
accumulate()( accumulate 是求和的意思)、fill(),。
常用的集合算法:
set_union()求并集、set_intersection()就交集,
set_difference()求差集。
常用的遍历算法:
for_each()、transform()( transform 是变换的意思)
注:这些函数有的会跟一个if,它是一个条件,比如find()直接查找数字3在哪,而find_if()可以找大于数字3的元素在哪,这个“大于3”就是一个条件。
组件4:仿函数(函数对象)
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象。一个类对象,表现出一个函数的特征,就是通过“对象名+(参数列表)”的方式使用一个类对象,如果没有上下文,完全可以把它看作一个函数对待。
这是通过重载类的operator()来实现的。
“在标准库中,函数对象被广泛地使用以获得弹性”,标准库中的很多算法都可以使用函数对象或者函数来作为自定的回调行为;
谓词
比如查找大于3的数,那“大于3”这个就是一个谓词。
谓词:
一元函数对象:函数参数1个;
二元函数对象:函数参数2个;
一元谓词 函数参数1个,函数返回值是bool类型,可以作为一个判断式(谓词可以是一个仿函数,也可以是一个回调函数)。
二元谓词 函数参数2个,函数返回值是bool类型
一元谓词函数举例:判断给出的string对象的长度是否小于6
bool GT6(const string &s)
{
return s.size() >= 6;
}
遍历算法
for_each: 用指定函数依次对指定范围内所有元素进行迭代访问。该函数不得修改序列中的元素。
for_each(v.begin(), v.end(), func);
vector,deque,list,set,map都可以使用这个for_each遍历,除了对栈和队列基本不挑容器。
换成List这个容器也可以
这里的show()是一个函数,我们也可以换成一个函数对象
for_each()的特点是遍历的过程中不能修改数据,如果需要修改数据就用transform
transform: 与for_each类似,遍历所有元素,但可对容器的元素进行修改
transform(v.begin(), v.end(), v.begin(),increase);
但是这个程序运行后出现了段错误
原因是v2创建后还是个空的容器,这个时候它里面是没有v2.begin()这个的迭代器的,可以理解为begin还是个空指针,所以程序就死掉了。
我们可以先扩容v2
有些地方这样写,也可以将修改后的数据放回到v里面
查找算法
adjacent_find():查找一对相邻重复(相等)元素,找到则返回指向这对元素的第一个元素的迭代器。
binary_search():在有序序列中查找value,找到则返回true。无序序列不可使用。
count():查找元素个数,用法:count(v.begin(), v.end(), 3);
count_if():有条件的统计,用法:count_if(v.begin(), v.end(), GreaterThree)
find():对指定范围内的元素与输入值进行比较。当匹配时,结束搜索,返回该元素的迭代器。
find_if():按条件查找,返回被找到的元素的迭代器。
用find_if查找
我们也可以用预定义函数对象来写。
预定义函数对象
预定义函数对象基本概念:标准模板库STL提前定义了很多预定义函数对象。
#include <functional> 必须包含。
等于:equal_to<Tpye> equal_to<string>;
不等于:not_equal_to<Type>
大于:greater<Type>
大于等于:greater_equal<Type>
小于:less<Type>
小于等于:less_equal<Type>
但是这样写会有一个问题,自己写的greater_four函数中我们可以自己指定返回大于4的数字,但是如果用这个greater<int>()这个对象我们无法指定大于4这个条件。
从报错的提示中可以看到它里面重载()的函数是传两个参数,一个参数是遍历到的数字,一个参数应该就是我们指定大于4的参数,可以填4,但是find_if总共只能传三个参数,那么我们怎么传这个4过去呢?
这个办法C++帮我们解决了,这里要用到函数适配器。
组件5:函数适配器
标准库提供一组函数适配器,用来特殊化或者扩展一元和二元函数对象。
注:刚才我们上面的greater是谓词,表示两者的关系,而且它属于二元谓词,一元谓词就是这个函数只有一个参数。二元谓词需要两个参数。所以我们greater需要传两个参数,这时候就要用到函数适配器。
常用适配器是:
- 绑定器(binder,是一个函数): binder通过把二元函数对象的一个实参绑定到一个特殊的值上,将其转换成一元函数对象。C++标准库提供两种预定义的binder适配器:bind1st和bind2nd,前者把值绑定到二元函数对象的第一个实参上,后者绑定在第二个实参上。
2、取反器(negator) : negator是一个将函数对象的值翻转的函数适配器。标准库提供两个预定义的ngeator适配器:not1翻转一元预定义函数对象的真值,而not2翻转二元谓词函数的真值。
常用函数适配器列表如下:
bind1st(op, value) (将velue绑定要op的第一个参数上面)
bind2nd(op, value) (将velue绑定要op的第二个参数上面)
not1(op)
not2(op)
问题
vector<string> v;
v.push_back(“aaa”);
v.push_back(“ccc”);
v.push_back(“ddd”);
v.find(v.begin(), v.end(), “ddd”);
v.find_if(v.begin(), v.end(), equal_to<string>());
//equal_to<string>()对象需要两个参数,如何传递?
v.find_if(v.begin(), v.end(), bind1st(equal_to<string>(), “aaa”));
排序算法
merge():合并两个有序序列,存放到另一个序列。
merge(vA.begin(), vA.end(), vB.begin(), vB.end(), vC.begin());、//将vA和vB合并的结果放到vB(有序的)
sort():以默认升序的方式重新排列指定范围内的元素(默认快速排序)
sort(vecStu.begin(), vecStu.end(), Compare);
random_shuffle():对指定范围内的元素随机调整次序。
random_shuffle(vecInt.begin(), vecInt.end());
reverse():对指定范围内的数据反转。
reverse(vecInt.begin(), vecInt.end());
拷贝和替换
copy():拷贝指定范围内的数据。
copy(vecIntA.begin(), vecIntA.end(), vecIntB.begin());
replace():将指定范围内的所有等于oldValue的元素替换成newValue。
replace(beg, end, oldValue, newValue);//在区间内找到old替换成new数据
replace_if():将指定范围内所有操作结果为true的元素用新值替换。
replace_if(vecIntA.begin(), vecIntA.end(), GreaterThree, 8); //将大于3的替换成8
swap(): 交换两个容器的元素
算数和生成算法
accumulate():对指定范围内的元素求和,然后结果再加上一个由val指定的初始值。
accumulate(vecIntA.begin(), vecIntA.end(), 100);//将begin到end区间的数加起来再加上100(这个100可以不写,默认是0)
fill():将输入值赋给标志范围内的所有元素。
fill(vecIntA.begin(), vecIntA.end(), 8); //将这个区间的数填充成8
集合算法
set_union:构造一个有序序列,包含两个有序序列的并集。
set_intersection:构造一个有序序列,包含两个有序序列的交集。
set_difference:构造一个有序序列,该序列保留第一个有序序列中存在而第二个有序序列中不存在的元素。
set_union(vecIntA.begin(), vecIntA.end(), vecIntB.begin(), vecIntB.end(), vecIntC.begin());//将A和B并后放在C中。
下次就开始学习数据库了,敬请期待!
如有问题可评论区或者私信留言,如果想要进扣扣交流群请私信!