一、vector的介绍
- vector是表示可变大小数组的序列容器。
- 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
- vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因此存储空间比实际需要的空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
- 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。
二、vector的使用
vector和string的底层都是顺序表,且由于STL库函数的设计互通,因此使用也类似。
所以相关函数的使用细节可以参考文章:【STL模版库】string类:常用接口函数
下面的内容只做简介,不再做详细介绍。
2.1 构造函数
2.2 迭代器
2.3 容量相关接口
-
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
-
resize在开空间的同时还会进行初始化,影响size。
-
相同的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
2.4 增删查改
- 下面重点介绍的find、sort和swap函数都是由算法模块实现的函数模版,并不是vector的成员接口。
- 在使用前需要包含头文件<algorithm>,并展开std命名空间。
2.4.1 find
此函数模版的行为等效于:
template<class InputIterator, class T>InputIterator find (InputIterator first, InputIterator last, const T& val)
{while (first!=last) {if (*first==val) return first;++first;}return last;
}
- find仅仅是一个函数模版,会根据传入参数的不同类型实例化出适应各种类型的find函数。
- 这里使用迭代器遍历容器,是因为迭代器是所有容器的通用遍历访问方法。
- 使用引用传参避免发生拷贝构造,更高效。
- 如果找到了,返回对应元素的迭代器;如果找不到,返回last;
其他查找函数
find_if:
功能:返回范围中第一个满足指定条件的元素的迭代器
参数:共有3个,前两个参数是待查找的迭代器区间;最后一个参数是一个返回值为bool类型的一元函数指针,该函数用于条件判断。
同类函数:find_if_not,与find_if逻辑相反,参数和使用方法相同。
find_first_of:
功能:在母序列中从前往后查找子序列,返回母序列中第一次出现子序列的首元素的迭代器
参数:共有5个,前两个参数是母序列迭代器区间;中间两个是子序列迭代器区间;最后一个参数是一个返回值为bool类型的二元函数指针,该函数用于条件判断。
同类函数:find_end,与find_first_of查找方向相反是从后往前查找,参数和使用方法相同。
2.4.2 sort
C++STL库中的sort接口底层采用快速排序实现。
#include <iostream>
#include <algorithm> //sort
#include <functional> //greater
using namespace std;int main(){string s("hello412535");sort(s.begin(), s.end()); //第三个参数缺省默认排升序;//sort(s.begin(), s.end(), less<char>()); //也可以显示的传less也是排升序;for(auto e : v1){cout << e << " ";}cout << endl;sort(s.begin(), s.end(), greater<char>()); //传greater是排降序;for(auto e : v1){cout << e << " ";}cout << endl;return 0;
}
前两个参数同样是待排序的迭代器区间范围
第三个参数缺省默认升序,传less是升序排序,传greater是降序排序。
注意:
- 这里的greater和less是类模版。
- 传入的less<char>()和greater<char>()是实例化出的模版类定义的匿名对象。
- 要使用greater类模版需要包<functional>头文件
2.4.3 swap
此函数模版的行为等效于:
template <class T> void swap ( T& a, T& b )
{T c(a); a=b; b=c;
}
-
由此可见,算法模块中实现的swap函数模版,采用的是创建临时变量,赋值交换的方法。仅仅适用于内置类型,而对于自定义类型会进行1次拷贝构造和2次赋值拷贝。尤其针对需要进行深拷贝的对象,这种交换方式的效率极低。
-
因此,对于需要进行交换操作的自定义类型一般会自己实现成员交换函数,只对成员变量进行浅交换即可。
三、动态二维数组
vector不仅可以存储内置类型,还可以用于存储自定义类型如:vector<string>和vector<vector<int>>
3.1 vector<string>
vector<char> 能否替代 string?
回答:不能
- string字符串由’\0’结尾,而vector<char>不会。
- string类重载了流插入<<和流提取>>运算符,能更加方便的进行输入输出。
- string类中还提供了操作字符串的专用接口,如operator+=、字符串比较大小、to_string等等。
- 因此vector<char> 不能替代 string。
int main(){vector<string> strv;string str1("张三");strv.push_back(str1);strv.push_back(string("李四")); //[1]strv.push_back("王五"); //[2]for(const auto &name : strv) //[3]{cout << name << endl;}
}
解释:
- [1] 定义匿名对象,匿名对象的生命周期只在当前行。
- [2] 由于string类中定义了一个单参构造函数
string (const char* s);
,所以常量字符串"王五"可以进行隐式类型转换,构造临时string对象。 - [2] 类型转换形成的临时对象具有常性,而
void push_back (const value_type& val);
的参数类型是常引用,因此这种写法可行。 - [3] 范围for中的临时变量的类型建议写成引用,这是因为如果是普通变量会进行频繁地拷贝构造(尤其针对自定义类型),效率极低。如果循环中不做修改建议加const进行保护。
3.2 vector<vector<int>>
以题目【Leetcode.118】杨辉三角为例:
题解:
class Solution {
public:vector<vector<int>> generate(int numRows) {vector<vector<int>> vv; //使用vector定义二维数组vv,vv中的每个元素都是vector<int>vv.resize(numRows); //[1]for(size_t i = 0; i<vv.size(); ++i){vv[i].resize(i+1); //[2]vv[i].front() = 1;vv[i].back() = 1;}for(size_t i = 2; i<vv.size(); ++i){for(size_t j = 1; j<vv[i].size()-1; ++j){vv[i][j] = vv[i-1][j] + vv[i-1][j-1]; //[3]}}return vv;}
};
解释:
-
[1] 为vector<vector>对象开空间并初始化。
-
[1] 这是resize的函数原型:
void resize (size_type n, value_type val = value_type());
-
[1] 第二个参数用value_type类型的匿名对象做缺省值,相当于调默认构造生成临时对象。
-
[1] resize函数内利用临时对象拷贝构造生成新对象。
-
[1] 因此执行过这条resize语句之后的结构为:设numRows==5
-
[2] 为每个vector元素开空间并初始化,然后将每行的第一列和最后一列置为1。
-
[3] vector<vector>动态二维数组的访问:先后调用两个operator[]的重载函数,实现了二维数组的访问遍历。
-
[3] 还记得普通二维数组的访问方式吗?二维数组在内存中和一维数组一样是线性连续存储的,编译器会先将两个二维下标换算成一个一维下标,然后才能通过解引用访问。
-
最后生成的二维数组结构图示:
-
关于二维动态数组的深拷贝问题我会在下一节进行详解:【STL模版库】模拟实现vector类模版6.2