Vector

在这里插入图片描述

欢迎来到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的一些特点和用法:

  1. 动态大小:vector可以根据需要自动调整大小,无需手动指定数组的大小。
  2. 随机访问:可以通过索引来访问和修改vector中的元素,例如myVector[0]。
  3. 插入和删除:可以在任意位置插入和删除元素,例如myVector.insert(position, value)和myVector.erase(position)。
  4. 动态增长:当vector的容量不足时,会自动分配更多的内存空间,以容纳更多的元素。
  5. 迭代器支持:可以使用迭代器来遍历vector中的元素,例如使用for循环或std::for_each函数。
  6. 元素访问:可以使用at()方法或[]运算符来访问元素,例如myVector.at(index)或myVector[index]。
  7. 大小和容量:可以使用size()方法获取vector中元素的数量,使用capacity()方法获取vector的容量。

vector官方文档

vector constructor

Vector_第1张图片

  • 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的成员接口)
    Vector_第2张图片

  • insert 在position之前插入val
    Vector_第3张图片

  • erase 删除position位置的数据

  • swap 交换两个vector的数据空间
    Vector_第4张图片

  • 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 << " ";
		}
	}

Vector_第5张图片
那么这里错在哪呢?
Vector_第6张图片
所以为了避免浅拷贝,我们还是只能自己动手,丰衣足食了,就不借助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++;
					}
				}
			}

val = T()主要是因为T可能会是int、string等,不能单单就令其为0,用匿名对象构造初始化更好

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_第7张图片
如果我是用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 << " ";
		}
	}

Vector_第8张图片
我们发现这里崩溃出错了,而且问题是pos>_finish引起的。
我们看看具体过程
Vector_第9张图片
上述过程中的主要问题就是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;
 
    }
};

如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注❤️ ,学海无涯苦作舟,愿与君一起共勉成长

在这里插入图片描述

你可能感兴趣的:(c++)