C++ STL standard template libaray 标准模板库
一、标准容器
顺序容器
vector
底层数据结构:动态开辟的数组,每次以原来空间大小的2倍进行扩容(开辟两倍大小的新数组,拷贝构造原数组对象,析构原数组对象,回收原数组内存)
vector<int> vec
;
增加:
vec.push_back(20);
末尾添加元素 O(1) 导致容器扩容
vec.insert(it,20);
it迭代器指向的位置添加一个元素20 O(n) 导致容器扩容
删除:
vec.pop_back();
末尾删除元素 O(1)
vec.erase(it);
删除it迭代器指向的元素 O(n)
查询:
operator[]
下标的随机访问 vec[5] O(1)
iterator
迭代器进行遍历
find,for_each
foreach
=> 底层通过iterator来实现的
常用方法介绍:
size()
empty()
reserve(20)
:vector预留空间的 只给容器底层开辟指定大小的内存空间,并不会添加新的元素
resize(20)
:容器扩容用的 不仅给容器底层开辟指定大小的内存空间,还会会添加新的元素
swap
:两容器进行元素交换
注意1:resize和reserver的区别
reserve预留空间 只给容器底层开辟指定大小的内存空间,并不会添加新的元素
resize:容器扩容 不仅给容器底层开辟指定大小的内存空间,还会添加新的元素
vector<int> ve;
ve.reserve(20);//预留内存空间
cout<<ve.empty()<<endl;//输出 1
cout<<ve.size()<<endl;//输出 0
ve.push_back(1);
ve.push_back(2);
ve.push_back(3);
for(int x:ve) cout<<x<<" ";//输出 1 2 3
vector<int> ve;
ve.resize(10);//开辟空间 添加元素
cout<<ve.empty()<<endl;//输出 0
cout<<ve.size()<<endl;//输出 10
ve.push_back(1);
ve.push_back(2);
ve.push_back(3);
for(int x:ve) cout<<x<<" ";//输出 0 0 0 0 0 0 0 0 0 0 1 2 3
注意2:vector调用clear()或者size(0),会清除容器内的所有元素,但是不会释放内存,如果想释放内存需要调用shrink_to_fit()函数
vector<int> ve{1,2,3,4,5,6,7,8,9,10};
cout<<ve.size()<<endl;//输出10
cout<<ve.capacity()<<endl;//输出10
ve. clear(); //ve.resize(0);
cout<<ve.size()<<endl;//输出0
cout<<ve.capacity()<<endl;//输出10
cout<<"ve[2]="<<ve[2]<<endl;//输出3 因为元素清空了 内存没有清空
ve.shrink_to_fit();
cout<<ve.size()<<endl;//输出0
cout<<ve.capacity()<<endl;//输出0
注意3:对容器进行连续插入或者删除操作(insert/erase),一定要更新迭代器,否则第一次insert或者erase完成,迭代器就失效了,比如下面代码经典错误
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> ve{1,2,3,4,5,6,7,8,9,10};
//把容器中所有的偶数删除
auto it = ve.begin();
for(;it!=ve.end();it++)
{
if(*it%2==0)
{
ve.erase(it);//错误迭代器已经失效了
}
}
for(int x:ve)
{
cout<<x<<" ";//输出为空
}
return 0;
}
正确写法:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> ve{1,2,3,4,5,6,7,8,9,10};
//把容器中所有的偶数删除
auto it = ve.begin();
while(it!=ve.end())
{
if(*it%2==0)
{
it = ve.erase(it);//删除之后返回迭代器位置
}else//迭代器指向的不是偶数才后移
{
++it;
}
}
for(int x:ve)
{
cout<<x<<" ";//输出1 3 5 7 9
}cout<<endl;
//给vector容器中所有的奇数前面都添加一个小于奇数1的偶数
for(it=ve.begin();it!=ve.end();++it)
{
if(*it%2!=0)
{
it = ve.insert(it,*it-1);//插入之后返回新的迭代器位置
++it;//迭代器需要向后多移动一次保证不重复插入
}
}
for(int x:ve)
{
cout<<x<<" ";//输出0 1 2 3 4 5 6 7 8 9
}
return 0;
}
deque
底层数据结构:动态开辟的二维数组(二维双端队列),一维数组从大小2开始,以2倍的方式进行扩容,每次扩容后,原来的二维数组(固定长度),从新的第一维数组的下标oldsize/2开始存放,上下都预留相同的空行,方便支持deque的首尾元素添加
注意:deque底层每一个第二维本身是连续的,但是所有的二维不是连续的,是重新new出来的(分段连续)
底层原理:
一般编译器默认大小
#define MAP_SIZE 2
第一维度大小
#define QUE_SIZE 4096/sizeof(T)
第二维度大小 T为数据类型
1.初始的队头队尾指针在队列中间(双端队列方便两头都可以插入删除)
2.随着元素的插入队头队尾指针移动
3.当队列满的时候,开辟新的二维空间
4.当所有队列满的时候进行2倍扩容(二维会放在一维中间位置)
操作:
deque<int> deq
;
增加:
deq.push_back(20);
从末尾添加元素 O(1) 可能扩容
deq.push_front(20);
从首部添加元素 O(1) 可能扩容
deq.insert(it,20);
it指向的位置添加元素 O(n) 可能扩容
删除:
deq.pop_back()
; 从末尾删除元素 O(1)
deq.pop_front()
; 从首部删除元素 O(1)
deq.erase(it)
; 从it指向的位置删除元素 O(n)
查询搜索
iterator (连续的insert和erase一定要考虑迭代器失效的问题)
list
底层数据结构:双向的循环列表
list mylist;
增加:
mylist.push_back(20);
从末尾添加元素 O(1) 可能扩容
mylist.push_front(20);
从首部添加元素 O(1) 可能扩容
mylist.insert(it,20);
it指向的位置添加元素 O(1) 可能扩容 往往需要先query查询操作
对于链表来说,查询操作效率就比较慢
删除:
mylist.pop_back()
; 从末尾删除元素 O(1)
mylist.pop_front()
; 从首部删除元素 O(1)
mylist.erase(it)
; 从it指向的位置删除元素 O(1)
查询搜索
iterator (连续的insert和erase一定要考虑迭代器失效的问题)
vector deque list对比
vector特点:动态数组,内存连续,2倍的方式进行扩容
deque特点:动态开辟的二维数组空间,第二维是固定长度的数组空间,扩容的时候(第一维的数组进行2倍扩容)
list特点:双向循环链表
vector和deque之间的区别?
1.底层数据结构:
2.前中后插入删除元素的时间复杂度:中间O(n),末尾都是O(1), 前插deque O(1) vector O(n)
3.对于内存的使用效率:vector需要的内存空间必须连续,deque可以分块进行数据存储,不需要内存必须连续
4.在中间进行insert或者erase,虽然时间复杂度都是O(n),但是vector都是连续空间比较方便,deque的第二维内存空间不是连续的,所以在deque中间进行元素的inset或者erase,指令肯定更多,造成元素移动的时候比vector要慢
deque和list比vector容器多出来的增加删除函数接口:push_front() pop_front()
vector和list之间的区别?
数组和链表的区别
数组:增加删除O(n) 查询O(n) 随机访问O(1)
链表:增加删除O(1) 查询(n)
容器适配器
怎么理解适配器?
1.适配器底层没有自己的数据结构,它是另一个容器的封装,它的方法,全部由底层依赖的容器进行实现
2.没有实现自己的迭代器
底层实现
template<typename T,typename Container=deque<T>>
class Stack
{
public:
void push(const T &val){con.push_back(val);}
void pop(){con.pop_back();}
T top()const{return con.back();}
private:
Container con;
};
stack
先进后出
push():入栈
pop():出栈
top():查看栈顶元素
empty():判断栈空
size():返回元素个数
queue
先进先出
push():入队
pop():出队
front():查看队头元素
back():查看队尾元素
empty():判断队空
size():返回队列元素个数
priority_queue
底层数据结构:大根堆
push():入队
pop():出队
top():查看队顶元素
empty():判断队空
size():返回元素个数
面试问题:
stack和queue底层依赖deque,为什么不依赖vector?
1.vector的初始内存使用效率太低了!没有deque好(vector需要二倍扩容,deque初始二维就开辟了1024个数组空间)
2.对于queue来说,需要支持尾部插入,头部删除O(1),如果queue依赖vector,其出队效率很低
3.vector需要大片连续内存,而deque只需要分段的内存,当存储大量的数据时,显得deque对于内存的利用率更好一些
priority_queue底层依赖vector,为什么不依赖deque?
底层默认把数据组成一个大根堆结构,在一个内存连续的数组上构建一个大根堆和小根堆(父子节点需要通过下标计算)
关联容器
(1)无序关联容器
底层数据结构:链式哈希表 增删查O(1)
unordered_set
#include<unordered_set>
增加:insert(val)
遍历:iterator
自己搜索,调用find
成员方法
删除:erase(key) erase(it)
存在:count()
unordered_multiset
#include<unordered_set>
#include<unordered_set>
增加:insert(val)
遍历:iterator
自己搜索,调用find
成员方法
删除:erase(key) erase(it)
个数:count()
unordered_map
#include<unordered_map>
unordered_multimap
#include<unordered_map>
#include<iostream>
#include<unordered_map>
#include<string>
using namespace std;
/*map存储的是pair对象
class pair
{
first;
second;
}
*/
int main()
{
unordered_map<int,string> map;
//插入
map.insert(make_pair(1000,"张三"));
map.insert({1010,"李四"});
map[1020] = "王五";
cout<<map.size()<<endl;
map.erase(1020);//删除
//查询 方式1
cout<<map[1020]<<endl;
//查询 方式2
auto it = map.find(1000);
if(it!=map.end())
{
cout<<" key:"<<it->first<<" value:"<<it->second;
}
//遍历方式一
for(const pair<int,string> &p:map)
{
cout<<" key:"<<p.first<<" value:"<<p.second;
}
//遍历方式二
it = map.begin();
for(;it!=map.end();it++)
{
cout<<" key:"<<it->first<<" value:"<<it->second;
}
return 0;
}
注意:
1.map的operator[]不仅有查询功能,如果key不存在,它会插入一对数据[key,type()]
2.map存储的是pair对象
(2)有序关联容器
底层数据结构:红黑树 增删查 O(log2n) n是底数(树的层数 树的高度)
set
multiset
#include
增加:insert(val)
遍历:iterator
自己搜索,调用find
成员方法
删除:erase(key) erase(it)
注意自定义类型需要重载运算符
#include<iostream>
#include<set>
#include<map>
#include<string>
using namespace std;
class Student
{
public:
Student(int id,string name):_id(id),_name(name){
}
//重载小于运算符
bool operator<(const Student &stu)const
{
return _id<stu._id;
}
private:
int _id;
string _name;
friend ostream& operator<<(ostream &out,const Student &stu);
};
ostream& operator<<(ostream &out,const Student &stu)
{
out<<"id:"<<stu._id<<" name:"<<stu._name<<endl;
return out;
}
int main()
{
set<Student> set;
set.insert(Student(1020,"李四"));
set.insert(Student(1000,"张文"));
for(auto it=set.begin();it!=set.end();it++)
{
cout<<*it<<endl;
}
return 0;
}
map
#include
mutimap
#include
注意使用operator[]需要提供默认参数构造函数
#include<iostream>
#include<set>
#include<map>
#include<string>
using namespace std;
class Student
{
public:
Student(int id=0,string name=""):_id(id),_name(name){
}
private:
int _id;
string _name;
friend ostream& operator<<(ostream &out,const Student &stu);
};
ostream& operator<<(ostream &out,const Student &stu)
{
out<<"id:"<<stu._id<<" name:"<<stu._name<<endl;
return out;
}
int main()
{
map<int,Student> mp;
mp.insert({1000,Student(1000,"张文")});
mp.insert({1020,Student(1000,"李广")});
mp.insert({1030,Student(1000,"高阳")});
cout<<mp[1020]<<endl;//使用[]需要提供默认参数构造函数 (因为可能查询的key不存在,则会构造一个新的对象)
auto it = mp.begin();
for(;it!=mp.end();it++)
{
cout<<"key:"<<it->first<<" value:"<<it->second;
}
return 0;
}
二、迭代器
iterator和const iterator
const_iterator:常量的正向迭代器 只能读不能写
iterator:普通的正向迭代器
两种迭代器其实是继承关系
class const_iterator
{
public:
const T& operator*(){//返回常引用 所以不能赋值
return *_ptr;
}
};
class iterator : public const_iterator
{
public:
T& operator*(){//返回普通引用 可以赋值
return *_ptr;
}
}
vector<int> const_iterator it = vec.begin();//子类的值赋值给父类 没毛病
reverse_iterator和const_reverse_iterator
reverse_iterator:普通的反向迭代器
const_reverse_iterator:常量的反向迭代器 只能读不能写
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> vec;
for(int i=0;i<20;i++)
{
vec.push_back(i);
}
//rbegin():返回的是最后一个元素的反向迭代器表示
//rend():返回的是首元素前驱位置的迭代器的表示
auto rit = vec.rbegin();
for(;rit!=vec.rend();++rit)
{
cout<<*rit<<" ";
}
return 0;
}
三、函数对象
(1)什么是函数对象
把有operator()小括号运算符重载函数的对象,称作为函数对象或者称为仿函数
(小括号运算符重载函数一个参数叫一元函数对象,二个参数叫二元函数对象)
C语言实现两数之和
int sum(int a,int b)
{
return a+b;
}
int ret = sum(10,20);
C++实现两数之和(函数对象)
class Sum
{
public:
operator()(int a,int b)
{
return a+b;
}
};
Sum sum;
int ret = sum(10,20);
(2)为什么使用函数对象
假如我们要实现比较两个数的大小,使用C语言实现
template<typename T>
bool greater(T a,T b)
{
return a>b;
}
template<typename T>
bool less(T a,T b)
{
return a<b;
}
cout<<greater(10,20)<<endl;
cout<<less(10,20)<<endl;
使用函数指针实现
template<typename T>
inline bool mygreater(T a,T b)
{
return a>b;
}
template<typename T>
inline bool myless(T a,T b)
{
return a<b;
}
template<typename T,typename Compare>
bool compare(T a,T b,Compare comp)//第三个参数接收函数指针
{
//通过函数指针调用函数,是没有办法内联的,效率很低,因为有函数调用开销
return comp(a,b);
}
int main()
{
cout<<compare(10,20,mygreater<int>)<<endl;
cout<<compare(10,20,myless<int>)<<endl;
return 0;
}
使用C++函数对象的版本实现
template<typename T>
class mygreater
{
public:
bool operator()(T a,T b)
{
return a>b;
}
};
template<typename T>
class myless
{
public:
bool operator()(T a,T b)
{
return a<b;
}
};
template<typename T,typename Func>
bool compare(T a,T b,Func fun)//第三个参数为函数对象
{
return fun(a,b);
}
int main()
{
cout<<compare(10,20,mygreater<int>())<<endl;//第三个参数为函数对象
cout<<compare(10,20,myless<int>())<<endl;
return 0;
}
1.通过函数对象调用operator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够inline内联调用)效率高
2.因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时的更多信息
(3)STL函数对象的使用
greater:函数对象 从大到小
less:函数对象 从小到大
举例1:priority_queue 模板类原型,第一个参数为类型
,第二个参数为容器
,第三个参数为函数对象
(默认less)
//默认大根堆 从小到大排序
priority_queue<int> queMax;
for(int i=0;i<10;i++)
{
queMax.push(rand()%100+1);
}
while(!queMax.empty())
{
cout<<queMax.top()<<" ";
queMax.pop();
}
//小根堆 从大到小排序
using MinHeap = priority_queue<int,vector<int>,greater<int>>;
MinHeap queMin;
for(int i=0;i<10;i++)
{
queMin.push(rand()%100+1);
}
while(!queMin.empty())
{
cout<<queMin.top()<<" ";
queMin.pop();
}
举例2:set模板类原型,第一个参数是类型
,第二个参数是函数对象
(默认less),第三个参数是容器空间配置器
set<int,greater<int>> set;//小根堆 从大到小排列
for(int i=0;i<10;i++)
{
set.insert(rand()%100);
}
for(int v:set)
{
cout<<v<<endl;
}
四、泛型算法
泛型算法 = template + 迭代器 + 函数对象
特点一:泛型算法的参数接收的都是迭代器
特点二:泛型算法的参数还可以接收函数对象(C函数指针)
绑定器
bindlst:把二元函数对象的operator()(a,b)的第一个形参绑定起来
bind2nd:把二元函数对象的operator()(a,b)的第二个参数绑定起来
sort
sort(vec.begin(),vec.end());//从小到大排序
sort(vec.begin(),vec.end(),greater<int>());//从大到小排序
find
遍历查找 O(n)
auto it1 = find(vec.begin(),vec.end(),43);
if(it1!=vec.end())
{
cout<<"43存在"<<endl;
}
binary_search
有序容器二分查找 O(log2n)
if(binary_search(vec.begin(),vec.end(),21))
{
cout<<"52存在"<<endl;
}
for_each
可以遍历容器的所有元素,可以自行添加合适的函数对象对容器的元素进行过滤
//输出所有偶数
for_each(vec.begin(),vec.end(),[](int val)->void{
if(val%2==0)
{
cout<<val<<" ";
}
});
测试代码:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
int arr[] = {12,4,78,9,21,43,56,52,42,31};
vector<int> vec(arr,arr+sizeof(arr)/sizeof(arr[0]));
for(int v:vec)
{
cout<<v<<" ";
}
sort(vec.begin(),vec.end());//从小到大排序
for(int v:vec)
{
cout<<v<<" ";
}
//有序容器二分查找 O(log2n)
if(binary_search(vec.begin(),vec.end(),21))
{
cout<<"52存在"<<endl;
}
//遍历查找 O(n)
auto it1 = find(vec.begin(),vec.end(),43);
if(it1!=vec.end())
{
cout<<"43存在"<<endl;
}
sort(vec.begin(),vec.end(),greater<int>());//从大到小排序
for(int v:vec)
{
cout<<v<<" ";
}
//输出所有偶数
for_each(vec.begin(),vec.end(),[](int val)->void{
if(val%2==0)
{
cout<<val<<" ";
}
});
}