
【C++ 】STL面试题
1.请解释vector容器和它的特点。
参考回答
std::vector
是 C++ 标准模板库 (STL) 中的一个动态数组容器。它的特点包括:
- 动态扩展:
vector
会根据需要自动调整大小,因此无需手动管理内存。 - 连续存储:它的元素存储在连续的内存中,可以通过指针或索引随机访问,性能接近普通数组。
- 高效的插入与删除:在尾部插入和删除元素非常高效(时间复杂度为 (O(1))),但在中间或头部操作时会较慢(时间复杂度为 (O(n)))。
- 支持 STL 算法:
vector
与 STL 算法(如std::sort
、std::find
)无缝兼容。 - 元素自动初始化:新增元素时可以自动调用默认构造函数进行初始化。
详细讲解与拓展
1. 内存管理与动态扩展
vector
的动态扩展是通过分配更大的连续内存并拷贝原有数据实现的。当容量不足时,vector
会以 倍增策略 增加容量(通常是 2 倍),这会导致一定的开销。但由于倍增策略的使用,均摊时间复杂度仍然是 (O(1))。例如:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
cout << "Size: " << v.size() << ", Capacity: " << v.capacity() << endl;
}
return 0;
}
C++
Copy
输出示例:
Size: 1, Capacity: 1
Size: 2, Capacity: 2
Size: 3, Capacity: 4
...
通过 v.capacity()
可以观察容量的倍增现象。
2. 连续存储
由于 vector
的存储是连续的,因此它可以像数组一样使用下标访问,同时支持与 C 风格数组互操作。例如:
vector<int> v = {1, 2, 3, 4};
cout << v[2] << endl; // 输出 3
int* arr = v.data(); // 获取指向底层数组的指针
cout << arr[2] << endl; // 输出 3
C++
Copy
连续存储使得 vector
非常适合需要随机访问的场景,但对于需要频繁插入和删除的场景(尤其是非尾部操作),则可能性能不佳。
3. 成员函数
以下是常用成员函数及其时间复杂度:
– push_back
:在尾部插入,均摊 (O(1))。
– pop_back
:删除尾部元素,时间复杂度 (O(1))。
– insert
:在指定位置插入,时间复杂度 (O(n))。
– erase
:删除指定位置的元素,时间复杂度 (O(n))。
– resize
:调整大小,可能触发元素拷贝。
– clear
:清空容器,时间复杂度 (O(n))。
4. 注意事项
- 频繁动态扩展的代价:扩展时会重新分配内存并拷贝数据,因此建议预先使用
reserve
设置合适的容量。 - 迭代器失效问题:当
vector
进行插入、删除或扩展操作时,可能导致迭代器失效。例如:
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 插入新元素
cout << *it << endl; // 未定义行为,迭代器已失效
C++
Copy
5. 扩展:vector
与其他容器的对比
- 相较于
std::array
或 C 风格数组,vector
的大小是动态的,使用更灵活。 - 相较于
std::deque
,vector
的内存使用更紧凑,但在频繁的头部插入删除场景中性能不如deque
。 - 相较于
std::list
,vector
支持随机访问,但在频繁的插入删除中效率较低。
2.vector如何保证元素的连续存储?
参考回答
std::vector
通过在底层使用动态分配的 连续内存块 来保证其元素的连续存储。每当需要扩展容量时,vector
会分配一块更大的连续内存,并将旧内存中的元素逐一复制到新的内存块中。因此,无论在任何时候,vector
的所有元素都存储在一块连续的内存空间中。
详细讲解与拓展
1. 底层机制:动态数组
std::vector
使用堆内存存储元素。它的核心是维护一个指针,指向分配的动态内存块。初始时,vector
会分配一小块连续内存来存储元素。当现有容量不足以容纳新增元素时,它会:
- 分配一个更大的连续内存块(通常是当前容量的 2 倍,称为 倍增策略)。
- 将现有的元素从旧内存块逐一复制到新内存块。
- 释放旧的内存块。
这一策略确保了所有元素始终存储在同一块连续的内存中。
2. 示例:动态扩展的实现
以下代码展示了 vector
在动态扩展时的行为:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
cout << "Size: " << v.size() << ", Capacity: " << v.capacity() << endl;
}
return 0;
}
C++
Copy
输出示例:
Size: 1, Capacity: 1
Size: 2, Capacity: 2
Size: 3, Capacity: 4
Size: 4, Capacity: 4
Size: 5, Capacity: 8
size
是当前元素个数。capacity
是当前分配的内存块大小。当容量不足时,vector
重新分配一块更大的内存,并将所有元素复制到新的内存中。
3. 为什么连续存储是必要的?
连续存储使得 vector
的设计具备以下特性:
- 随机访问:由于存储是连续的,
vector[i]
的访问是常数时间 (O(1)),本质上是通过指针偏移实现的:*(data + i)
。 - 兼容性:
vector
可以与 C 风格的数组无缝对接。例如:
“`cpp
vector v = {1, 2, 3, 4};
int* arr = v.data(); // 获取底层数组指针
cout << arr[2] << endl; // 输出 3
“`
4. 内存连续的代价:扩展时的性能开销
由于 vector
需要保证内存连续,扩展容量时必须重新分配内存并复制数据,这会带来性能开销:
– 扩展容量时的时间复杂度为 (O(n))(线性),因为需要逐一复制现有元素。
– 如果容量很大,拷贝操作可能对性能造成显著影响。
为减少扩展的频率,可以提前调用 reserve
函数分配足够的容量。例如:
vector<int> v;
v.reserve(100); // 提前分配 100 个元素的空间
5. 扩展:迭代器失效问题
由于扩展时会分配新的内存块,旧内存中的指针和迭代器都会失效。例如:
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 容量不足,分配新内存
cout << *it << endl; // 未定义行为,迭代器失效
解决方法:
– 避免在扩展前使用迭代器。
– 如果可能的扩展不可避免,可以通过 reserve
提前分配足够的容量。
3.当vector空间不足时,如何扩容?
参考回答
当 std::vector
空间不足时,它会自动扩容。具体过程如下:
- 分配更大的连续内存块:通常,新容量是当前容量的两倍(倍增策略)。
- 复制现有元素:将旧内存中的元素逐一复制到新分配的内存中。
- 释放旧内存:释放之前的内存块。
这种机制确保了 vector
的元素始终存储在连续内存中,但扩容过程中会有一定的性能开销。
详细讲解与拓展
1. 扩容触发条件
扩容的触发条件是当前 vector
容量不足以存储新增的元素。例如:
vector<int> v = {1, 2, 3};
v.push_back(4); // 如果容量不足,这里会触发扩容
C++
Copy
使用 vector
的 capacity()
成员函数可以查看当前容量。比如:
vector<int> v;
cout << v.capacity() << endl; // 初始容量为 0
v.push_back(1);
cout << v.capacity() << endl; // 容量可能变为 1
v.push_back(2);
cout << v.capacity() << endl; // 容量可能变为 2 或更大
2. 扩容机制
扩容的核心步骤包括:
-
分配新内存:
– 分配一个更大的连续内存块,新容量通常为当前容量的 2 倍。
– 倍增策略减少了频繁扩容的开销,使得扩容的均摊时间复杂度为 (O(1))。 -
数据迁移:
- 将旧内存块中的所有元素逐一拷贝到新内存中。
- 如果
vector
中的元素有复杂的析构函数或拷贝构造函数,这一步可能会导致较高的性能开销。
-
释放旧内存:
- 释放之前的内存块,避免内存泄漏。
3. 示例代码
下面的代码演示了 vector
的扩容机制:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
cout << "Size: " << v.size() << ", Capacity: " << v.capacity() << endl;
}
return 0;
}
输出示例(具体值可能因实现不同而异):
Size: 1, Capacity: 1
Size: 2, Capacity: 2
Size: 3, Capacity: 4
Size: 4, Capacity: 4
Size: 5, Capacity: 8
4. 手动优化扩容
由于扩容可能带来较大的性能开销(尤其在大规模数据操作时),我们可以通过以下方法优化:
- 提前预分配容量:
使用reserve
方法提前分配足够的空间,避免频繁扩容。例如:
“`cpp
vector v;
v.reserve(100); // 分配 100 个元素的空间
for (int i = 0; i < 100; ++i) {
v.push_back(i);
}
“`
这避免了多次扩容操作,提高了效率。
-
使用
resize
:
如果已知最终大小,可以直接使用resize
:vector<int> v; v.resize(100); // 分配并初始化 100 个元素
5. 倍增策略的均摊效率
虽然每次扩容会导致 (O(n)) 的拷贝开销,但由于倍增策略,扩容的次数是对数级别的。例如,当从 1 扩展到 100 个元素时,扩容的次数约为 (\log_2(100) \approx 7)。因此,扩容的 均摊时间复杂度 为 (O(1))。
6. 注意迭代器失效
扩容后,vector
的迭代器、指针或引用都会失效,因为数据已经迁移到新的内存地址。例如:
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 容量不足导致扩容
cout << *it << endl; // 未定义行为,迭代器已失效
解决方法:
– 在扩容可能发生时,避免使用旧的迭代器。
– 如果需要频繁扩容,可以通过 reserve
预分配空间。
4.vector的push_back和emplace_back有什么区别?
参考回答
push_back
和 emplace_back
都是用来向 std::vector
添加新元素的函数,主要区别在于:
push_back
:需要一个已构造的对象,将其拷贝(或移动)到vector
中。emplace_back
:直接在vector
的存储空间中原地构造对象,避免了额外的拷贝或移动操作。
简单来说:
– 如果已经有一个现成的对象,使用 push_back
。
– 如果需要传递构造参数创建对象,使用 emplace_back
。
详细讲解与拓展
1. push_back
的工作机制
push_back
将一个现成的对象添加到 vector
中:
– 如果对象支持移动语义,调用其移动构造函数。
– 否则调用其拷贝构造函数。
示例:
#include <vector>
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int x) { cout << "Constructor: " << x << endl; }
MyClass(const MyClass& other) { cout << "Copy Constructor" << endl; }
MyClass(MyClass&& other) noexcept { cout << "Move Constructor" << endl; }
};
int main() {
vector<MyClass> v;
MyClass obj(10);
v.push_back(obj); // 调用拷贝构造函数
v.push_back(std::move(obj)); // 调用移动构造函数
return 0;
}
输出:
Constructor: 10
Copy Constructor
Move Constructor
2. emplace_back
的工作机制
emplace_back
将对象的构造过程直接嵌入到 vector
的存储空间中,无需临时对象:
– 使用提供的参数,直接调用对象的构造函数。
示例:
#include <vector>
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int x) { cout << "Constructor: " << x << endl; }
MyClass(const MyClass& other) { cout << "Copy Constructor" << endl; }
MyClass(MyClass&& other) noexcept { cout << "Move Constructor" << endl; }
};
int main() {
vector<MyClass> v;
v.emplace_back(10); // 直接在 vector 中调用构造函数
return 0;
}
输出:
Constructor: 10
可以看到,emplace_back
直接调用了构造函数,没有拷贝或移动操作。
3. 性能比较
push_back
需要传递一个现成的对象,可能会涉及拷贝或移动操作,效率较低。emplace_back
直接原地构造对象,省去了构造临时对象的开销,效率更高。
适用场景:
– 如果直接构造对象,优先使用 emplace_back
。
– 如果已有现成的对象(如变量或函数返回值),使用 push_back
。
4. 示例对比
以下代码展示了两者的性能差异:
#include <vector>
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int x, int y) { cout << "Constructor: " << x << ", " << y << endl; }
MyClass(const MyClass& other) { cout << "Copy Constructor" << endl; }
MyClass(MyClass&& other) noexcept { cout << "Move Constructor" << endl; }
};
int main() {
vector<MyClass> v1, v2;
cout << "Using push_back:" << endl;
MyClass obj(1, 2);
v1.push_back(obj); // 调用拷贝构造
v1.push_back(std::move(obj)); // 调用移动构造
cout << "\nUsing emplace_back:" << endl;
v2.emplace_back(1, 2); // 直接原地构造
return 0;
}
输出:
Using push_back:
Constructor: 1, 2
Copy Constructor
Move Constructor
Using emplace_back:
Constructor: 1, 2
可以看到,emplace_back
避免了拷贝和移动操作。
5. 注意事项
- 如果使用
emplace_back
,传递的参数必须匹配对象的构造函数,否则会导致编译错误。 - 在性能敏感的代码中,优先考虑使用
emplace_back
。
5.使用vector需要注意哪些问题?
参考回答
使用 std::vector
时需要注意以下几个问题:
- 迭代器失效:
vector
在扩容或删除元素时,会导致迭代器、指针和引用失效。 - 频繁扩容的性能开销:每次扩容都会重新分配内存并拷贝元素,可以提前使用
reserve
优化。 - 插入和删除性能:
vector
在中间或头部插入和删除元素的性能较差,因为需要移动后续元素。 - 空间未释放:使用
clear
只清空元素,容量不变,可使用shrink_to_fit
回收内存。 - 线程安全:
vector
不是线程安全的,需在多线程场景下显式同步。
详细讲解与拓展
1. 迭代器失效问题
当 vector
扩容、插入或删除元素时,其底层存储可能会重新分配内存,导致迭代器失效。
场景 1:扩容导致迭代器失效
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 扩容可能发生
cout << *it << endl; // 未定义行为,迭代器已失效
return 0;
}
C++
Copy
场景 2:插入或删除导致迭代器失效
– 插入:从插入点开始的所有迭代器失效。
– 删除:被删除的元素及其之后的迭代器失效。
解决方法:
– 尽量避免在迭代过程中修改 vector
。
– 如果需要插入或删除,可以记录索引值而非使用迭代器。
2. 频繁扩容的性能开销
当 vector
容量不足时,扩容会分配更大的内存并拷贝现有元素。这可能导致较大的性能开销。
优化方法:使用 reserve
提前分配容量
#include <vector>
using namespace std;
int main() {
vector<int> v;
v.reserve(100); // 提前分配 100 个元素的空间
for (int i = 0; i < 100; ++i) {
v.push_back(i); // 无需多次扩容
}
return 0;
}
C++
Copy
通过 reserve
减少内存分配次数,显著提升性能。
3. 插入和删除性能
由于 vector
是连续存储,插入或删除元素时需要移动后续的所有元素,其时间复杂度为 (O(n))。对于频繁插入和删除的场景,可以考虑以下替代方案:
– 使用 std::deque
,支持高效的头尾插入。
– 使用 std::list
,支持高效的任意位置插入和删除。
示例:
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4};
v.erase(v.begin() + 1); // 删除第二个元素,后续元素移动
v.insert(v.begin(), 0); // 在头部插入元素,所有元素后移
return 0;
}
C++
Copy
4. 空间未释放
使用 vector
的 clear
函数时,元素会被销毁,但底层的内存容量不会减少。如果需要回收未使用的空间,可以调用 shrink_to_fit
。
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
v.clear(); // 清空元素,但容量不变
cout << "Capacity: " << v.capacity() << endl;
v.shrink_to_fit(); // 回收未使用空间
cout << "Capacity after shrink: " << v.capacity() << endl;
return 0;
}
C++
Copy
5. 线程安全问题
vector
是非线程安全的,在多线程环境中需要显式同步,例如使用 mutex
或其他同步机制。
示例:
#include <vector>
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
vector<int> v;
mutex mtx;
void addElements() {
lock_guard<mutex> guard(mtx);
for (int i = 0; i < 100; ++i) {
v.push_back(i);
}
}
int main() {
thread t1(addElements);
thread t2(addElements);
t1.join();
t2.join();
cout << "Vector size: " << v.size() << endl;
return 0;
}
C++
Copy
6. 扩展:vector
的常见误用
-
越界访问:
vector
的下标访问不进行边界检查,会导致未定义行为。vector<int> v = {1, 2, 3}; cout << v[5]; // 未定义行为
C++
Copy
解决方法:使用
at
函数,它会进行边界检查。cout << v.at(5); // 抛出 std::out_of_range 异常
C++
Copy
-
不合理的容量设置:
resize
和reserve
的功能不同,误用可能导致逻辑错误。vector<int> v; v.reserve(5); // 仅分配空间,size 不变 v.resize(5); // 分配空间并初始化元素
C++
Copy
6.为什么 list
支持 push_front()
?
std::list
是通过 双向链表 实现的,因此在头部插入元素非常高效,时间复杂度为 (O(1))。链表的节点存储了两个指针(分别指向前一个和后一个节点),在头部插入时只需调整前后指针即可。
示意图:
– 初始链表:NULL <- 2 <-> 3 <-> 4 -> NULL
– 执行 push_front(1)
后:
NULL <- 1 <-> 2 <-> 3 <-> 4 -> NULL
2. 与其他容器的对比
std::deque
:- 也支持
push_front()
,但它是通过分段连续数组实现的。 - 适合高效的头部和尾部插入。
- 也支持
std::vector
:- 不支持
push_front()
,因为它是动态数组,头部插入需要移动所有元素,时间复杂度为 (O(n)),效率低下。
- 不支持
3. 常见用法
push_front()
常用于需要在链表头部高效插入元素的场景。例如:
– 构建一个逆序链表。
– 在处理优先级队列时,按优先级插入到头部。
示例代码:
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> lst;
lst.push_front(10);
lst.push_front(20); // 插入到头部
lst.push_front(30);
for (int val : lst) {
cout << val << " ";
}
return 0;
}
输出:
30 20 10
7.unordered_map和map有什么区别?
参考回答
std::unordered_map
和 std::map
是 C++ STL 中的关联容器,它们主要区别在于底层实现、性能、功能和适用场景。
主要区别:
- 底层实现:
map
是基于 红黑树 实现的,保证键值对按照键的顺序存储。unordered_map
是基于 哈希表 实现的,键值对无序存储。
- 时间复杂度:
map
的操作(插入、删除、查找)时间复杂度为 (O(\log n))。unordered_map
的操作(插入、删除、查找)平均时间复杂度为 (O(1)),最坏情况 (O(n))。
- 键的顺序:
map
中的键自动按照升序(或自定义顺序)排列。unordered_map
中的键没有固定顺序。
- 内存使用:
unordered_map
为了实现哈希表,通常需要更多内存来存储哈希桶。map
使用红黑树,相对节省内存,但在插入和维护顺序时稍慢。
详细讲解与拓展
1. 底层实现
map
:- 使用红黑树(自平衡二叉搜索树)。
- 保证所有键值对按照键的顺序存储。
- 每次插入或删除都需要调整红黑树以保持平衡。
unordered_map
:- 使用哈希表存储元素。
- 哈希表通过键的哈希值将数据存储在哈希桶中。
- 如果发生哈希冲突(多个键映射到同一个哈希桶),采用链表或其他方式解决冲突。
2. 时间复杂度
map
:- 插入、删除和查找的时间复杂度为 (O(\log n)),因为需要遍历红黑树的高度。
unordered_map
:- 插入、删除和查找的平均时间复杂度为 (O(1))。
- 最坏情况下,若哈希函数不均匀导致大量冲突,时间复杂度可能退化为 (O(n))。
3. 键的顺序
map
:- 自动对键进行排序(默认升序),可以通过自定义比较函数改变排序规则。
- 遍历时,元素总是按照键的顺序输出。
unordered_map
:- 键值对存储顺序完全由哈希函数决定。
- 遍历时,元素的顺序是不可预测的。
4. 使用示例
map
示例:
#include <map>
#include <iostream>
using namespace std;
int main() {
map<int, string> myMap = {{2, "Two"}, {1, "One"}, {3, "Three"}};
for (const auto& [key, value] : myMap) {
cout << key << ": " << value << endl;
}
return 0;
}
输出:
1: One
2: Two
3: Three
unordered_map
示例:
#include <unordered_map>
#include <iostream>
using namespace std;
int main() {
unordered_map<int, string> myUnorderedMap = {{2, "Two"}, {1, "One"}, {3, "Three"}};
for (const auto& [key, value] : myUnorderedMap) {
cout << key << ": " << value << endl;
}
return 0;
}
输出(顺序可能不同,每次运行可能变化):
2: Two
3: Three
1: One
5. 内存使用
map
:- 每个节点存储一个键值对,同时维护左右子节点和父节点的指针,内存使用较为紧凑。
- 插入或删除时需要调整红黑树的结构,有一定的性能开销。
unordered_map
:- 使用哈希桶(bucket)存储数据。
- 需要额外的内存用于存储桶指针,以及解决哈希冲突的链表或其他结构。
6. 适用场景
场景 | 推荐容器 |
---|---|
数据需要按键排序输出 | map |
需要快速查找键值对,且对顺序无要求 | unordered_map |
内存有限,避免额外的哈希桶开销 | map |
数据规模较大,需要高效插入和查找 | unordered_map |
自定义键的排序规则 | map |
7. 总结对比表
特性 | map |
unordered_map |
---|---|---|
底层实现 | 红黑树 | 哈希表 |
时间复杂度 | (O(\log n)) | 平均 (O(1)),最坏 (O(n)) |
键的顺序 | 按照键排序(默认升序) | 无序 |
内存使用 | 相对节省内存 | 需要更多内存用于哈希桶 |
适用场景 | 排序数据、范围查询 | 快速查找、插入和删除 |
总结
- 使用
map
:- 当需要自动对键排序,或需要范围查询(如上下界操作)时,
map
是最佳选择。 - 如需要更高效的内存利用率,
map
也更适合。
- 当需要自动对键排序,或需要范围查询(如上下界操作)时,
- 使用
unordered_map
:- 当键不需要排序,并且查找、插入和删除的性能要求较高时,
unordered_map
是更优选择。 - 在数据量较大且操作频繁的情况下,
unordered_map
的平均性能更高,但需要注意哈希冲突可能导致性能退化。
- 当键不需要排序,并且查找、插入和删除的性能要求较高时,
根据具体需求选择适合的容器,可以显著提升代码性能和可维护性。
五种迭代器类型分别是什么?
参考回答
C++ STL 中的五种迭代器类型是:
- 输入迭代器(Input Iterator):
只允许从容器中读取元素,且只能向前移动一次。适用于只进行读取操作的场景。 - 输出迭代器(Output Iterator):
只允许向容器写入元素,且只能向前移动一次。适用于只进行写入操作的场景。 - 前向迭代器(Forward Iterator):
既可以读取元素,也可以写入元素。它支持多次向前移动,但不支持回退。适用于不需要回溯的场景。 - 双向迭代器(Bidirectional Iterator):
除了支持前向移动外,还可以回退到之前的位置。适用于需要双向遍历的场景。 - 随机访问迭代器(Random Access Iterator):
支持前向移动、回退,并且支持任意位置的跳跃(如+
、-
运算符)。它是最强大的迭代器,适用于vector
和deque
等支持随机访问的容器。
详细讲解与拓展
-
输入迭代器(Input Iterator):
-
输入迭代器只能进行读取操作,它只能向前遍历容器元素,并且每个元素只能被访问一次。这使得它在单次读取时非常高效。一个典型的例子是
istream_iterator
,它用于从流中读取数据。 -
示例:
std::istream_iterator<int> input_it(std::cin); std::istream_iterator<int> end_it; while (input_it != end_it) { std::cout << *input_it << " "; ++input_it; }
C++
Copy
-
该代码片段会从标准输入读取整数并打印,直到输入结束。
-
-
输出迭代器(Output Iterator):
-
输出迭代器只能用于写操作,且只能将数据写入容器中。它也只能向前遍历一次,不支持回退或随机访问。一个常见的输出迭代器是
ostream_iterator
,用于将数据输出到流中。 -
示例:
std::ostream_iterator<int> output_it(std::cout, " "); *output_it = 10; // 将 10 输出到标准输出
C++
Copy
-
-
前向迭代器(Forward Iterator):
-
前向迭代器不仅支持读取和写入操作,而且可以在容器中进行多次前向遍历。它适用于需要多次读取数据但不需要回溯的情况。
list
容器的迭代器就是前向迭代器。 -
示例:
std::list<int> lst = {1, 2, 3, 4, 5}; std::list<int>::iterator it = lst.begin(); while (it != lst.end()) { std::cout << *it << " "; ++it; }
C++
Copy
-
-
双向迭代器(Bidirectional Iterator):
-
双向迭代器除了支持前向遍历外,还支持后退操作。这使得它可以在容器中进行双向遍历。
list
和set
容器的迭代器就是双向迭代器。 -
示例:
std::list<int> lst = {1, 2, 3, 4, 5}; auto it = lst.begin(); ++it; // 向前移动 --it; // 向后移动
C++
Copy
-
-
随机访问迭代器(Random Access Iterator):
-
随机访问迭代器是最强大的迭代器,支持前向、后退并且支持任意位置的跳跃操作(通过加减偏移量)。它们通常用于
vector
和deque
容器,因其支持常数时间的随机访问。 -
示例:
std::vector<int> vec = {1, 2, 3, 4, 5}; auto it = vec.begin(); std::cout << *(it + 2) << std::endl; // 输出 3
C++
Copy
-
-
迭代器的继承关系:
C++ STL 中的迭代器是按照能力的强弱进行分类的。所有迭代器类型都从一个基础类Iterator
继承,并逐渐增加其能力:Input Iterator
是最基础的迭代器,支持输入操作。Output Iterator
支持输出操作。Forward Iterator
能多次向前遍历。Bidirectional Iterator
支持前进和回退操作。Random Access Iterator
是最强大的,支持任意的索引和移动。
-
常量迭代器:
还有一种与上述迭代器相关的类型叫做“常量迭代器(Const Iterator)”。它不允许通过迭代器修改容器中的元素,只能进行读取。这对于需要保护容器数据不被修改的场景非常有用。
介绍一下STL中的算法库。
参考回答
C++ STL 中的算法库提供了一组通用的算法函数,用于在容器中执行常见操作(如排序、查找、修改、复制等)。这些算法不依赖于容器类型,可以与任何标准容器配合使用,极大地提高了代码的复用性和效率。
STL 算法库主要包括以下几类常见算法:
- 排序算法:
std::sort()
:对容器进行排序。std::stable_sort()
:稳定排序,保持相等元素的相对顺序不变。std::partial_sort()
:对部分元素进行排序。
- 查找算法:
std::find()
:查找指定元素。std::binary_search()
:在已排序容器中查找元素。std::lower_bound()
和std::upper_bound()
:查找元素的插入位置。
- 修改算法:
std::transform()
:对容器中的元素进行修改。std::replace()
和std::replace_if()
:替换容器中的元素。std::fill()
:填充容器的所有元素。
- 统计算法:
std::count()
:统计容器中某个元素出现的次数。std::count_if()
:统计满足特定条件的元素个数。std::accumulate()
:计算容器中元素的总和。
- 集合操作:
std::set_intersection()
:计算两个集合的交集。std::set_union()
:计算两个集合的并集。std::set_difference()
:计算两个集合的差集。
- 其他常用算法:
std::reverse()
:反转容器中的元素。std::shuffle()
:打乱容器中的元素顺序。std::remove()
和std::remove_if()
:移除容器中的元素。
详细讲解与拓展
STL 算法库是 C++ 标准库中的一个重要组成部分,提供了大量功能强大且高效的算法,能帮助开发者减少手动编写常见操作的代码,并且能够优化程序的性能。这里我们将进一步讲解一些常见的 STL 算法。
-
排序算法:
std::sort()
:对容器中的元素进行升序排序。时间复杂度为 O(n log n),采用快速排序(在特定情况下,可能会退化为堆排序)。std::stable_sort()
:与std::sort()
类似,但它保证在排序过程中,等于的元素保持原有的相对顺序。常用于需要保持元素相对顺序的情况(例如按名字排序但保留原有年龄顺序)。
示例:
std::vector<int> vec = {5, 2, 8, 3, 1}; std::sort(vec.begin(), vec.end()); // 升序排序
C++
Copy
-
查找算法:
std::find()
:查找容器中是否存在指定的元素,返回指向该元素的迭代器,若未找到,则返回容器的end()
。std::binary_search()
:在已排序的容器中查找元素。它返回布尔值,表示容器中是否存在指定元素。它的时间复杂度是 O(log n),因为它使用二分查找。
示例:
std::vector<int> vec = {1, 2, 3, 4, 5}; auto it = std::find(vec.begin(), vec.end(), 3); // 查找元素 3 if (it != vec.end()) { std::cout << "Found 3" << std::endl; }
C++
Copy
-
修改算法:
std::transform()
:对容器中的每个元素应用指定的操作。它非常适合用于修改容器的元素,如将所有元素平方。
示例:
std::vector<int> vec = {1, 2, 3, 4}; std::transform(vec.begin(), vec.end(), vec.begin(), [](int x) { return x * x; }); // vec 变为 {1, 4, 9, 16}
C++
Copy
-
统计算法:
std::accumulate()
:计算容器中所有元素的累积和,支持指定初始值。
示例:
std::vector<int> vec = {1, 2, 3, 4}; int sum = std::accumulate(vec.begin(), vec.end(), 0); // 计算总和,结果为 10
C++
Copy
-
集合操作:
STL 提供了操作集合的算法,能够帮助开发者进行集合间的并集、交集、差集等常见集合操作。std::set_intersection()
:计算两个已排序集合的交集,并将结果存储到指定的容器中。
示例:
std::vector<int> vec1 = {1, 2, 3, 4}; std::vector<int> vec2 = {3, 4, 5, 6}; std::vector<int> result; std::set_intersection(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), std::back_inserter(result)); // result 变为 {3, 4}
C++
Copy
-
其他常用算法:
std::reverse()
:反转容器中的元素,适用于任何双向迭代器容器(如vector
、deque
、list
)。
示例:
std::vector<int> vec = {1, 2, 3, 4}; std::reverse(vec.begin(), vec.end()); // vec 变为 {4, 3, 2, 1}
- 感谢你赐予我前进的力量