欢迎来到Cefler的博客
博客主页:那个传说中的man的主页
个人专栏:题目解析
推荐文章:题目大解析2
作为C++STL中的一个类模板,vector是一个动态数组容器,可以存储任意类型的元素。它提供了许多方便的方法来操作和管理数组。
以下是vector的一些特点和用法:
vector官方文档
capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。
这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义
的。vs是PJ版本STL,g++是SGI版本STL。
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问
题
push_back(重点) 尾插
pop_back (重点) 尾删
erase 删除position位置的数据
operator[] (重点) 像数组一样访问
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
}
~vector()
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(v.capacity());//先开一个和v一样大的空间
//而后直接将v中的数据插入到_start中
for (auto x : v)
{
push_back(x);
}
}
//写完拷贝构造后,我们就可以开始写赋值了
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
void swap(vector<T>& v)//打工人swap
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endofstorage - _start;
}
void reserve(size_t n)
{
if (n > capacity())//不缩容
{
T* tmp = new T[n];
size_t sz = size();//记录一下_finish和_start的偏移量✨
if (_start != nullptr)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;//释放旧空间
}
_start = tmp;
_finish = tmp + sz;
_endofstorage = _start + n;
}
}
这里memcpy()有个大坑,如果针对整型等浅拷贝的拷贝是没问题的。
但是如果拷贝的类型是string呢?
我们这里举个例子:
void test_vector3()
{
vector<string> v;
for (int i = 0; i < 5; i++)
{
v.push_back("11111111111111111111");
}
for (auto str : v)
{
cout << str << " ";
}
}
那么这里错在哪呢?
所以为了避免浅拷贝,我们还是只能自己动手,丰衣足食了,就不借助memcpy了。
这里我们直接用string的赋值运算符,可以直接拷贝构造无需担心浅拷贝问题。
void reserve(size_t n)
{
if (n > capacity())//不缩容
{
T* tmp = new T[n];
size_t sz = size();
if (_start != nullptr)
{
for (int i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;//释放旧空间
}
_start = tmp;
_finish = tmp + sz;
_endofstorage = _start + n;
}
}
这样就解决问题了
void resize(size_t n, const T& val = T())//因为T()是匿名对象是临时变量具有常性,所以引用传参不能权限放大得加const
{
if (n <= size())
{
_finish = _start + n;
}
else
{
reserve(n);//先开辟n大的空间
//然后再进行插入数据
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
val = T()主要是因为T可能会是int、string等,不能单单就令其为0,用匿名对象构造初始化更好
void push_back(const T& x)
{
if (_endofstorage == _finish)
{
size_t cp = capacity() == 0 ? 4 : capacity() * 2;
reserve(cp);
//T* tmp = new T[cp];
//if (_start != nullptr)
//{
// memcpy(tmp, _start, sizeof(T) * size());
// delete[] _start;//释放旧空间
//}
//_finish = tmp + size();//_finish = tmp + size()一定要先写,那不然_start先修改,size()大小会不理想
//_start = tmp;
_endofstorage = _start + cp;
}
*_finish = x;
_finish++;
}
typedef T* iterator;//记得要公有
typedef const T* const_iterator;//
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finish;
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos)const //写个常量访问
{
assert(pos < size());
return _start[pos];
}
void insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage)//扩容情况
{
//迭代器失效问题:扩容后_start指向新空间,但是Pos仍然指向旧空间
//我们先记录原来_start和Pos的偏移量
//扩容后,pos再指向新空间中新位置
size_t offset = pos - _start;//记录偏移量
reserve(capacity() == 0 ? 0 : capacity() * 2);
pos = _start + offset;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;//数据挪动
end--;
}
*(pos) = x;
++_finish;
}
会导致迭代器失效版本:
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator end = _finish - 1;
while (pos < end)
{
*pos = *(pos + 1);
pos++;
}
_finish--;
}
vector的构造函数中,还提供了模板构造函数。
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
有人会问,既然是用迭代器区间初始化,为什么不能用vector自己本身的iterator呢?
可以是可以,但格局就小啦。
如果我是用vector自己本身的iterator区间,那么假如说我这个vector的iterator是int*,那初始化也就是整型。
那如果我想初始化为字符串呢?
所以这里模板构造函数就显示出它的神通广大了,只要是迭代器区间,我都可以接收,这不香吗?
⭐️补充小知识点
当我们写了拷贝构造函数、模板构造函数、或者其它重载构造函数时
总之只要写了,默认无参构造函数一定要写出来(即使你并没有做什么)
默认无参构造函数是最基本的,如果不写,编译器就会使用它自己的默认构造函数。而这时,你写的其它构造函数都不会生效了
vector(size_t n, const T val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
为什么这里还要多此一举再写个重载?
因为我们上面的模板函数缘故,为了使(int,int)的传参更好的匹配到构造函数(初始化n个val值),我们写了一个参数类型为int 的n
C++中的迭代器失效是指在对容器进行修改操作后,之前获取的迭代器可能会变得无效。这意味着在迭代器失效后,尝试使用该迭代器进行访问或操作容器的行为将导致未定义的行为。
迭代器失效的常见情况包括:
为了避免迭代器失效,可以采取以下措施:
在上述的vector模拟实现中,我们实现了insert和erase的功能。
但是它们会导致怎样的迭代器失效问题呢?
insert
void insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage)//扩容情况
{
//迭代器失效问题:扩容后_start指向新空间,但是Pos仍然指向旧空间
//我们先记录原来_start和Pos的偏移量
//扩容后,pos再指向新空间中新位置
size_t offset = pos - _start;//记录偏移量
reserve(capacity() == 0 ? 0 : capacity() * 2);
pos = _start + offset;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;//数据挪动
end--;
}
*(pos) = x;
++_finish;
}
上述代码是经过优化改正的,这个是已经解决迭代器问题了。
但我们还得分析一下问题所在
为什么要记录pos和_start偏移量?
因为我们在扩容时,会开辟新空间,当_start指向新空间时,此时pos还是指向旧空间。
所以为了使pos指向正确的位置,我们得先记录它和_start的偏移量,而后新的_start加上偏移量就是pos在新空间的位置。
erase
会导致迭代器失效版本
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator end = _finish - 1;
while (pos < end)
{
*pos = *(pos + 1);
pos++;
}
_finish--;
}
我们举个删除偶数的例子。
void test_vector2()
{
vector<int> v;
for (int i = 1; i <= 6; i++)
{
v.push_back(i);
}
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
v.erase(it);
}
it++;
}
for (auto x : v)
{
cout << x << " ";
}
}
我们发现这里崩溃出错了,而且问题是pos>_finish引起的。
我们看看具体过程
上述过程中的主要问题就是it进行++后指向出现的问题。
那么怎么解决这个问题呢?
实际上c++官方就已经注意到了这个问题,所以,它所设置的erase,会返回函数调用擦除的元素后面元素的新位置的迭代器。
所以,在模拟实现这里我们也要这么做。
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator end = _finish - 1;
iterator p = pos;
while (p < end)
{
*p = *(p + 1);
p++;
}
_finish--;
return pos;//返回被删的位置
}
所以可以有
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
it++;//不是偶数再++
}
总结:
上述中,我们解决迭代器失效问题的主要思路就是在进行插入或删除操作后,更新迭代器,使其指向有效的位置。
而迭代器所谓失效,就是看看在具体情况中,它所指向的元素位置是否和我们预想的有所偏差,如果有偏差,则就会导致迭代器失效
原题链接:电话号码的字母组合
class Solution {
public:
const char* numsArr[10] = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
void Combine(const string& digits,string combinestr,int i ,vector<string>& ret)
{
if(i == digits.size())//当数字中的字母全部都进行完组合后
{
ret.push_back(combinestr);
return;
}
int num = digits[i] - '0';
string str = numsArr[num];
for(auto ch:str)//将当前数字代表的全部字母拿去依次拿去跟其它数字的字母进行组合
{
Combine(digits,combinestr+ch,i+1,ret);
}
}
vector<string> letterCombinations(const string& digits) {
vector<string> v;//存储全部组合的字符串
if(digits=="")
return v;
string str;//这个是专门用来组合的字符串
int i =0;
Combine(digits,str,i,v);
return v;
}
};
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注❤️ ,学海无涯苦作舟,愿与君一起共勉成长