1 类介绍
vector被称为向量容器,该容器擅长在尾部插入或删除元素,时间复杂度为O(1);而对于在vector容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。使用vector容器,需要包含<vector>,注意这里无扩展名。
1.1 vector内存扩展策略
vector属于序列式容器(sequence_container) :其中的元素都可序,但未必有序。
如图所示,vector的内存模型与数组较为相似,但vector的内存模型是动态增长的线性空间,动态增长的实质是:需求空间超过当前可用空间后,不是在原空间之后接续新空间。这是因为线性空间后不一定有足够大小的空间,因此重新申请一块更大的空间来作为载体,然后复制已有数据到新申请的内存空间。
具体操作为:首先配置一块新空间,然后将元素从原空间搬到新的空间上,再把原空间释放。(涉及到了新空间的配置和旧空间的释放) 新空间的大小为一般为原空间大小的二倍。注意:二倍增长并不是必然的,不同的编译环境可以有不同的实现,但若增长倍数小于2则可能会导致扩容频繁;增长倍数较大则可能导致申请了较大的空间而未使用,从而造成浪费。 此外,vector为了降低空间扩容的速度,在配置空间时留有一部分空间以便之后扩容,这就是size()和capacity()的区别。size()返回使用了多少空间,capacity()返回了配置了多少空间。当两者相等时说明vector容器已经满了,再插入元素需要扩容。
2 vector常用函数介绍
2.1 构造函数
| default (1) | explicit vector (const allocator_type& alloc = allocator_type()); |
| fill (2) | explicit vector (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type()); |
| range (3) | template vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type()); |
| copy (4) | vector (const vector& x); |
说明:
默认构造函数:构造一个没有元素的空对象
有参构造函数:explicit vector (size_type n, const value_type& val = value_type(),const allocator_type& alloc = allocator_type());构造一个还有n个元素的对象,每个元素都是val的拷贝
拷贝构造:vector (const vector& x);将x对象的元素拷贝给新对象
迭代器构造函数:vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());将对家的firrst last区间拷贝给新对象
2.2 构造函数的模拟实现
默认构造函数模拟实现
vector()
:start(nullptr),
final_end(nullptr),
finally(nullptr)
{
}
带参的构造函数
vector(int n, const T& value = T())
: start(nullptr)
, final_end(nullptr)
,finally(nullptr)
{
reserve(n);
while (n--)
{
push_back(value);
}
}
拷贝构造函数
//拷贝构造函数的传统写法
vector( vector<T>& var) {
//开辟和var一样大小的空间
this->start = new T[var.capacity()];
memcpy(start, var.start, sizeof(T)* var.size());
final_end= start+var.size();
finally = start + var.capacity();
}
vector(const vector<T>&v)
:start(nullptr),
final_end(nullptr),
finally(nullptr)
{
reserve(v.capacity());
for (const auto&e:v) {
this->push_back(e);
}
}
//构造函数的现代写法
vector(const vector<T>& v)
:start(nullptr),
final_end(nullptr),
finally(nullptr)
{
//定义临时对象调用构造初始化temp
vector<int> temp(v.begin(),v.end());
//this和临时对象调换3个指针的所指向的内存
this->Swap(temp);
//temp临时对象调用析构释放内存
}
迭代器构造函数
//其他容器的迭代器和vector迭代器相通
template<class inputiterator>
vector(inputiterator first, inputiterator last)
:start(nullptr),
final_end(nullptr),
finally(nullptr)
{
while (first != last) {
push_back(*first);
++first;
}
}
说明:迭代器构造函数可以将其他容器的数据拷贝到vector容器中,实现不同容器之间数据拷贝
2.3 "[]" 操作符(经典面试题:vector随机访问是怎样做到的?)
vector可以像数组一样,支持使用'[]'操作符根据下标获取元素。简单来讲,vector中元素大小固定,在知道start的基础上,我们只需要在其基础上进行地址偏移就能找到所需元素。
模拟实现:
T& operator[](size_t i) {
assert(i < size());
return *(start + i);
}
2.4 push_back()函数
函数原型:将新元素插入vector的尾端,在插入时需要关注两种情况:即vector当前是否还有空间,如果有则直接在备用空间上构造元素,调整迭代器finish;若没有,则需要扩充空间。
void push_back (const value_type& val);
函数实现:
void push_back(const T&var) {
if (final_end ==finally) {
size_t newcode = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcode);
}
*final_end = var;
++final_end;
}
说明:再增加元素时要考虑底层容量是不是够,需不需要扩容,需要扩容就先扩容,每次扩容都是扩展到原来容量的2倍
2.5 resize函数
函数原型:该函数跟string的resize功能是差不多的
void resize (size_type n, value_type val = value_type());
函数实现:在实现上跟string的resize也是差不多的也是分3中情况
void resize(size_t n,const T& var=T()) {
if (n <= capacity()) {
if (n > size()) {
while (final_end<start+n) {
*final_end = var;
++final_end;
}
}
else if (n <= size()) {
final_end = start + n;
}
}
else if (n>capacity()) {
reserve(n);
while (final_end < start + n) {
*final_end = var;
++final_end;
}
}
}
2.6 find函数
函数原型:vector没有自己的find()函数,vector要使用find,就得使用算法中得find函数
template <class InputIterator, class T>
InputIterator find (InputIterator first, InputIterator last, const T& val);
函数实现:
Iteratot find(Iteratot first, Iteratot end, const T&var) {
assert(first >= start && end <= final_end);
while (first-end>0) {
if (*first == var) {
return first;
}
}
return -1;
}
2.7 insert函数
函数原型:在指定位置position前插入n个值为x的元素,返回指向这个元素的迭代器,
| single element (1) | iterator insert (iterator position, const value_type& val); |
| fill (2) | void insert (iterator position, size_type n, const value_type& val); |
| range (3) | template void insert (iterator position, InputIterator first, InputIterator last); |
函数实现:
Iteratot insert(Iteratot iterator,const T&var) {
assert(iterator <= final_end && iterator >= start);
size_t pos = iterator - start;
if (final_end == finally) {
size_t newcode = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcode);
}
//插入操作
auto it = final_end;
while (it >= start+pos) {
*(it+1)=*it;
it--;
}
*iterator = var;
final_end++;
return iterator;
}
在指定位置iterator前插入一个原宿var,先检查容量需不需要扩容,紧接着将iterator和后边的元素往后移动一位,最后将元素插入到iterator位置
void insert(Iteratot iterator, size_t n,const T &var) {
assert(iterator <= final_end && iterator >= start);
size_t pos = iterator - start;
if (iterator + n > finally) {
size_t newcode = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcode);
}
//插入操作
auto it = final_end;
while (it >= start + pos+n) {
*(it + n) = *it;
it--;
}
while (iterator<= start + pos + n) {
*iterator = var;
iterator++;
}
final_end= start + pos + n;
}
说明:在指定位置前插入n个val,先检查容量需不需要扩容,需要扩容了就先扩容,接着再将iterator及后边的元素往后移动n个位置,最后将n个元素插入到iterator前边
3 vector迭代器及迭代器失效问题
先看一下vector底层原型
Iteratot start;
Iteratot final_end;
Iteratot finally;
vector底层模型就是迭代器,其中start指向vector得首部,fidnal_end指向有效数据得尾部,findally指向底层容量的尾部。当finally==final_end表明vector容量满了需要扩容,当start==filal_end
表 示容器为null
迭代器是行为类似指针的变量,相当于容器和算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。
迭代器是行为类似指针的变量,相当于容器和算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。
如上图所示,两个迭代器start和finish来指向目前线性空间已使用的头和尾,以迭代器end_of_storage来指向当前配置空间的尾端。该图中扩容和迭代器变化步骤如下:
-
- vector vec(2,5),即申请两个元素空间并赋值5,此时start指向起始地址,finish指向两元素空间末尾,end_of_storage指向申请空间(两元素)末尾;
-
- 当push_back(4)时,原2个元素空间已满,申请二倍空间,复制5,5于新空间,并填入4,此时由于空间重新分配,原迭代器失效,新start指向起始地址,finish指向4之后,end_of_storage指向申请空间(四个元素)末尾;
-
- push_back(3)时,原空间共有4个位置,已填入3个元素,3加入后刚好填满;
-
- 当push_back(2)时,原4个元素空间已满,便申请一块8元素的新空间,再将2填入;
-
- 再将1填入,此时仍有两个元素的空位以供备用; 所以finish和end_of_storage不是一个含义,finish指的是使用空间的末尾,end_of_storage是申请的空间的末尾。因此,使用size()和capacity()进行区分,size()的大小时start到finish的大小,capacity()的大小则是start到end_of_storage的大小。 此处需要注意的是,对vector的操作如果引起的空间重新分配,那么原vector的所有迭代器就都失效。
3.1迭代器的常用方法
vector的所有操作都可以从源码中看出端倪,此处将常用操作源码总结:
STL中规定容器的区间遵循前闭后开原则,即容器中的一对迭代器start和finish标识的前闭后开区间,从start开始,直到finish-1.迭代器finish所指的是“最后一个元素的下一位置”。这样定义的好处主要有两点:1. 为“遍历元素时,循环的结束时机”提供一个简单的判断依据。只要尚未到达end(),循环就可以继续下去; 2. 不必对空区间采取特殊处理手段。空区间的begin()就等于end();
Iteratot end()const {
return final_end;
}
Iteratot begin()const {
return start;
}
3.2 迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。 对于vector可能会导致其迭代器失效的操作有:resize reserve insert push_back等 这就都是在插入数据时改变了底层容量,所以引起了迭代器失效,因为vector底层就是没有在原来空间上扩容,而是新开辟一段空间,将指向老空间的3个指针指向新空间其他指针就会失效
erase的迭代失效问题
erase函数介绍
函数原型:
iterator erase(iterator first,iterator last)
| 函数项 | 说明 |
|---|---|
| 函数功能 | 用于删除vector容器中的一段区间的元素 |
| 参数 | 1.迭代器:iterator first 2.迭代器:iterator last |
| 返回值 | 指向删除的区间起点迭代器 |
| 时间复杂度 | O(n) |
// 删除某段元素 参数为需要删除段的迭代器起点和终点
iterator erase(iterator first,iterator last) {
iterator i = copy(last, finish, first); //将后面的元素前移
destroy(i,finish); //释放其后元素
finish = finish - (last - first); //调整迭代器
return first;
}
iterator erase(iterator position)
| 函数项 | 说明 |
|---|---|
| 函数功能 | 用于删除vector容器中的一个元素 |
| 参数 | 1.迭代器:iterator position |
| 返回值 | 指向被删除元素位置迭代器 |
| 时间复杂度 | O(n) |
// 删除某个元素 参数为需要删除的元素迭代器
iterator erase(iterator position) {
if(position + 1 != end()) {
copy(position + 1,finish,position); // 将position后的元素整体向前移动
}
--finish; // 迭代器前移
destroy(finish);
return position;
}
#include <iostream>
using namespace std;
#include <vector>
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据,导致pos迭代器失效。
v.erase(pos);
cout << *pos << endl; // 此处会导致非法访问
return 0;
}
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了
删除元素的迭代器会发生变化,删除位置之后的迭代器需重新调整。