✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/fYaBd
📚专栏简介:在这个专栏中,我将会分享 C++ 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
4. STL 中 vector 删除其中的元素,迭代器如何变化?为什么是两倍扩容?释放空间 ?
增加元素:
size() 函数返回的是已用空间大小,capacity() 返回的是总空间大小,capacity() - size() 则是剩余的可用空间大小。当 size() 和 capacity() 相等,说明 vector 目前的空间已被用完,如果再添加新元素,则会引起 vector 空间的动态增长。
由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用 reserve(n) 预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当 n > capacity() 时,调用 reserve(n) 才会改变 vector 容量。
resize() 成员函数改变元素的数目,至于空间的的变化需要看具体情况去分析,如下:
void resize(size_type __new_size, const _Tp& __x) {
if (__new_size < size())
erase(begin() + __new_size, end());
else
insert(end(), __new_size - size(), __x);
}
1、空的 vector 对象,size() 和 capacity() 都为 0。
2、当空间大小不足时,新分配的空间大小为原空间大小的 2 倍。
3、使用 reserve() 预先分配一块内存后,在空间未满的情况下,不会引起重新分配,从而提升了效率。
4、当 reserve() 分配的空间比原空间小时,是不会引起重新分配的。
5、resize() 函数只改变容器的元素数目,未改变容器大小。
6、用 reserve(size_type) 只是扩大 capacity 值,这些内存空间可能还是 “野” 的,如果此时使用 “[ ]” 来访问,则可能会越界。而 resize(size_type new_size) 会真正使容器具有 new_size 个对象。
不同的编译器,vector 有不同的扩容大小。在 vs 下是 1.5 倍,在 GCC 下是 2 倍;
-
Windows 扩容底层(1.5倍)
Windows 中堆管理系统会对释放的堆块进行合并,当堆管理器发现 2 个空闲块彼此相邻的时候,就会对堆块进行合并。因此,vs 下的 vector 扩容机制选择使用 1.5 倍的方式扩容,这样多次扩容之后,就可以使用之前已经释放的空间。
-
Linux的扩容底层(2倍)
Linux 下主要使用 glibc 的 ptmalloc 来进行用户空间申请的,如果 malloc 的空间小于 128KB,其内部通过 brk() 来扩张,如果大于 128KB 时,通过 mmap 将内存映射到进程地址空间。
总的来说:
-
扩容倍数为 2 时,时间上占优势,扩容倍数为 1.5 时,空间上占优势。
-
vector 在 push_back 以成倍增长可以在均摊后达到 O(1) 的事件复杂度,相对于增长指定大小的 O(n) 时间复杂度更好。
-
为了防止申请内存的浪费,现在使用较多的有 2 倍与 1.5 倍的增长方式,而 1.5 倍的增长方式可以更好的实现对内存的重复利用。
使用 k = 2 增长因子的问题在于,每次扩展的新尺寸必然刚好大于之前分配的总和,也就是说,之前分配的内存空间不可能被使用。这样对内存不友好,最好把增长因子设为 (1, 2),也就是 1 ~ 2 之间的某个数值。
假如说我们是以 2 倍方式扩容(1,2,4,8,16),则第 i 次扩容期间所需要的空间总量就是 2^i 次方,如果第 4 次扩容时总共需要 8 个元素大小的空间,但是前3次已经释放的空间加起来的总量,刚好是 7,而 7 小于 8,不足以我们第 4 次扩容时所需要的空间,也就是说,如果恰巧以 2 倍方式扩容,那么每次扩容时前面释放的空间它都不足以支持本次的扩容。
当 k = 1.5 时,在几次扩展以后,可以重用之前的内存空间了。
对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到 O(n) 的时间复杂度,因此,使用成倍的方式扩容。
时间复杂度:在最坏情况下,插入操作的时间复杂度是 O(n),其中 n 表示当前 std::vector
中的元素数量。这是因为在插入元素时,如果需要移动已有元素,需要将后续的元素逐个后移。如果插入操作位于末尾,它的时间复杂度可以视为 O(1)。
需要注意的是,std::vector
的动态扩容会导致元素的重新分配和复制,这可能在频繁插入大量元素时引起性能问题。为了避免频繁扩容,可以在初始化 std::vector
时估算好元素数量(使用 reserve
函数预留一定容量),以减少扩容的次数。
删除元素:
由于 vector 的内存占用空间只增不减,比如你首先分配了 10,000 个字节,然后 erase 掉后面 9,999 个,留下一个有效元素,但是内存占用仍为 10,000 个。所有内存空间是在 vector 析构时候才能被系统回收。empty() 用来检测容器是否为空的,clear() 可以清空所有元素。但是即使 clear(),vector 所占用的内存空间依然如故,无法保证内存的回收。
如果需要空间动态缩小,可以考虑使用 deque。
如果使用 vector,可以用 swap() 来帮助你释放多余内存或者清空全部内存。
vector(Vec).swap(Vec); //将Vec中多余内存清除;
vector().swap(Vec); //清空Vec的全部内存;
实例:
#include <iostream>
#include <vector>
using namespace std;
int main ()
{
vector<int> vec (100,100); // three ints with a value of 100
vec.push_back(1);
vec.push_back(2);
cout <<"vec.size(): " << vec.size() << endl;
cout <<"vec.capasity(): " << vec.capacity() << endl;
vector<int>(vec).swap(vec); //清空vec中多余的空间,相当于vec.shrink_to_fit();
cout <<"vec.size(): " << vec.size() << endl;
cout <<"vec.capasity(): " << vec.capacity() << endl;
vector<int>().swap(vec); //清空vec的全部空间
cout <<"vec.size(): " << vec.size() << endl;
cout <<"vec.capasity(): " << vec.capacity() << endl;
return 0;
}
/*
运行结果:
vec.size(): 102
vec.capasity(): 200
vec.size(): 102
vec.capasity(): 102
vec.size(): 0
vec.capasity(): 0
*/
5. vector 动态扩展时,编译器为什么不先判断一下原有空间后面的 内存是否空闲,如果空闲,直接在后面的内存空间继续分配空间?
编译器在处理 vector 动态扩展时,并不会先行判断原有空间后面的内存是否空闲,而是在需要的时候重新申请更大的一块内存空间,然后将原空间数据拷贝到新空间,释放旧空间数据,最后在新的空间中插入新的元素。这种做法的背后,涉及到内存池的设计目标与 vector 的扩展策略的冲突。
实际上,内存池的设计是为了更有效地管理和利用内存资源,它预先在内存中分配一定数量的内存块,当程序需要分配内存时,首先会去内存池中查找是否有合适的空闲内存块可以使用。如果有,就从中分配内存并返回;如果没有,再向操作系统申请新的内存。这种方式可以显著提高内存分配和释放的效率。
6. ST L 中 slist 的实现
list 是双向链表,而 slist(single linked list)是单向链表,它们的主要区别在于:前者的迭代器是双向的 Bidirectional iterator,后者的迭代器属于单向的 Forward iterator。虽然 slist 的很多功能不如 list 灵活,但是其所耗用的空间更小,操作更快。
根据 STL 的习惯,插入操作会将新元素插入到指定位置之前,而非之后,然而 slist 是不能回头的,只能往后走,因此在 slist 的其他位置插入或者移除元素是十分不明智的,但是在 slist 开头却是可取的,slist 特别提供了 insert_after() 和 erase_after() 供灵活应用。考虑到效率问题,slist 只提供 push_front() 操作,元素插入到 slist 后,存储的次序和输入的次序是相反的。
slist 的单向迭代器如下图所示:
slist 默认采用 alloc 空间配置器配置节点的空间,其数据结构主要代码如下:
template <class T, class Allco = alloc>
class slist
{
...
private:
...
static list_node* create_node(const value_type& x){}//配置空间、构造元素
static void destroy_node(list_node* node){}//析构函数、释放空间
private:
list_node_base head; //头部
public:
iterator begin(){}
iterator end(){}
size_type size(){}
bool empty(){}
void swap(slist& L){}//交换两个slist,只需要换head即可
reference front(){} //取头部元素
void push_front(const value& x){}//头部插入元素
void pop_front(){}//从头部取走元素
...
}
举个例子:
#include <forward_list>
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
forward_list<int> fl;
fl.push_front(1);
fl.push_front(3);
fl.push_front(2);
fl.push_front(6);
fl.push_front(5);
forward_list<int>::iterator ite1 = fl.begin();
forward_list<int>::iterator ite2 = fl.end();
for(;ite1 != ite2; ++ite1)
{
cout << *ite1 <<" "; // 5 6 2 3 1
}
cout << endl;
ite1 = find(fl.begin(), fl.end(), 2); //寻找2的位置
if (ite1 != ite2)
fl.insert_after(ite1, 99);
for (auto it : fl)
{
cout << it << " "; //5 6 2 99 3 1
}
cout << endl;
ite1 = find(fl.begin(), fl.end(), 6); //寻找6的位置
if (ite1 != ite2)
fl.erase_after(ite1);
for (auto it : fl)
{
cout << it << " "; //5 6 99 3 1
}
cout << endl;
return 0;
}
需要注意的是 C++ 标准委员会没有采用 slist 的名称,forward_list 在 C++11 中出现,它与 slist 的区别是没有 size() 方法。