C++ vector的使用(蓝桥杯比赛必备知识)

                                         

目录

vector介绍 

vector的常用接口介绍

constructor构造函数

vector()

vector(size_type n, const value_type& val = value_type())

vector(const vector& x)

vector(InputIterator first, InputIterator last)

iterator

begin

end                      ​

rbegin

rend

 vector 空间增长问题

 size

capacity

 empty

resize

reserve

 reserve不同环境在的增容规则

vector增删查改

push_back

pop_back

find

insert

erase

swap

operator[]

 迭代器失效问题

因为扩容导致迭代器失效

解决方式

因为删除数据导致迭代器失效

解决方法

以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?


vector介绍 

1、vector是表示可变大小数组的序列容器。
2、就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3、本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4、 vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
5、 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6、 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_lists统一的迭代器和引用更好。

vector的常用接口介绍

        vector是非常常见的一个容器,在这里,就介绍一些常用的接口。其它一些接口如果用到时,查文档再了解学习就好了,毕竟一个人的学习精力有限嘛。老规矩,和string一样,在下一篇我会写一篇vector的模拟实现,希望大家支持一下博主~

constructor构造函数

vector()

         无参构造。

const allocator_type& alloc = allocator_type() :这个是空间适配器,在这里我们不用去管它,只用知道它是用来申请空间用的,可以提高申请空间效率就足够了。(下面的类似,我就不在这里介绍了)

举个栗子:

int main()
{
	vector<int> v;
	return 0;
}

vector<int> v:我们在使用vector时要用< 类型 >指定一下,它和string是有区别的。在这里我们指定了int,说明这个容器里面放的数据是int类型。

我们调试一下看看这个构造函数做了什么:

我们发现无参的构造函数size和capacity是0,说明里面是没有数据的。 

vector(size_type n, const value_type& val = value_type())

        

size_type n:要构造指定类型数据的个数。

const value_type& val = value_type():要初始化构造的vector里面存放的数据。

举个栗子:

int main()
{
	vector<int> v(5, 10);
	vector<vector<int>> vv(5, vector<int>(5, 100)); //二维数组
	return 0;
}

    vector<int> v(5, 10):初始化的vector里面存放了5个10
    vector<vector<int>> vv(5, vector<int>(5, 100)):vector里面放了5个vector<int>(5,100),vector里面又放了一个vector,相当于二维数组的功能。vector<int>(5,100)中vector初始化(存放)了5个100。

我们调试一下代码看看情况:

vector(const vector& x)

                       

 拷贝构造,用一个已经初始化好的对象来拷贝构造一个没有初始化的对象

举个栗子:

int main()
{
	vector<int> v1(5, 10);
	vector<int> v2(v1);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

我们可以看到v1的内容拷贝构造出了v2,所以v2和v1里面存放的数据一样 。

vector(InputIterator first, InputIterator last)

    

 使用迭代器区间构造初始化对象

举个栗子:

int main()
{
	vector<int> v1(5, 10);
	vector<int> v2(v1.begin(), v1.end());
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	vector<int> v3(v1.begin(), v1.begin() + 2);
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

        我们可以看到可以用一个相同类型的vector对象区间来构造初始化一个新对象,这个区间我们可以合理控制大小。 

注意:这个迭代器区间是左闭右开的,也就是说last指的是最后一个数据的下一个坐标的位置。

                      

iterator

begin                

 获取第一个数据位置的迭代器。

举个栗子:

int main()
{
	vector<int> v(5, 10);
	vector<int>::iterator it = v.begin();
	vector<int>::const_iterator it2 = v.cbegin();
	cout << *it << endl;
	cout << *it2 << endl;
	return 0;
}

运行结果:

begin:可以修改迭代器位置的值

cbegin: 不可以修改迭代器位置的值

          

end                     

 获取最后一个数据的下一个位置的迭代器。

举个栗子:

int main()
{
	vector<int> v(5, 10);
	vector<int>::iterator it = v.end();
	cout << *(--it) << endl;

	*it = 100; //it刚才已经 -- 了,已经表示最后一个数据的迭代器了
	vector<int>::const_iterator it2 = v.cend();
	cout << *(--it2) << endl;
	return 0;
}

运行结果:

*(--it):--it代表最后一个数据的迭代器,解引用之后拿到最后一个数据

 *it = 100:解引用之后再修改最后一个数据的值为100,这里不要 再 -- 了,前面已经--过了

注意:it2是const类型的迭代器,它指向的内容解引用之后不可以被修改

rbegin

获取最后一个位置的迭代器。

crbegin:const版本,通过const拿到的crbegin的迭代器,它指向的内容不可以被修改,和上面一样,下面就不再演示了。 

举个栗子:

int main()
{
	vector<int> v(5, 10);
	vector<int>::reverse_iterator it = v.rbegin();
	cout << *it << endl;

	*it = 100;
	vector<int>::const_reverse_iterator it2 = v.crbegin();
	cout << *it2 << endl;
	return 0;
}

运行结果:

我们发现rbegin/crbegin指向的是最后一个数据的迭代器。

反向迭代器:我们用迭代器遍历的时候,我们++,底层实际上是在--往后走,与正向迭代器用法一样,但是意思是反的。 

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	vector<int>::reverse_iterator it = v.rbegin();
	while (it != v.rend())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	return 0;
}

v.push_back(),待会会介绍,我们为了在这里更好演示效果就先使用这个接口~

我们暂时只用明白上面5个push_back后,vector里面存放的数据时:1 2 3 4 5

运行结果:

      

 我们发现使用反向迭代器之后数据是反着打印的。

上面我们使用了rend,可能有的码友没见过,我们在下面来介绍一下就明白刚才的代码了:

rend

获取第一个数据的前一个位置的迭代器。 

在刚才展示的例子中我们在这里画个图来解释一下:

                   

 vector 空间增长问题

 size

                               

 获取数据个数。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	cout << v.size() << endl;
	return 0;
}

在这里我们还是要借助push_back()接口,意思是往vector里面插入数据。(待会详细介绍~)

运行结果:

   

 我们可以看到通过v.size(),就拿到了vector里面数据的个数。

capacity

                            

 获取vector容量的大小。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	cout << v.capacity() << endl;
	return 0;
}

运行结果:

 empty

                           

 判断是否为空,为空就返回true;否则返回false。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	cout << v.empty() << endl;

	vector<int> v2;//无参的构造函数初始化时,v2里面是没有数据的,size为0
	cout << v2.empty() << endl;
	return 0;
}

运行结果:

 我们发现v不为空,则返回false(0); v2为空,则返回true(1)。

resize

            

 改变vector的数据个数(size)。

size_type n:要改变vector里面size的个数到n

const value_type& val:如果vector里面当前的size小于n,则往后用值val初始化到个数n。

 举个栗子:

int main()
{
	//使用默认的val
	vector<int> v;
	v.resize(5);

	//指定val
	//1、当 v2.size() < n时
	vector<int> v2;
	v2.resize(5, 10);

	//1、当 v2.size() > n时
	vector<int> v3(v2);
	v3.resize(2, 50);
	return 0;
}

运行结果:

 v.resize(5):size修改到5,使用默认的val,int对应的是0。

v2.resize(5,10):v2.size < 5,size刚开始为0,所以size往后追加到5,用10来初始化

v3.resize(2,50):刚开始v3.size() > 2,所以size直接变成2,并且不会用50来初始化 

reserve

                                

 扩容,改变vector的capacity(容量),实际上这个接口用的没有resize多。

举个栗子:

int main()
{
	vector<int> v;
	cout << v.capacity() << endl;
	v.reserve(20);
	cout << v.capacity() << endl;
	return 0;
}

运行结果:

            

 reserve不同环境在的增容规则

对于下面的一段代码,在VS和Linux环境下来验证它们的增容规则:

int main()
{
	size_t sz;
	std::vector<int> foo;
	sz = foo.capacity();
	std::cout << "making foo grow:\n";
	for (int i = 0; i < 100; ++i) {
		foo.push_back(i);
		if (sz != foo.capacity()) {
			sz = foo.capacity();
			std::cout << "capacity changed: " << sz << '\n';
		}
	}
}

VS:

 Linux:

                                 

 我们可以从上面的结果发现。

VS的capacity增长方式大概是按1.5倍方式增长。

Linux的capacity增长方式是按照2倍方式扩容的。

总结:

1、capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
2、reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。(reservr扩容直接到指定的capacity大小,不是至少扩大指定的大小)
3、resize在开空间的同时还会进行初始化,影响size。
 

vector增删查改

push_back

                       

 在vector尾部插入数据。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

    

 通过遍历push_back后的v,我们发现每次push_back都是在尾部插入数据。

pop_back

                     

 删除尾部的数据。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.pop_back();
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

     

        v刚开始尾插了1 2 3 ,再经过pop_back()之后,尾部数据3被删除了,这时候再遍历v,就只有数据1 2。

find

find: 算法库里面提供的(string里面的find是string自己的),不是vector成员的接口。在使用时要传迭代器区间。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	vector<int>::iterator it = find(v.begin(), v.end(), 3);
	cout << *it << endl;
	return 0;
}

运行结果:

vector<int>::iterator it = find(v.begin(), v.end(), 3): int是传给模板类型T;v.begin(),v.end()是传给迭代器区间InputIterator first,InputIterator last;3是要查找的内容vector<int>::iterator是返回类型。

insert

在指定的位置前插入数据val。(指定的位置都是传对应的迭代器)

       vector我们发现库里面是没有提供头插头删的,因为vector是连续的空间,头插头删效率太低。所以接口是不提供了。

但是如果想要头插,insert可以实现其功能(尽量少用)。 

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.insert(v.begin(), 10);
	vector<int>::iterator it = find(v.begin(), v.end(), 3);
	v.insert(it, 30);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

    

       我们在使用insert时,传的position必须是迭代器。(这和string不同,string可以传下标或迭代器)

erase

                

 删除position(迭代器)位置的值。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	//iterator erase(iterator position);
	v.erase(v.begin());
	vector<int>::iterator it = find(v.begin(), v.end(), 3);
	v.erase(it);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	//iterator erase (iterator first, iterator last);
	v.erase(v.begin(), v.end());
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

v.erase(v.begin()):v.begin()表示第一个数据的迭代器,删除 v.begin()

v.erase(it):删除it位置迭代器对应的数据

v.erase(v.begin(), v.end()):删除指定迭代器区间的数据

swap

                        

交换两个vector的数据空间。(底层实际上交换两个指针的指向)

举个栗子:

int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	vector<int> v2;
	v2.push_back(10);
	v2.push_back(20);
	v2.push_back(30);
	v2.push_back(40);
	v2.push_back(50);
	v1.swap(v2);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:

v1.swap(v2):我们可以看出来swap是成员函数。之后v1和v2两个vector里面的数据内容就交换成功了。 

operator[]

                      

 像数组一样访问。

vector常用的接口更多是插入和遍历。遍历更喜欢用数组operator[i]的形式访问,因为这样便捷。

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	//像数组一样访问数据
	cout << v[2] << endl;

	//修改数据(非const)
	int& tmp = v[3];
	tmp = 30;
	cout << v[3] << endl;
	return 0;
}

运行结果:

  

对于const版本就不能用引用修改其值了。 

举个栗子:

 迭代器失效问题

        迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)

因为扩容导致迭代器失效

会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等

我们在不断往vector里面插入数据时,可能存在扩容的情况。如果扩容方式是异地扩容,那么原来迭代器的位置指向原来的旧空间(异地扩容后就销毁了)。

我们举个栗子: 

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	vector<int>::iterator it = find(v.begin(), v.end(), 2);
	cout << "插入前:" << *it << endl;
	v.insert(it, 10);
	cout << "插入后:" << *it << endl;
	return 0;
}

运行结果:

我们发现插入后,再打印*it程序就崩溃了。我用的是vs2019版本的编译器,如果每次insert后再使用该迭代器就会报错,但是在其他编译器就不一定了。实际上v.insert(it,10)后,it指向的内容时10。

为什么会出现这种情况?我们来画图分析一下底层:

        我们可能会思考,如果pos传引用不就解决了这类迭代器失效的问题了,实际上并不是,就算传引用解决了这个问题,实际上在其他场景还会出现问题,所以这样做并不合适。 

解决方式

     在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。(返回值接收也行,及时更新it的位置)

因为删除数据导致迭代器失效

指定位置元素的删除操作--erase

举个栗子:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	vector<int>::iterator it = find(v.begin(), v.end(), 3);
	v.erase(it);
	cout << *it << endl;
	return 0;
}

运行一下:

我们发现程序崩溃了。 我用的是vs2019版本的编译器,如果每次erase后再使用该迭代器就会报错,但是在其他编译器就不一定了。实际上v.erase(it)后,it指向的内容时4。(指向要删除位置的迭代器的下一个迭代器)

调试窗口来看:

       erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。

解决方法

在使用前,对迭代器重新赋值即可


以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?

//1、
int main()
{
	vector<int> v = { 1, 2, 3, 4 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		++it;
	}
	return 0;
}

//2、
int main()
{
	vector<int> v{ 1, 2, 3, 4 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			++it;
		}
	}
	return 0;
}

答案是 2 中的代码是正确的。

因为每次erase之后it都会指向删除元素的下一个位置的迭代器。

对于这种情况:

    while (it != v.end())
    {
        if (*it % 2 == 0)
        {
            v.erase(it);
        }
        ++it;
    }

删除元素后it在删除后it已经往后推了一个迭代器,但是下面还有++it,说明刚才指向的位置还没有经过 if(*it%2 == 0)的判断就被++跳过去了。

由此可知,当erase后下面就不用++it了。如果没有erase就++it。

// 1 2 3 4 5 -->最后一个数是奇数&没有连续的偶数 正常

//1  2  4  5 -->有连续的偶数&最后一个数是奇数 没删除完偶数

//1  2  3  4  -->最后一个数是偶数崩溃

由于在VS2019下不好演示,我就在Linux下来展示这个现象: 

运行一下:

猜你喜欢

转载自blog.csdn.net/qq_58724706/article/details/123966811
今日推荐