面试笔记---vector

381 阅读5分钟

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来指向当前配置空间的尾端。该图中扩容和迭代器变化步骤如下:

    1. vector vec(2,5),即申请两个元素空间并赋值5,此时start指向起始地址,finish指向两元素空间末尾,end_of_storage指向申请空间(两元素)末尾;
    1. 当push_back(4)时,原2个元素空间已满,申请二倍空间,复制5,5于新空间,并填入4,此时由于空间重新分配,原迭代器失效,新start指向起始地址,finish指向4之后,end_of_storage指向申请空间(四个元素)末尾;
    1. push_back(3)时,原空间共有4个位置,已填入3个元素,3加入后刚好填满;
    1. 当push_back(2)时,原4个元素空间已满,便申请一块8元素的新空间,再将2填入;
    1. 再将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就认为该位置迭代器失效了

删除元素的迭代器会发生变化,删除位置之后的迭代器需重新调整。