在C++标准模板库(STL)中,vector无疑是最常用且最重要的容器之一。它结合了数组的高效随机访问和动态内存管理的灵活性,是每个C++开发者必须掌握的工具。本文将带你全方位深入理解vector,从基本使用到底层实现原理,让你真正掌握这个强大的容器
在学习数据结构中我们已经实现了C语言版本的顺序表,在C++中Vector就是C语言中的顺序表,其是C++标准库的一个序列容器,能够动态增长和缩小,提供了对元素的随机访问能力。其底层实现是一个动态数组,这意味着元素在内存中是连续存储的。
主要特性:
1.动态数组:可根据需要自动调整大小
2.随机访问:支持O(1)时间的元素访问
3.尾部操作高效:在末尾插入和删除元素效率高
4.中间操作相对低效:在中间插入或删除元素需要移动后续元素
如果想要了解C语言版本实现的Vector可以点击我哦《【初阶数据结构】从 0 到 1 速通顺序表:C 语言实现 + 手撕算法(附完整代码)》 由于C++的STL的学习具有相似性,因此文章的大体框架类似
在之前实现string时,C++设计的过于冗余,因此在实现Vector时便很好的解决了string的问题,下面我们通过对比string和Vector的文档,String文档 和vector文档 可以清楚看出vector更简洁
定义:

在C++中的定义,我们发现vector是模版实现的,那么就意味着,vector可以支持任何类型的数据
构造:

1.由于allocator是空间适配器,后续会介绍,这里仅把它当做默认构造
2 .用n个val构造
3 .迭代器区间构造
4 .拷贝构造
代码演示:
void test_vector1()
{
//默认构造
vector<int> v1;
//n个val
vector<int> v2(10, 1);
//迭代器构造
vector<int> v3(v2.begin(), v2.end());
//拷贝构造
vector<int> v4(v2);
}那么构造学完了,根据string的学习过程来看,我们实现了打印遍历,在这里依旧如此,同样遍历有三种方式:
1.下标+
2 .迭代器遍历,这里仅演示正向迭代器,反向和常量迭代器可以看string的实现
3 .范围for遍历底层(底层是迭代器)
//下标+[ ]遍历
for (size_t i = 0 ; i < v3.size(); i++)
{
cout << v3[i] << " ";
}
cout << endl;
//迭代器遍历
vector<int>::iterator it = v3.begin();
while (it != v3.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//范围for遍历(底层还是迭代器)
for (auto e : v3)
{
cout << e << " ";
}
cout << endl;
看完构造函数,那么我们还需要看下析构函数的实现
析构函数是自动调用的

理解vector的容量管理机制对于编写高效代码至关重要:
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
我们发现在vs编译器中vector和string一样,依旧是1.5倍扩容,那么在gcc编译器是否也是2倍扩容呢?
答案是正确的:

在了解完扩容机制后,我们同样希望不扩容,这时候我们的reserve就要登场了,这里我们直接reserve(100),就没有扩容了

下面我们来看看reserve的介绍:标准给出我们给出n,不一定开n,但一定大于等于n

和string,一样,在reserve时也会出现三种情况:
1.reserve的n大于capacity时,便会扩容
2 .reserve的n小于capacity,但是大于size,capacity并不会缩容(这里和string就有所不同了)
在这种情况下,string在vs编译器不会缩容,但是gcc会缩容,但是最多缩容到size,不会减少size()

在vector下,不管是哪个编译器平台都不会缩容

代码演示:
void test_vector2()
{
//TestVectorExpand();
vector<int> v(10, 1);
v.reserve(20);
//预计size是10,capacity是20
cout << v.size() << endl;
cout << v.capacity() << endl;
//不缩容,不影响size
v.reserve(15);
cout << v.size() << endl;
cout << v.capacity() << endl;
//不缩容,不影响size
v.reserve(5);
cout << v.size() << endl;
cout << v.capacity() << endl;
}vs编译器下的:

gcc编译器下:

3 .reserve的n小于szie下也不会影响size

resize和reverse一样,也是三种情况:
1.n小于size,在这种情况下,并不会缩容,但是会改变size,删除数据
2.size<n<capacity,插入数据,由于小于capacity,因此不会扩容
3.n>capacity,插入数据,由于小于capacity,因此会扩容

在这里insert不支持下标了,仅支持迭代器
代码演示:
void test_vector4()
{
vector<int> v(10, 1);
v.push_back(2);
v.insert(v.begin(), 0);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
//在第三个位置插入数据
v.insert(v.begin()+3, 10);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}同样这里仅支持迭代器删除,和string的也是类似,这里通过代码演示更容易清楚vector的修改函数的特点
void test_modification() {
vector<int> v = {1, 2, 3, 4, 5};
// 尾部插入
v.push_back(6);
cout << "push_back后: ";
for (auto e : v) cout << e << " ";
cout << endl;
// 尾部删除
v.pop_back();
cout << "pop_back后: ";
for (auto e : v) cout << e << " ";
cout << endl;
// 指定位置插入
v.insert(v.begin() + 2, 99);
cout << "插入99后: ";
for (auto e : v) cout << e << " ";
cout << endl;
// 删除指定位置元素
v.erase(v.begin() + 2);
cout << "删除99后: ";
for (auto e : v) cout << e << " ";
cout << endl;
// 清空vector
v.clear();
cout << "clear后大小: " << v.size() << endl;
}vector不支持流插入和流提取,因为vector的输入输出具有不确定性
下面我们可以自己实现下流插入和流提取
vector<int> v1(5, 0);
for (size_t i = 0; i < 5; i++)
{
cin >> v1[i];
}
for (auto e : v1)
{
cout << e << ", ";
}
cout << endl;
我们思考个有趣的问题,可以用vector替换string吗?
答案是不可以,因为最底层的区别是string有‘\0’,vector没有,因此string可以很好的兼容C语言,但是vector不可以,这里不可以为vector加入’\0以满足替换string,因为一旦加入后,那么vector/vector那些中的"\0"的意思无法确定
vector除了可以存储内置类型以外,当然还可以存储自定义类型,如之前实现的日期类,string也可以,这是为了以后的vector可以存其他容器做铺垫,这是因为vector本身是由模板实现的
void test_vector5()
{
vector<string> v1;
string s1("xxxx");
v1.push_back(s1);
v1.push_back("yyyyyy");
for (const auto& e : v1)
{
cout << e << " ";
}
cout << endl;
}下面我们分析下vector<vector>代表什么?
我们便举一反三,可知,vector代表是顺序表是由一个个int对象组成的数组(一维数组)
那么vector<vector>就相当于数组里面存的对象是由一个个vector组成的元素,这里可以把它分解成我们学过的二维数组,既然数组中的每个元素都是vector,那么我们可以把其分解成二维数组,每一行代表其是由vector组成。
举个例子
void test_vector6()
{
vector<int> v(5, 1);
vector<vector<int>> vv(10, v);
for (int i = 0; i < vv.size(); i++)
{
for (int j = 0; j < vv[i].size(); j++)
{
cout << vv[i][j] << " ";
}
cout << endl;
}
}
有人可能说这不也可以使用静态数组吗?为什么需要这么麻烦,答案是vector可以扩容,方便某些场景的使用,下面我们看个经典题目——杨辉三角

由于杨辉三角是需要动态开辟空间,并且每一行并不是规整的,因此我们发现使用C语言实现会过于麻烦,下面我们通过C++的vector来分析下:杨辉三角的每一行的头与尾均是1,从第三行开始的中间元素等于上方两个相邻元素相加
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for(size_t i=0;i<numRows;++i)
{
vv[i].resize(i+1,1);
// vv[i].reize(i+1,0);
// //vv[i][0]=vv[i][vv[i].size()-1]=1;
// vv[i].front()=vv[i].back();
}
//由于前两行首尾都是1,上面处理过了
for(int i=2;i<vv.size();++i)
{
//首尾不需要管
for(int j=1;j<vv[i].size()-1;++j)
{
//该行元素等于上一行两个相邻元素相加
vv[i][j]=vv[i-1][j-1]+vv[i-1][j];
}
}
return vv;
}
};以上便是vector大部分接口的优点和需要避坑的讲解,我们在学习STL的知识时,要多练多写,明白底层原理,下一章我们便来探究下vector的模拟实现,深层次了解vector的工作原理,敬请期待