一、常见容器的底层实现结构 and 常用接口梳理 and 迭代器失效
vector list dqueue string stack queue priority_queue map unordered_map set unordered_set
<1> vector
底层: 数组
扩容方式: vs下以2倍速度增长,g++ 下以1.5倍速度增长。 (两种环境下STL的版本不同,前者为PJ版本,后者为SGI版本)
初始化方式:
(1)vector<int> v; // vector 为空,size = 0,没有任何元素
(2)vector<int> v(v2) ; //
等价于 vector<int> v = v2; // v 将具有和 v2 一样的元素以及容量
(3)vector<int> v = {1,2,3,4,5,6}; // 等价于 vector<int> v{1,2,3,4,5,6} v被初始化为,列表中元素的拷贝。
(4)vector<int> v1{1,2,3,4,5,6};
vector<int> v2(v1.begin() + 2, v1.end() - 1); // v2被初始化为两个迭代器指定范围的元素,特备适合用来获取一个序列的子序列
(5)vector<int> v(7); //默认值初始化,v中将包含七个元素,每个元素进行缺省的值初始化,对于int来说,就是被赋值为了0。 元素数量大致可以预知时,而元素需要动态获取时,可以采用这种初始化方式。
(6)vector<int> v(7,3); // 与上面一样,v中将含有7个元素,不同的是,该七个元素被初始化为了3;
常用接口:
迭代器相关:
iterator begin(); // 正向迭代器:返回一个迭代器,指向vector对象的第一个元素
iterator end(); // 正向迭代器:返回一个迭代器,指向vector对象的最后一个元素的后边
reverse_iterator rbegin(); // 反向迭代器:返回一个迭代器,指向vector对象的最后一个元素
reverse_iterator rend(); // 反向迭代器:返回一个迭代器,指向vector对象的第一个元素的前面
容量相关:
size_t size(); // 获取当前对象中元素的个数
size_t capacity(); //获取当前对象的容量,是已经分配给当前对象的空间
void resize(size_t sz,T c = T()); //将当前对象的个数设置为sz个,sz比size大,则默认使用0填充,或者指定使用c来填充
void reserve(size_t n); //保证当前对象容量至少有为个
bool empty() const; //判空,判断当前对象元素是否为空,为空返回true,否则返回 false
元素获取:
front() cosnt; // 获取第一个元素
back() const; // 获取最后一个元素
v[n]; //使用下标随机访问
元素修改:
void push_back(const T& x); //尾插,插入元素之前会先检查是否有足够空间,不够则会进行扩容
void pop_back(); //尾删,不会改变容量
void insert(iterator pos,size_t n,const T& x); //指定位置插入元素,插入n个x
void insert(iterator pos,IpIter first,IpIter last); //指定位置插入迭代器所指范围的元素
iterator erase(iterator pos); //删除迭代器所指位置的元素
iterator erase(iterator first,iterator end); //删除迭代器所指范围内的元素
swap(); //交换两个对象的内容
clear(); //清空对象,即size设置为0
随机查找位置:
vector本身没有find方法,其find是依靠algorithm来实现的。
find(iterator begin(),iterator end(), num); // 返回一个迭代器指针
例子:
vector<int> v{ 1,2,3,4,5,6 };
vector<int>:: iterator it = find(v.begin(),v.end(),5);
迭代器失效问题:
向容器中添加或者删除元素的操作可能使指向容器的指针、引用、迭代器失效。一个失效的指针、引用、迭代器将不再表示任何元素。
(1)在向容器添加元素后,如果储存空间 “未重新分配” ,指向插入位置之前的元素的迭代器、指针、引用有效,但指向插入位置之后的将会失效。
(2)在从容器删除元素之后,指向被删元素之前元素的迭代器、引用、指针仍有效。尾后迭代器也就失效。
两种列子:
1.遍历时插入元素
iter = v.insert(iter,n); //正常遍历,想要指向下一个元素,就要跳过当前和被添加的元素
iter += 2;
2.遍历时删除元素
注意:erase函数返回的是删除之后的有效的迭代器
iter = v.eares(iter);
<2>list
底层实现:双向循环链表,再某些版本中是采用双向循环链表(例 SGI STL)
特点:可以在常规时间内,再序列已知的人任何位置插入或者删除元素,这就是我们使用list 而不适用vector 或者 queue的原因。
初始化方式:
(1)list<string> ls(20) 等价于 list<string> ls{20}; //初始化为含有20个元素,且都为空的链表
(2) list<int> ls(10,1); //和vector一样,这里的ls将含有十个元素,且每一个元素的值是1
(3) list<int> ls(ls1); //容器含有拷贝函数,生成另一个list的副本。
(4) list<int> ls(ls1.begin(),ls1.end()); // 可以用另一个list的开始迭代器和结束迭代器来初始化当前list。
常用接口:
迭代器:与vector大致相同
容量操作:
size_t size();
bool empty();
void clear();
元素访问:
reference front(); //返回第一个元素的引用
reference back(); //返回最后一个元素的引用
元素修改:
void push_front(const value_type& val); //在链表头部插入只为val的节点
void pop_front(); //删除链表头部结点
void push_back(); //尾插
void pop_back(); //尾删
insert(插入):
iterator insert(iterator pos,const value_type& val); //在pos之前插入一个元素val
iterator insert(iterator pos,size_type n,const value_type& val); //在pos位置之前插入n个val
iterator insert(iterator pos,iterator first,iterator last); // pos之前插入[first,last)迭代器范围内的所有元素,左闭右开
earse(删除):
iterator erase(const_iterator pos); //删除pos位置的元素
iterator erase(const_iterator first,const_iterator last); //删除[first,last) 迭代器范围内的元素,左闭右开
void swap(lsit& x); //交换两个list的元素
void resize(size_type n,value_type val = value_type()); //将现有的list中的有效元素变为n个,大于出当前size的用val填充
迭代器失效问题:
因为vector的元素是顺序存储的,所以删除一个元素会导致后面的所有元素会向前移动一个位置,所以当前位置之后的迭代器就会失效。(影响后面所有)
但是,list使用了不连续分配的内存,删除会使得当前位置的迭代器失效,但是不会失效其他的迭代器。
因此:低于list删除引起的迭代器失效的解决方式是为:
1.与vector相同,erase会返回下一个有效的迭代器
2.或者erase(iter++); //提前使得iter为下一个有效元素的迭代器
注:list的插入不会是的迭代器失效
<3>deque(双端队列) 底层结构:采用分块的线性存储结构(块的大小一般为512B,成为deque块),支持随机访问,可以通过下标来访问元素。所有的块儿通过map块进行管理,每个map数据项记录deque块的首地址,所以允许较为快速的随机访问。
特点:头部尾部都可以快速的插入元素,不需要移动其他元素
构造函数:
deque<T> deqT; //默认构造形式
deque(beg, end); //构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem); //构造函数将n个elem拷贝给本身。
deque(const deque &deq); //拷贝构造函数
主要操作函数:
push_back(T elem); //在容器尾部添加一个数据
push_front(T elem); //在容器头部插入一个数据
pop_back(); //删除容器最后一个数据
pop_front(); //删除容器第一个数据
insert(iterator pos, T elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(iterator pos,size_t n, T elem); //在pos位置插入n个elem数据,无返回值。
insert(iterator pos,iterator beg, iterator end); //在pos位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(iterator beg,iterator end); // 删除[beg,end)区间的数据,返回下一个数据的位置。
erase(iterator pos); // 删除pos位置的数据,返回下一个数据的位置
迭代器失效问题:
插入操作:
1、在队前或队后插入元素时(push_back(),push_front()),由于可能缓冲区的空间不够,需要增加map中控器,而中控器的个数也不够,所以新开辟更大的空间来容纳中控器,所以可能会使迭代器失效;但指针、引用仍有效,因为缓冲区已有的元素没有重新分配内存。
2、在队列其他位置插入元素时,由于会造成缓冲区的一些元素的移动(源码中执行copy()来移动数据),所以肯定会造成迭代器的失效;并且指针、引用都会失效。
删除操作:
1、删除队头或队尾的元素时,由于只是对当前的元素进行操作,所以其他元素的迭代器不会受到影响,所以一定不会失效,而且指针和引用也都不会失效;
2、删除其他位置的元素时,也会造成元素的移动,所以其他元素的迭代器、指针和引用都会失效。
<4> priority_queue 优先级队列(堆)
底层:本质上是通过一个堆实现的,包括队列的所有特性,只是在基础上添加了一个内部排序,它本质是一个堆实现的。
定义方式:
priority_queue<Type,Container,Functional> Type就是数据类型,Container是容器类型(如vector,queue等等,但不能使用list,默认使用vector),Functional就是比较方式。
//升序,小顶堆,堆顶元素最小
priority_queue<int,vector<int>,greater<int> > q;
//降序,大顶堆,堆顶元素最小
priority_queue<int,vector<int>,less<int> > q;
注意:
1.定义priority_queue时,若不指定容器类型,则默认使用vector,若不指定比较方式,则默认生成大堆,底层按照小于号进行比较。
例:
priority_queue<int> q; //定义为大堆,底层容器使用vector
2.当数据类型为自定义类型时,则需要在该类中对 > 或者 < 运算符进行重载,使其可以进行比较。
3.有些情况下,用户也需要提供比较器规则(重写仿函数)
例:
class Less
{
public:
bool operator()(const Date* pLeft,const Date* pRight)
{
return *pLeft < *pRight;
}
};
void TestPriority_queue()
{
//自己制定的比较规则
priority_queue<Date*,vector<Date*>,Less> q;
q.push(&Date(2019,10,11));
q.push(&Date(2019,10,12));
q.push(&Date(2019,10,13));
cout<<*q.top()<<endl;
}
接口: 和queue基本一致,包括 push(),pop(),empty(),size() 等;
<5> map && unordered_map && mutile_map
[1] map
底层实现:红黑树
基本特点:map是一类关联式容器,它的特点是删除和插入节点对迭代器的影响比较小,除了当前操作节点,对其他节点没有什么影响。
对于迭代器来说,只可以修改节点的value值,而不能修改key
功能:
1.有自动排序功能(默认升序less,降序为greater)
2.去重功能,不能有key值相同的节点
3.快速插入节点
4.快速删除
5.快速查找(重要)
插入元素方式:
map<int,int> mp;
1.//赋值方式插入
//插入时若key存在,则相当于修改当前key 的 vaule 为后面数值
//插入时若key不存在,则相当于相当于插入(1,1)
mp[1] = 1; //相当于插入(1,1)
2. insert();
由于map的每一个元素都是一个键值对,因此插入时需要插入一个pair
因此: mp.insert(pair<int,int>(1,1));
注意:insert在插入时,若插入的key值不存在,则会将当前键值对插入进去,否则无论vaule值是否不同,都不进行插入。
删除元素方式:
erase(T key); //通过key值删除
erase(iterator ite); //通过迭代器删除
查找接口:
iterator find(T key); //查找当前map中是否含有当前key,如果有返回指向该key位置的迭代器,如果没有则返回迭代器end();
迭代器失效问题:
由于map的底层是红黑树,因此每一个节点都是独立不连续的,所以使用迭代器来删除当前结点,只会是的当前的迭代器失效。插入不会使得迭代器失效。
因此为了防止该问题,我们在遍历时删除元素应如下处理:
mp.insert(iter++); //提前使得迭代器++到下一个有效的位置;
[2]multimap
底层及特点:基本与map相同,不同之处在于multimap可含有重复的key。
其次功能上的不同在于:multimap没有重载[],因此不支通过[]访问元素的value
其他常用接口:
count(T key); //返回某一个key出现的次数
[3]unordered_map
底层及特点:与map不同,unordered_map 采用哈希的方式实现,与map不同之处在于其没有排序功能
<6> set && unordered_set && multimap
对应特点与map类相似,区别在于set的元素不是键值对,是单一的数据类型。
<7> string(字符串)
对于字符串string我们最应掌握的就是一系列字符串接口的使用:
构造函数:
string s;
string s("hello") 等价于 string s = "hello";
string s(s1); //拷贝构造
容量类的接口:
size(); //返回字符串有效字符串的长度
capacity(); //返回总空间的大小
empty(); //判空
clear(); //清除有效字符,不改变空间大小
resize(8) //重新设置s字符串的有效个数为8,更改的是size
resize(10,'x') //重新设置字符串有效字符的长度为10,并且多余位置用x填充
遍历方式:
1.operator[] //类似与数组方括号操作
2.iterator begin() -> end() //迭代器遍历
3.范围for
修改操作:
追加操作:
append("hello"); //在字符串末尾追加一个字符串
append(2,'x'); //在字符串末尾追加两个字符x
append(string s,size_t pos,size_t n); //在当前字符串末尾追加,从s的pos位置开始,复制n个
也可以使用 += 等运算符进行上面操作。
提取子串:
substr(size_t pos,size_t n); //从pos位置开始,提取n个字符
c_str() ; //返回const char*
查找接口:
size_t find(char c); //查找某一个字符在字符串中第一次出现的位置
size_t find(string s); //查找某一个子串在当前字符串中第一次出现的位置
size_t rfind(char c); //从末尾开始,查找某一个字符在字符串中第一次出现的位置
size_t rfind(string s); //从末尾开始,查找某一个子串在当前字符串中第一次出现的位置
删除接口:
erase(size_t pos,size_t n); //从pos位置开始,删除n个字符,若不够则直接删除到字符串结束
erase(iterator iter); //删除当前迭代器所指向位置的字符
erase(iterator begin,iterator end); //删除迭代器所指范围内的字符
插入接口:
insert(size_t pos,string s); //从字符串的pos位置,开始插入字符串s
insert(size_t pos,size_t n,char c); //从pos位置开始插入n个字符c
s1.insert(size_t pos,string s2,size_t indes,size_t n); //从s1的0位置开始,把字符串s2的第2个位置复制3个字符插入
替换接口:
replace(size_t pos,size_t n,string s); //从索引为pos的位置替换n个字符换为 s
replace(iterator begin,iterator end,string s); //将迭代器所指范围内的字符替换为 s
交换函数:
s.swap(s1);
swap(s1,s2);