推荐一个学习网站:https://github.com/0voice
1.请解释vector容器和它的特点
在C++中,vector
是标准模板库(STL)的一部分,它是一个动态数组。与普通数组相比,它的大小可以在运行时动态改变。下面是vector
的一些主要特点和应用场景:
- 动态大小:与传统的数组不同,
vector
可以根据需要动态地扩展或缩减大小。这意味着你不需要事先知道数据的数量。#include <iostream> #include <vector> int main() { std::vector<int> vec; // 初始化一个空的 vector // 添加元素(动态扩展大小) vec.push_back(1); vec.push_back(2); vec.push_back(3); // 打印 vector 中的元素和大小 std::cout << "Elements: "; for (int i = 0; i < vec.size(); ++i) { std::cout << vec[i] << " "; } std::cout << std::endl; std::cout << "Size: " << vec.size() << std::endl; // 打印当前大小 std::cout << "Capacity: " << vec.capacity() << std::endl; // 打印当前容量 // 删除最后一个元素(动态缩减大小) vec.pop_back(); std::cout << "Size after pop_back: " << vec.size() << std::endl; return 0; }
-
随机访问:就像数组一样,
vector
支持随机访问,这意味着你可以通过索引直接访问任何元素,访问时间是常数时间复杂度(O(1))。#include <iostream> #include <vector> int main() { std::vector<int> vec = {10, 20, 30, 40, 50}; // 通过索引访问元素 std::cout << "Element at index 2: " << vec[2] << std::endl; // 快速访问 std::cout << "Element at index 4: " << vec.at(4) << std::endl; // 带越界检查的访问 return 0; }
-
内存管理:
vector
在内部管理其存储的内存。当元素被添加到vector
中,并且当前分配的内存不足以容纳它们时,它会自动重新分配更多的内存。#include <iostream> #include <vector> int main() { std::vector<int> vec; vec.reserve(10); // 提前分配内存,容量为10 for (int i = 0; i < 10; ++i) { vec.push_back(i * 10); // 添加元素 } // 打印容量变化 std::cout << "Size: " << vec.size() << std::endl; std::cout << "Capacity: " << vec.capacity() << std::endl; return 0; }
-
灵活性:你可以在
vector
的末尾添加或删除元素,而且效率很高。但在中间或开始位置插入或删除元素可能会比较慢,因为这可能需要移动现有的元素。#include <iostream> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; // 在末尾添加元素 vec.push_back(6); // 在第二个位置插入一个新元素 vec.insert(vec.begin() + 1, 10); // 删除第三个位置的元素 vec.erase(vec.begin() + 2); // 打印结果 std::cout << "Elements after insert and erase: "; for (int val : vec) { std::cout << val << " "; } std::cout << std::endl; return 0; }
应用场景
-
动态数据集合:当你需要一个可以根据数据量动态调整大小的数组时,
vector
是一个很好的选择。例如,处理用户输入的数据集,其中输入数量事先未知。 -
需要快速访问的数据:由于
vector
支持随机访问,它非常适合于需要频繁读取元素的情况,比如查找或排序算法中。 -
性能敏感的应用:由于其元素紧密排列在连续的内存块中,
vector
通常提供高效的内存访问性能,适合用于性能敏感的应用。
总之,vector
是一个非常灵活且强大的容器,适合用于多种不同的编程场景。在实际应用中,选择正确的数据结构往往是优化程序性能的关键。
2.vector如何保证元素的连续存储?
std::vector
能够保证其元素在内存中是连续存储的,这是 vector
的一个关键特性,也是它与链表等数据结构的根本区别。元素的连续存储意味着每个元素在内存中紧挨着存放,这使得 vector
能够像数组一样支持高效的随机访问。为了实现这一点,vector
的内存管理机制非常关键。
内存分配机制
动态数组的工作原理:
std::vector
在内部是基于动态数组实现的。在创建一个vector
时,容器会为一定数量的元素预先分配一块连续的内存。当向vector
中添加元素时,如果当前分配的内存容量足够,那么元素会被放置在分配的连续内存块中。- 如果内存不足以容纳新元素时,
vector
会重新分配一块更大的内存空间(通常是当前容量的 2 倍),并将现有的元素从旧内存区域复制到新内存区域。然后,释放旧的内存块。#include <iostream> #include <vector> int main() { std::vector<int> vec; vec.reserve(4); // 提前分配 4 个元素的空间,保证内存连续性 // 添加元素 for (int i = 0; i < 4; ++i) { vec.push_back(i); std::cout << "Added: " << i << ", size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl; } // 添加第 5 个元素,触发内存重新分配 vec.push_back(4); std::cout << "After adding one more element:" << std::endl; std::cout << "Size: " << vec.size() << std::endl; std::cout << "Capacity: " << vec.capacity() << std::endl; return 0; }
解释:
- 在上面的代码中,我们通过
reserve(4)
为vector
提前分配了存储 4 个元素的空间,这保证了添加前 4 个元素时内存是连续的。 - 当第 5 个元素被添加时,由于当前容量不足,
vector
会重新分配一块更大的内存,并将现有的 4 个元素复制到新内存中。这保证了内存连续性,但也引入了内存重新分配和数据拷贝的开销。
容量的扩展策略
std::vector
的扩展策略是每次需要扩容时,通常会将当前容量扩展到原来的 2 倍(具体倍数依实现不同可能有所不同)。这种策略有助于避免频繁的内存重新分配,同时确保每次重新分配的空间仍然是连续的。
代码示例:扩展策略
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
// 连续添加元素,观察容量的变化
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "Added: " << i << ", size: " << vec.size()
<< ", capacity: " << vec.capacity() << std::endl;
}
return 0;
}
解释:
- 随着元素不断添加,
vector
的容量(capacity()
)会随着需要动态增加。在这段代码中,你会观察到每次超出容量时,capacity()
发生跳跃式增长。 - 例如,当容量从 4 扩展到 8 时,
vector
会重新分配足够大的内存空间,以保证元素的连续存储。
如何保证元素的地址连续性
std::vector
保证每个元素都存储在一个连续的内存块中,这意味着 vector
中的元素地址是连续的。这使得 vector
与数组类似,可以通过指针进行遍历,并支持常数时间的随机访问。
验证地址连续性
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
// 打印元素的地址
for (size_t i = 0; i < vec.size(); ++i) {
std::cout << "Address of vec[" << i << "]: "
<< &vec[i] << std::endl;
}
return 0;
}
解释:
- 在这段代码中,我们打印了每个
vector
元素的地址。由于vector
保证了连续存储,所以每个元素的地址会按照固定的步幅增加。 - 可以看到,第
i
个元素的地址和第i+1
个元素的地址是相邻的,步幅等于每个元素的大小(对int
类型来说,步幅为sizeof(int)
)。
拷贝与移动:内存重新分配的开销
当 vector
需要重新分配内存时,现有的所有元素都会被拷贝到新的内存块。这意味着在元素数量较多的情况下,频繁的扩展会带来性能开销。因此,如果你知道即将向 vector
中添加大量元素,可以提前使用 reserve()
方法一次性分配足够的空间,减少内存重分配和元素拷贝的次数。
预分配内存避免扩展开销
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
vec.reserve(100); // 预先分配足够的空间,避免频繁扩展
// 添加 100 个元素
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
std::cout << "Final size: " << vec.size() << std::endl;
std::cout << "Final capacity: " << vec.capacity() << std::endl;
return 0;
}
解释:
- 使用
reserve(100)
提前分配了足够的内存空间,可以避免在添加元素时多次扩展和重新分配内存,提升性能。
总结:
std::vector
通过动态数组实现元素的连续存储,当容量不足时,会自动扩展内存,并将现有元素拷贝到新的内存块。- 为了减少内存重新分配和数据拷贝的开销,可以通过
reserve()
预分配足够的内存空间,尤其在你预知需要存储大量数据时。 - 由于
vector
元素连续存储,支持常数时间的随机访问,并且可以通过指针或下标直接遍历,这使得它在需要频繁访问和遍历的场景中非常高效。
3.当vector空间不足时,如何扩容?
当 std::vector
的容量不足以容纳新元素时,它会自动进行扩容。这个扩容过程涉及到重新分配内存、复制现有元素到新内存块,并释放旧内存块。这个过程是由 vector
的实现自动管理的,作为开发者,你不需要手动干预扩容,但理解其背后的机制可以帮助优化性能。
扩容机制概述
- 当
vector
添加新元素时,如果当前容量(capacity()
)不足,vector
会申请一块新的、更大的内存空间,通常是当前容量的两倍(不同编译器和实现可能有所差异)。 - 然后,现有元素会被复制到新内存块中,旧的内存块会被释放。
- 新增元素被放入新内存块的相应位置中。
- 由于内存重新分配涉及到元素拷贝,因此扩容是一个相对昂贵的操作,尤其当
vector
存储大量数据时。
扩容示例
通过以下代码可以观察 vector
的容量扩展过程:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
// 连续添加元素,观察 vector 的扩容情况
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "Added element: " << i
<< ", Size: " << vec.size()
<< ", Capacity: " << vec.capacity() << std::endl;
}
return 0;
}
输出示例:
Added element: 0, Size: 1, Capacity: 1
Added element: 1, Size: 2, Capacity: 2
Added element: 2, Size: 3, Capacity: 4
Added element: 3, Size: 4, Capacity: 4
Added element: 4, Size: 5, Capacity: 8
Added element: 5, Size: 6, Capacity: 8
Added element: 6, Size: 7, Capacity: 8
Added element: 7, Size: 8, Capacity: 8
Added element: 8, Size: 9, Capacity: 16
Added element: 9, Size: 10, Capacity: 16
解释:
- 初始时,
vector
的容量从 1 开始。当需要添加第二个元素时,容量扩展到 2。 - 当需要添加第 3 个元素时,
vector
扩容为 4,接着在第 5 个元素加入时扩展到 8。 - 之后,当第 9 个元素被添加时,
vector
扩展到 16。可以看到,容量基本上是以倍增的方式扩展。
扩容细节:常见的扩容策略
倍增策略:
- 大多数
std::vector
实现采用倍增策略来扩展容量。通常是两倍扩展,即每次重新分配的内存是当前容量的两倍。 - 这种策略是为了避免频繁的扩容操作。频繁的扩容会导致大量的内存重新分配和元素拷贝,导致性能瓶颈。
不同实现的细节:
- 不同的 C++ 标准库实现可能采用略有不同的扩展策略。虽然倍增策略是最常见的,但某些库可能会根据情况采取不同的增长方式(如 1.5 倍扩展等)。
性能影响
扩容时的性能开销:
- 重新分配内存:扩容时,
vector
会为所有现有元素分配一块新的、更大的连续内存块。这个过程是需要时间的。 - 元素拷贝:扩容后,所有现有元素必须从旧内存块复制到新内存块。对于大对象或有复杂拷贝构造函数的对象,这一步可能非常耗时。
- 释放旧内存:旧的内存块在元素复制完成后会被释放。
因此,当你需要频繁向 vector
添加大量元素时,提前分配足够的内存可以显著减少扩容带来的开销。
如何优化扩容性能
reserve() 提前分配内存
为了避免 vector
频繁扩容,可以通过 reserve()
提前分配足够的内存。这样,vector
在添加元素时就不必频繁重新分配和复制内存,从而提高性能。
使用 reserve()
优化扩容
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
vec.reserve(10); // 提前分配足够的空间以容纳 10 个元素
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "Added element: " << i
<< ", Size: " << vec.size()
<< ", Capacity: " << vec.capacity() << std::endl;
}
return 0;
}
输出示例:
Added element: 0, Size: 1, Capacity: 10
Added element: 1, Size: 2, Capacity: 10
Added element: 2, Size: 3, Capacity: 10
Added element: 3, Size: 4, Capacity: 10
Added element: 4, Size: 5, Capacity: 10
Added element: 5, Size: 6, Capacity: 10
Added element: 6, Size: 7, Capacity: 10
Added element: 7, Size: 8, Capacity: 10
Added element: 8, Size: 9, Capacity: 10
Added element: 9, Size: 10, Capacity: 10
解释:
- 通过
reserve(10)
,我们一次性为vector
分配了足够的空间来存储 10 个元素,避免了扩容的发生。此时,无论添加多少元素,只要在容量范围内,vector
的容量保持不变,不会重新分配内存。
扩容与移动语义
在 C++11 及其之后的标准中,引入了移动语义,这对 vector
的扩容过程的性能提升有显著作用。当 vector
扩容时,如果元素支持移动操作(即支持移动构造函数),元素可以通过移动语义而不是拷贝语义进行传递,从而避免了不必要的拷贝。
移动语义在扩容中的应用
#include <iostream>
#include <vector>
class LargeObject {
public:
LargeObject() {
std::cout << "Constructed\n";
}
LargeObject(const LargeObject&) {
std::cout << "Copy constructed\n";
}
LargeObject(LargeObject&&) noexcept {
std::cout << "Move constructed\n";
}
};
int main() {
std::vector<LargeObject> vec;
vec.reserve(5);
for (int i = 0; i < 5; ++i) {
vec.push_back(LargeObject()); // 利用移动构造避免拷贝
}
return 0;
}
输出示例:
Constructed
Move constructed
Constructed
Move constructed
Constructed
Move constructed
Constructed
Move constructed
Constructed
Move constructed
解释:
- 当
LargeObject
支持移动构造时,在vector
扩容时,元素会通过移动语义传递,避免了昂贵的拷贝操作,显著提升性能。
总结
- 扩容机制:当
vector
容量不足时,它会自动重新分配更大的连续内存空间,通常是当前容量的两倍,并将现有元素复制到新空间。 - 性能影响:扩容涉及内存重新分配和元素拷贝,可能会带来性能开销,尤其是在存储大量数据时。
- 优化扩容:可以使用
reserve()
提前分配足够的内存,减少扩容操作的频率。此外,利用 C++11 的移动语义,可以进一步优化扩容时的性能。
4.vector的push_back和emplace_back有什么区别?
std::vector
的 push_back()
和 emplace_back()
都是用于向 vector
的末尾添加元素的成员函数,但它们在执行添加元素的方式上有所不同,尤其在对象的构造、拷贝和移动操作方面有显著差异。
push_back()
push_back()
用于将一个现有的对象或值拷贝(或移动)到 vector
的末尾。也就是说,它要求你已经有一个对象,而不是直接在 vector
中构造新对象。
工作流程:
- 如果传入的是临时对象或右值,C++ 会优先使用移动构造函数来将对象添加到
vector
中。 - 如果传入的是左值,则会使用拷贝构造函数将对象复制到
vector
的末尾。
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(int value) : value_(value) {
std::cout << "Constructed with value: " << value_ << std::endl;
}
MyClass(const MyClass& other) {
value_ = other.value_;
std::cout << "Copy constructed" << std::endl;
}
MyClass(MyClass&& other) noexcept {
value_ = other.value_;
std::cout << "Move constructed" << std::endl;
}
private:
int value_;
};
int main() {
std::vector<MyClass> vec;
MyClass obj(10); // 构造一个对象
vec.push_back(obj); // 通过拷贝构造将对象添加到 vector
vec.push_back(MyClass(20)); // 通过移动构造将临时对象添加到 vector
return 0;
}
输出:
Constructed with value: 10
Copy constructed
Constructed with value: 20
Move constructed
解释:
- 第一次调用
push_back(obj)
,由于传递的是一个已有对象(左值),vector
使用拷贝构造函数将obj
复制到容器中。 - 第二次调用
push_back(MyClass(20))
,传递的是临时对象(右值),使用了移动构造函数来避免拷贝操作。
emplace_back()
emplace_back()
直接在 vector
的末尾原地构造对象,而不是像 push_back()
那样需要构造一个临时对象然后再拷贝或移动它。它接收构造对象所需的参数,并直接调用该对象的构造函数。
工作流程:
emplace_back()
接收对象构造函数的参数,直接在vector
的内存位置上构造对象,而不是先创建一个临时对象再拷贝或移动它。- 它避免了临时对象的创建和拷贝,因此在某些情况下比
push_back()
更高效。
emplace_back()
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(int value) : value_(value) {
std::cout << "Constructed with value: " << value_ << std::endl;
}
private:
int value_;
};
int main() {
std::vector<MyClass> vec;
vec.emplace_back(30); // 直接在 vector 中构造对象,无需创建临时对象
return 0;
}
输出:
Constructed with value: 30
解释:
emplace_back(30)
直接使用参数30
在vector
的末尾调用MyClass
的构造函数构造对象。这避免了临时对象的创建和移动/拷贝操作。
区别总结
1. 操作方式:
push_back()
:需要一个已构造的对象或值(现有对象),然后将其拷贝或移动到vector
的末尾。emplace_back()
:直接在vector
内存中构造对象,传递构造函数所需的参数,而不是拷贝或移动现有对象。
2. 性能差异:
push_back()
可能涉及到临时对象的构造和随后的拷贝/移动操作,特别是在使用自定义类型的情况下,这可能带来性能开销。emplace_back()
可以避免不必要的临时对象构造和拷贝/移动操作,因此在某些情况下更高效,特别是当对象构造较为复杂或拷贝代价较高时。
3. 使用场景:
- 如果你已经有一个现成的对象,或者只是添加基本数据类型,使用
push_back()
很合适。 - 如果你需要直接构造对象,且不想多余的临时对象开销,
emplace_back()
是更高效的选择。
实例分析:push_back()
vs emplace_back()
为了更直观地展示两者的差异,以下是一个完整的对比示例:
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(int value) : value_(value) {
std::cout << "Constructed with value: " << value_ << std::endl;
}
MyClass(const MyClass& other) {
value_ = other.value_;
std::cout << "Copy constructed" << std::endl;
}
MyClass(MyClass&& other) noexcept {
value_ = other.value_;
std::cout << "Move constructed" << std::endl;
}
private:
int value_;
};
int main() {
std::vector<MyClass> vec;
// 使用 push_back,需要先构造临时对象再拷贝或移动
vec.push_back(MyClass(10)); // Move constructed
// 使用 emplace_back,直接在 vector 中构造对象
vec.emplace_back(20); // Constructed with value: 20
return 0;
}
输出:
Constructed with value: 10
Move constructed
Constructed with value: 20
解释:
- 对于
push_back()
,MyClass(10)
先创建了一个临时对象,然后通过移动构造函数将其移动到vector
中。 - 对于
emplace_back()
,则直接在vector
中调用MyClass
的构造函数,无需临时对象的创建和移动操作。
使用建议
- 当对象的构造、拷贝或移动操作代价较高时,优先考虑使用
emplace_back()
,这样可以避免不必要的临时对象开销。 - 如果你已经有了现成的对象,直接使用
push_back()
会更加直观。
5.使用Vector,需要注意哪些问题
在使用 std::vector
时,虽然它是一个功能强大且灵活的容器,但在特定情况下需要注意一些常见问题,以避免性能损失或错误行为。以下是使用 vector
需要注意的几个关键点:
容量 vs 大小 (capacity()
vs size()
)
-
size()
表示vector
中当前元素的个数,而capacity()
表示vector
分配的内存可以容纳的元素数量。当向vector
中添加新元素时,如果size()
超过了capacity()
,vector
会触发扩容(重新分配更大的内存块并复制现有元素)。 -
注意事项:频繁的扩容会导致性能开销(内存重新分配和元素拷贝)。如果你预先知道数据的大致数量,应该使用
reserve()
来避免频繁的扩容操作。
迭代器失效问题
-
什么是迭代器失效:当
vector
扩容、删除元素或插入元素时,可能会导致原来的迭代器、指针或引用指向无效的内存地址,从而出现迭代器失效问题。 -
扩容导致失效:当
vector
发生扩容时,所有指向vector
元素的迭代器都会失效,因为底层内存位置发生了变化。 -
删除元素导致失效:当删除
vector
中的某个元素时,指向被删除元素及其后续元素的迭代器将失效,因为这些元素的索引发生了变化。 - 解决办法:当你使用迭代器并且有可能触发扩容或删除元素时,应该重新获取迭代器或使用下标访问来避免失效。
大数据量操作的性能
-
虽然
vector
是非常高效的容器,但在处理非常大数据量的情况下,某些操作(如插入、删除)可能会带来显著的性能问题。 -
插入和删除元素的复杂度:
push_back()
是在vector
末尾添加元素,其时间复杂度是常数(O(1)),但在中间插入或删除元素时,可能需要移动后续的所有元素,导致线性时间复杂度(O(n))。 -
解决办法:如果你需要频繁在
vector
中间插入或删除元素,可以考虑使用其他容器,如std::list
或std::deque
,它们在插入和删除元素时效率更高。
元素的构造、拷贝与移动
-
拷贝与移动:当
vector
发生扩容时,所有现有的元素都需要被拷贝或移动到新的内存位置。如果vector
中的元素是复杂对象,这个过程会涉及到拷贝构造或移动构造操作,可能会带来性能开销。 -
emplace_back()
与push_back()
:push_back()
需要传递一个对象或临时对象,而emplace_back()
可以直接在容器中构造对象,避免了不必要的拷贝或移动操作。对于复杂对象,emplace_back()
通常比push_back()
更高效。
避免多余的动态内存分配
-
vector
底层依赖于动态内存分配。当频繁增加或删除元素时,如果不注意内存管理,可能会导致性能下降。因此,可以通过reserve()
预先分配内存,避免每次增加元素时都进行重新分配。 -
shrink_to_fit()
:在移除大量元素后,vector
的容量可能会大于实际需要。此时可以使用shrink_to_fit()
来释放多余的内存,不过它只是一个提示,并不保证一定会减少容量。#include <vector> #include <iostream> int main() { std::vector<int> vec(100, 1); // 初始化包含 100 个元素的 vector std::cout << "Before erase, Capacity: " << vec.capacity() << std::endl; vec.erase(vec.begin(), vec.end() - 10); // 删除大部分元素 std::cout << "After erase, Capacity: " << vec.capacity() << std::endl; vec.shrink_to_fit(); // 提示释放多余内存 std::cout << "After shrink_to_fit, Capacity: " << vec.capacity() << std::endl; return 0; }
使用reserve()
避免不必要的扩容
当你知道 vector
需要存储的大致元素数量时,调用 reserve()
预先分配足够的内存空间是一个非常有效的优化策略。这样可以避免多次扩容带来的性能损耗。
总结
- 扩容的性能开销:频繁的动态扩容会导致性能问题,建议使用
reserve()
来预先分配足够的内存。 - 迭代器失效:扩容、插入或删除元素可能导致迭代器失效,特别是在
vector
扩容或修改结构时。 - 插入和删除性能:
push_back()
是高效的,但在中间插入或删除元素会导致元素移动,影响性能。 - 对象拷贝和移动:在存储复杂对象时,使用
emplace_back()
可以避免不必要的拷贝或移动操作,提升性能。 - 管理内存:合理使用
reserve()
和shrink_to_fit()
可以有效控制vector
的内存占用,避免不必要的内存分配。
6.list和vector的区别
std::list
和 std::vector
都是 C++ 标准库中的容器,它们分别适合不同的应用场景。它们的区别主要在于底层数据结构、性能特性和使用场景。
底层数据结构
-
std::vector
:底层是一个动态数组。所有元素都存储在一块连续的内存中,因此支持随机访问。当容量不足时,vector
会自动进行内存扩展,并将所有元素复制到新的内存块中。 -
std::list
:底层是一个双向链表。每个元素都有一个指向下一个和上一个元素的指针,因此内存不是连续的。list
不支持随机访问,必须通过遍历找到指定的元素。
#include <vector>
#include <list>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3};
std::list<int> lst = {1, 2, 3};
std::cout << "Vector element access: " << vec[1] << std::endl; // O(1), 支持随机访问
auto it = lst.begin();
std::advance(it, 1);
std::cout << "List element access: " << *it << std::endl; // O(n), 需要遍历找到元素
return 0;
}
访问和修改性能
-
std::vector
:- 随机访问:
vector
支持常数时间复杂度的随机访问,[]
操作符可以直接访问任意位置的元素。时间复杂度为 O(1)。 - 插入/删除元素:在末尾插入或删除元素非常高效,时间复杂度为 O(1)。但是,在中间或开头插入/删除元素时,需要移动元素,时间复杂度为 O(n)。
- 随机访问:
-
std::list
:- 随机访问:不支持随机访问,只能通过遍历访问元素,时间复杂度为 O(n)。
- 插入/删除元素:在任意位置插入或删除元素非常高效,时间复杂度为 O(1),只需要调整指针指向,不需要移动数据。
#include <vector>
#include <list>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3};
std::list<int> lst = {1, 2, 3};
// 在 vector 的开头插入元素,性能较差 O(n)
vec.insert(vec.begin(), 0);
std::cout << "Vector after insert: ";
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
// 在 list 的开头插入元素,性能较高 O(1)
lst.insert(lst.begin(), 0);
std::cout << "List after insert: ";
for (int i : lst) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
内存管理与空间开销
-
std::vector
:vector
使用连续内存,因此空间利用率较高,适合需要频繁访问的场景。- 扩容时会重新分配内存,导致已有的元素被复制到新的位置,扩容时可能会带来性能开销。
- 空间开销:
vector
只保存元素的值,没有额外的指针开销。
-
std::list
:list
的元素分散存储在不同的内存块中,每个元素占用额外的空间来存储指向前一个和后一个元素的指针。- 空间开销:相比
vector
,list
由于每个元素都有两个指针(前向和后向),其空间开销更大。
典型使用场景
-
std::vector
:- 适合场景:频繁访问数据、查找、排序等操作非常适合使用
vector
,因为它支持快速的随机访问。 - 不适合场景:频繁在中间插入或删除元素时不适合,因为这会导致大量的数据移动。
- 适合场景:频繁访问数据、查找、排序等操作非常适合使用
-
std::list
:- 适合场景:需要频繁在任意位置进行插入和删除操作的场景非常适合使用
list
。 - 不适合场景:不适用于需要随机访问和排序的场景,因为链表的随机访问性能很差。
- 适合场景:需要频繁在任意位置进行插入和删除操作的场景非常适合使用
算法复杂度对比
操作 | std::vector 性能 | std::list 性能 |
---|---|---|
访问元素(随机访问) | O(1) | O(n) |
在末尾插入/删除 | O(1) | O(1) |
在开头插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(1) |
遍历 | O(n) | O(n) |
总结
std::vector
适合于需要随机访问和频繁末尾操作的场景,尤其在数据量固定或不常改变的情况下表现非常优秀。std::list
适合频繁进行插入和删除操作的场景,尤其是在数据量不断变化或在容器的中间频繁操作时效果更佳。
6.list如何实现元素的插入和删除?
在 C++ 的标准库容器 std::list
中,元素的插入和删除基于双向链表的结构实现。双向链表中的每个节点存储一个元素,同时有两个指针:一个指向前一个节点,另一个指向下一个节点。因此,在 list
中插入或删除元素不需要移动其他元素,只需调整指针即可。这使得插入和删除操作非常高效,时间复杂度为 O(1),无论是在链表的开头、中间还是末尾操作。
1. 插入操作的实现
std::list
提供了多种插入方式,最常见的是在某个位置之前插入元素。list
的插入操作不会移动任何已有的元素,只需要调整相关节点的指针。
- 常用插入函数:
push_back
:在链表的尾部插入元素。push_front
:在链表的头部插入元素。insert
:在指定位置插入元素。
插入元素的基本原理:
- 创建一个新的节点。
- 将新节点的前后指针分别指向其前驱和后继节点。
- 更新前驱和后继节点的指针,使其指向新节点。
#include <list>
#include <iostream>
int main() {
std::list<int> lst = {10, 20, 30};
// 在开头插入元素
lst.push_front(5); // {5, 10, 20, 30}
// 在尾部插入元素
lst.push_back(40); // {5, 10, 20, 30, 40}
// 使用 insert 在指定位置插入元素
auto it = lst.begin();
std::advance(it, 2); // 迭代器移动到第三个位置 (20 后)
lst.insert(it, 15); // {5, 10, 15, 20, 30, 40}
// 打印结果
for (const int &val : lst) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
解释:
push_front
和push_back
分别在链表的头部和尾部插入元素。insert
函数插入元素到指定位置,通过调整前后节点的指针来完成操作。
2. 删除操作的实现
std::list
同样支持高效的删除操作。因为是双向链表结构,删除某个节点只需要更新前后节点的指针即可,时间复杂度为 O(1)。删除元素不会导致其他元素的移动。
- 常用删除函数:
pop_back
:删除链表的尾部元素。pop_front
:删除链表的头部元素。erase
:删除指定位置的元素。
删除元素的基本原理:
- 将待删除节点的前驱和后继节点的指针互相指向。
- 释放待删除节点的内存。
#include <list>
#include <iostream>
int main() {
std::list<int> lst = {5, 10, 15, 20, 30, 40};
// 删除头部元素
lst.pop_front(); // {10, 15, 20, 30, 40}
// 删除尾部元素
lst.pop_back(); // {10, 15, 20, 30}
// 使用 erase 删除指定位置的元素
auto it = lst.begin();
std::advance(it, 1); // 迭代器移动到第二个位置 (15)
lst.erase(it); // {10, 20, 30}
// 打印结果
for (const int &val : lst) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
解释:
pop_front
和pop_back
分别删除链表的头部和尾部元素。erase
函数删除指定位置的元素,通过更新其前驱和后继节点的指针来完成删除操作。
3. 时间复杂度分析
- 插入和删除操作:在双向链表中插入或删除元素的复杂度为 O(1),因为不需要移动其他元素,只需调整前后节点的指针。
- 遍历:在链表中查找某个元素或遍历链表的时间复杂度为 O(n),因为需要从头到尾依次访问每个元素。
4. 使用场景
- 当需要在容器的任意位置进行频繁的插入或删除操作时,
std::list
是合适的选择。例如,任务调度、事件处理等场景中,常需要在中间插入或删除数据。 - 但如果你需要快速的随机访问,
std::list
不是一个好的选择,因为它不支持常数时间的随机访问。
总结:
- 插入和删除操作:
std::list
的插入和删除操作非常高效,适合需要频繁调整元素位置的应用场景。 - 访问性能:由于链表的结构特点,
std::list
不适合频繁进行随机访问的场景,但非常适合动态调整数据结构的场合,比如实现队列、双向队列或某些需要不断增删元素的场景。