Vector的总结
Vector底层是一个动态数组
默认构造的方式是0, 之后插入按照1 2 4 8 16 二倍扩容。注(GCC是二倍扩容,VS13是1.5倍扩容。
原因可以考虑内存碎片和伙伴系统,内存的浪费)。《 扩容后是一片新的内存,需要把旧内存空间中的所有元素都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行插入构造新元素,并且同时释放旧内存空间,并且,由于vector 空间的重新配置,导致旧vector的所有迭代器都失效了。》
扩容原理概述
新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
结论:可以根据输出看到,vector是以2倍的方式扩容的。这里让我产生了两个疑问:
1.为什么要成倍的扩容而不是一次增加一个固定大小的容量呢?
2.为什么是以两倍的方式扩容而不是三倍四倍,或者其他方式呢?
1.为什么要成倍的扩容而不是一次增加一个固定大小的容量呢?
首先第一个问题:
1.vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
2.为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用。
2.为什么是以两倍的方式扩容而不是三倍四倍,或者其他方式呢?
第二个问题:
1.根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2二倍的方式扩容,或者以1.5倍的方式扩容。
2.以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间:
总结
1.vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
2.为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用,因为更好。
Vector的接口使用
一.扩容
Size()——返回当前的vector的元素个数。
Capacity()—返回当前vector当前能够返回的个数,就是扩容之后的大小。
扩容有两种方式:
第一种:当增添元素的时候,进行扩容;
第二种:reserve/resize——两者都是将容器扩大到恰好达到指定的元素
reserve(int new_size):
a.将容器扩大到能容纳new_size的大小。
b.扩大的只是容器的预留空间,空间内不正真创建元素对象。
c.改变capacity()的返回值,不改变size()的返回值。
resize(int new_size,/int init_value/):
a.第一个参数是指将要扩大到能容纳多少元素的大小;第二个参数是对扩大的空间进行初始化的值,如果不写,默认调用容器中存的元素的默认构造函数。
b.将容器扩大到能容纳new_size的大小。
c.改变容器的大小,并且创建对象。
d.改变capacity()的返回值,改变size()的返回值。
从这里我认为vector的初始的扩容方式代价太大,初始扩容效率低, 需要频繁增长,不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用resize()和reserve()方法提高效率减少内存碎片的,需要
resize()
1、resize方法被用来改变vector中元素的数量,我们可以说,resize方法改变了容器的大小,且创建了容器中的对象;
2、如果resize中所指定的n小于vector中当前的元素数量,则会删除vector中多于n的元素,使vector得大小变为n;
3、如果所指定的n大于vector中当前的元素数量,则会在vector当前的尾部插入适量的元素,使得vector的大小变为n,在这里,如果为resize方法指定了第二个参数,则会把后插入的元素值初始化为该指定值,如果没有为resize指定第二个参数,则会把新插入的元素初始化为默认的初始值;
4、如果resize所指定的n不仅大于vector中当前的元素数量,还大于vector当前的capacity容量值时,则会自动为vector重新分配存储空间;
reserve():
1、reserve方法被用来重新分配vector的容量大小;
2、只有当所申请的容量大于vector的当前容量时才会重新为vector分配存储空间;小于当前容量则没有影响
3、reserve方法对于vector元素大小没有任何影响,不创建对象。
vector中数据的随机存取效率很高,O(1)的时间的复杂度,但是在vector 中随机插入元素,需要移动的元素数量较多,效率比较低。
二,容器的回收
int main()
{
vector<int> v;
for (int i = 0; i<30; i++){
v.push_back(i);
cout << "size=" << v.size() << endl;
cout << "capacity=" << v.capacity() << endl;
}cout << "---------------------------------------------" << endl;
//释放空间
v.clear();
cout << "size=" << v.size() << endl;//0
cout << "capacity=" << v.capacity() << endl;//42
v.erase(v.begin(), v.end()); // 第一步:先产生一个和原先一样的临时对象
cout << "size=" << v.size() << endl;//0 //第二步:临时量调用swap()函数两者进行交换。
cout << "capacity=" << v.capacity() << endl;//42 //第三步:语句结束,临时量自动析构。
vector<int>().swap(v); //这里省略了一部分,可以直接省略括号里面的参数,下面解说。
//vector<int>(vec).swap(vec); ===> vec.swap(vector<int>())
cout << "size=" << v.size() << endl;//0
cout << "capacity=" << v.capacity() << endl;//0
return 0;
}
//代码实现——根据上面的解释和这里实现得出下面的总结
size=30
capacity=42
size=0
capacity=42
size=0
capacity=42
size=0
capacity=0
总结:
使用clear()和erase()两个函数只是清空元素,但不回收内存。
先使用clear()再使用swap(),释放空间并且回收内存。
Erase(); // 清除元素【可以在如果删除中间的元素的时候,可以使用这个接口】
也可以使用Erase(v.begin(),v.end()) //删除全部的链表。
Clear(); //清除整个元素【如果是清空整个元素列表,就可以使用这个借口】
迭代器使用
一般的迭代器
Vector<int>::iterator it = v.begin()
While(it != v.end())
{
Cout << *it <<” ”;
++it;
}
删除迭代器
for(it=iVec.begin();it!=iVec.end();)
{
if(*it % 3 ==0)
it=iVec.erase(it); //删除元素,返回值指向已删除元素的下一个位置
else
++it; //指向下一个位置
}
关联式容器:map和set
序列式容器: Lisr和Vector的区别 / deque
//查看这个区别,可以看博客或者看课件的vector和list打卡,看前序,讲的非常好
**vector和数组类似,**它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取(使用[]操作符访问其中元素),但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝(复杂度是O(n)),另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问,这个特点使得它的随机存取变的非常没有效率,需要遍历中间的元素,搜索复杂度O(n),因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。
deque是一个double-ended queue,它的具体实现不太清楚,但知道它具有以下两个特点:
它支持[]操作符,也就是支持随即存取,并且和vector的效率相差无几,它支持在两端的操作:push_back,push_front,pop_back,pop_front等,并且在两端操作上与list的效率也差不多。
由于vector拥有一段连续的内存空间,能非常好的支持随机存取,因此vector::iterator支持“+”、“+=”、“<”等操作符。
而list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等操作符运算
因此在实际使用时,如何选择这三个容器中哪一个,应根据你的需要而定,一般应遵循下面的原则:
1、vector拥有一段连续的内存空间,如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
2、list拥有一段不连续的内存空间,因此支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。
3、如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。
迭代器失效问题:insert/erase,还要分为序列式容器和关联式容器
一,序列式容器的迭代器失效
1.insert会导致迭代器失效,是因为insert可能会导致增容,增容后pos还指向原来的空间,而原来的空间已经释放了。
2.Erase中,vector,删除当前的iterator会使后面所有元素的iterator都失效。这是因为顺序容器内存是连续分配(分配一个数组作为内存),删除一个元素导致后面所有的元素会向前移动一个位置
list的迭代器失效
前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
二,关联式迭代器失效
对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
总结:迭代器失效分三种情况考虑,也是非三种数据结构考虑,分别为数组型,链表型,树型数据结构。
数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);
for( iter = c.begin(); iter != c.end(); ) iter = c.erase(iter);
链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).
for( iter = c.begin(); iter != c.end(); ) c.erase(iter++);
树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
*注意:经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,ite