欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析2
目录
- 👉🏻vector概念
- 👉🏻vector constructor
- 👉🏻vector 容量
- 👉🏻vector 增删查改
- 🌅vector的模拟实现
- 构造函数和析构函数
- 拷贝构造、赋值、swap
- size和capacity
- reserve
- resize
- push_back
- 迭代器和访问
- insert
- erase
- 模板构造函数(迭代器区间)
- 构造函数(初始化n个val值)
- 🌩迭代器失效问题
- insert和erase导致的迭代器失效
- 👉🏻电话号码的字母组合
👉🏻vector概念
作为C++STL中的一个类模板,vector是一个动态数组容器,可以存储任意类型的元素。它提供了许多方便的方法来操作和管理数组。
以下是vector的一些特点和用法:
- 动态大小:vector可以根据需要自动调整大小,无需手动指定数组的大小。
- 随机访问:可以通过索引来访问和修改vector中的元素,例如myVector[0]。
- 插入和删除:可以在任意位置插入和删除元素,例如myVector.insert(position, value)和myVector.erase(position)。
- 动态增长:当vector的容量不足时,会自动分配更多的内存空间,以容纳更多的元素。
- 迭代器支持:可以使用迭代器来遍历vector中的元素,例如使用for循环或std::for_each函数。
- 元素访问:可以使用at()方法或[]运算符来访问元素,例如myVector.at(index)或myVector[index]。
- 大小和容量:可以使用size()方法获取vector中元素的数量,使用capacity()方法获取vector的容量。
vector官方文档
👉🏻vector constructor
- vector()(重点) 无参构造
- vector(size_type n, const value_type& val = value_type()) 构造并初始化n个val
- vector (const vector& x); (重点) 拷贝构造
- vector (InputIterator first, InputIterator last); 使用迭代器进行初始化构造
👉🏻vector 容量
- size 获取数据个数
- capacity 获取容量大小
- empty 判断是否为空
- resize(重点) 改变vector的size
- reserve (重点) 改变vector的capacity
capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。
这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义
的。vs是PJ版本STL,g++是SGI版本STL。
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问
题
👉🏻vector 增删查改
-
push_back(重点) 尾插
-
pop_back (重点) 尾删
-
find 查找。(注意这个是算法模块实现,不是vector的成员接口)
-
insert 在position之前插入val
-
erase 删除position位置的数据
-
swap 交换两个vector的数据空间
-
operator[] (重点) 像数组一样访问
🌅vector的模拟实现
构造函数和析构函数
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
}
~vector()
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
拷贝构造、赋值、swap
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和capacity
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endofstorage - _start;
}
reserve
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;
}
}
这样就解决问题了😄
resize
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++;
}
}
}
push_back
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];
}
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;
}
erase
会导致迭代器失效版本:
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*,那初始化也就是整型。
那如果我想初始化为字符串呢?
所以这里模板构造函数就显示出它的神通广大了,只要是迭代器区间,我都可以接收,这不香吗?😍
⭐️补充小知识点
当我们写了拷贝构造函数、模板构造函数、或者其它重载构造函数时
总之只要写了,默认无参构造函数一定要写出来(即使你并没有做什么)
默认无参构造函数是最基本的,如果不写,编译器就会使用它自己的默认构造函数。而这时,你写的其它构造函数都不会生效了
构造函数(初始化n个val值)
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++中的迭代器失效是指在对容器进行修改操作后,之前获取的迭代器可能会变得无效。这意味着在迭代器失效后,尝试使用该迭代器进行访问或操作容器的行为将导致未定义的行为。
迭代器失效的常见情况包括:
- 在向容器中插入或删除元素后,指向被修改位置的迭代器会失效。
- 在对容器进行重分配内存的操作后,所有指向容器元素的迭代器都会失效。
- 在使用迭代器遍历容器时,如果在遍历过程中对容器进行了修改操作,迭代器也会失效。
为了避免迭代器失效,可以采取以下措施:
- 在进行插入或删除操作后,更新迭代器,使其指向有效的位置。
- 在遍历容器时,避免在循环内部对容器进行修改操作。
- 在进行可能导致迭代器失效的操作之前,先将迭代器保存下来,以便需要时重新定位
insert和erase导致的迭代器失效
在上述的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;
}
};
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长