C++常用容器详解:原理、适用场景与代码示例
C++标准模板库(STL)提供了丰富的容器类型,这些容器封装了数据结构,为开发者提供了高效、灵活的数据存储和操作方式。本文将详细介绍C++中所有常用容器,包括它们的内部原理、适用场景,并通过代码示例展示其基本用法。
容器分类概述
C++容器主要分为三大类:
- 序列容器:按顺序存储元素,如vector、list、deque等
- 关联容器:按关键字存储元素,如set、map等
- 容器适配器:基于其他容器实现,提供特定接口,如stack、queue等
此外,C++11还引入了无序关联容器,如unordered_set、unordered_map等。
一、序列容器
1. vector(动态数组)
原理
vector是最常用的序列容器,内部基于动态数组实现,元素在内存中连续存储。当存储空间不足时,会自动分配更大的内存块(通常是当前容量的2倍),并将所有元素复制到新内存中。
特点
- 支持随机访问,时间复杂度O(1)
- 尾部插入/删除效率高,O(1)
- 中间或头部插入/删除效率低,需要移动元素,O(n)
- 内存连续,缓存友好
适用场景
- 需要频繁随机访问元素
- 尾部插入/删除操作较多
- 不需要频繁在中间插入/删除元素
代码示例
#include <vector>
#include <iostream>int main() {// 创建vectorstd::vector<int> vec;// 尾部插入元素vec.push_back(10);vec.push_back(20);vec.push_back(30);// 访问元素std::cout << "第二个元素: " << vec[1] << std::endl;std::cout << "第三个元素: " << vec.at(2) << std::endl;// 遍历元素std::cout << "所有元素: ";for (size_t i = 0; i < vec.size(); ++i) {std::cout << vec[i] << " ";}std::cout << std::endl;// 使用迭代器遍历std::cout << "使用迭代器: ";for (auto it = vec.begin(); it != vec.end(); ++it) {std::cout << *it << " ";}std::cout << std::endl;// 插入元素vec.insert(vec.begin() + 1, 15);// 删除元素vec.erase(vec.end() - 1);// 容量和大小std::cout << "大小: " << vec.size() << ", 容量: " << vec.capacity() << std::endl;// 清空容器vec.clear();return 0;
}
2. list(双向链表)
原理
list基于双向链表实现,每个元素包含数据和两个指针(前驱和后继),元素在内存中不连续存储。
特点
- 不支持随机访问,访问元素需从头部或尾部遍历,O(n)
- 任何位置的插入/删除效率高,只需调整指针,O(1)
- 内存开销较大,需要存储额外的指针
- 迭代器在插入和删除时不会失效(被删除元素的迭代器除外)
适用场景
- 需要频繁在任意位置插入/删除元素
- 不需要随机访问元素
- 元素数量不确定,频繁增减
代码示例
#include <list>
#include <iostream>int main() {// 创建liststd::list<int> mylist;// 插入元素mylist.push_back(10); // 尾部插入mylist.push_front(5); // 头部插入mylist.insert(++mylist.begin(), 7); // 中间插入// 遍历元素std::cout << "所有元素: ";for (auto it = mylist.begin(); it != mylist.end(); ++it) {std::cout << *it << " ";}std::cout << std::endl;// 大小std::cout << "大小: " << mylist.size() << std::endl;// 删除元素mylist.pop_front(); // 删除头部mylist.pop_back(); // 删除尾部// 排序mylist.push_back(3);mylist.push_back(15);mylist.sort();std::cout << "排序后: ";for (auto val : mylist) { // 使用范围for循环std::cout << val << " ";}std::cout << std::endl;// 反转mylist.reverse();return 0;
}
3. deque(双端队列)
原理
deque(double-ended queue)是双端队列,内部由多个连续的内存块组成,通过一个中央控制器维护这些内存块的指针。
特点
- 支持随机访问,O(1)
- 头部和尾部的插入/删除效率高,O(1)
- 中间插入/删除效率低,O(n)
- 相比vector,deque在头部操作更高效,且不会像vector那样在扩容时复制所有元素
适用场景
- 需要在两端频繁插入/删除元素
- 需要随机访问
- 如实现队列、缓冲区等
代码示例
#include <deque>
#include <iostream>int main() {// 创建dequestd::deque<int> dq;// 两端插入dq.push_back(10);dq.push_front(5);dq.push_back(15);dq.push_front(0);// 访问元素std::cout << "第一个元素: " << dq.front() << std::endl;std::cout << "最后一个元素: " << dq.back() << std::endl;std::cout << "第二个元素: " << dq[1] << std::endl;// 遍历std::cout << "所有元素: ";for (auto val : dq) {std::cout << val << " ";}std::cout << std::endl;// 两端删除dq.pop_front();dq.pop_back();// 大小std::cout << "大小: " << dq.size() << std::endl;return 0;
}
4. array(固定大小数组)
原理
array是C++11引入的固定大小数组容器,封装了静态数组,大小在编译时确定。
特点
- 大小固定,不能动态改变
- 支持随机访问,O(1)
- 内存连续,存储在栈上(通常)
- 相比原生数组,提供了更安全的访问方式和成员函数
适用场景
- 元素数量固定且已知
- 需要高效的随机访问
- 希望使用容器接口操作静态数组
代码示例
#include <array>
#include <iostream>int main() {// 创建array,指定类型和大小std::array<int, 5> arr = {1, 2, 3, 4, 5};// 访问元素std::cout << "第三个元素: " << arr[2] << std::endl;std::cout << "第四个元素: " << arr.at(3) << std::endl;// 大小std::cout << "大小: " << arr.size() << std::endl;std::cout << "最大大小: " << arr.max_size() << std::endl; // 对于array,size() == max_size()// 遍历std::cout << "所有元素: ";for (auto val : arr) {std::cout << val << " ";}std::cout << std::endl;// 填充arr.fill(0);return 0;
}
5. forward_list(单向链表)
原理
forward_list是C++11引入的单向链表,每个元素只包含数据和一个指向下一个元素的指针。
特点
- 只支持单向遍历
- 不支持随机访问
- 内存开销比list小
- 在元素前面插入效率高,O(1)
- 没有size()方法,获取大小需要遍历,O(n)
适用场景
- 需要单向遍历
- 内存受限,希望减少开销
- 主要在头部或当前位置前插入元素
代码示例
#include <forward_list>
#include <iostream>int main() {// 创建forward_liststd::forward_list<int> flist = {1, 2, 3, 4};// 头部插入flist.push_front(0);// 在指定位置前插入auto it = flist.begin();++it; // 指向1flist.insert_after(it, 5); // 在1后面插入5// 遍历std::cout << "所有元素: ";for (auto val : flist) {std::cout << val << " ";}std::cout << std::endl;// 删除元素flist.pop_front(); // 删除头部it = flist.begin();flist.erase_after(it); // 删除it后面的元素return 0;
}
二、关联容器
关联容器按关键字存储和访问元素,内部通常基于红黑树实现,保证元素有序且操作高效。
1. set(集合)
原理
set是存储唯一关键字的有序集合,内部基于红黑树实现,元素按关键字自动排序。
特点
- 元素唯一,不允许重复
- 自动排序(默认升序)
- 插入、删除、查找操作效率高,O(log n)
- 不能直接修改元素值(会破坏排序),需先删除再插入
适用场景
- 需要存储唯一元素
- 需要自动排序的集合
- 频繁查找、插入和删除操作
代码示例
#include <set>
#include <iostream>int main() {// 创建set,默认升序std::set<int> myset;// 插入元素myset.insert(30);myset.insert(10);myset.insert(20);myset.insert(20); // 重复元素,不会被插入// 大小std::cout << "大小: " << myset.size() << std::endl;// 遍历(自动排序)std::cout << "所有元素: ";for (auto val : myset) {std::cout << val << " ";}std::cout << std::endl;// 查找元素auto it = myset.find(20);if (it != myset.end()) {std::cout << "找到元素: " << *it << std::endl;} else {std::cout << "未找到元素" << std::endl;}// 删除元素myset.erase(20);// 自定义排序(降序)std::set<int, std::greater<int>> desc_set = {5, 3, 8};std::cout << "降序set: ";for (auto val : desc_set) {std::cout << val << " ";}std::cout << std::endl;return 0;
}
2. map(映射)
原理
map存储键值对(key-value),每个键唯一,内部基于红黑树实现,按键自动排序。
特点
- 键唯一,每个键对应一个值
- 自动按键排序
- 插入、删除、查找操作效率高,O(log n)
- 可以通过键快速访问值
适用场景
- 需要键值对映射关系
- 需要按键排序
- 如字典、索引表等
代码示例
#include <map>
#include <string>
#include <iostream>int main() {// 创建map,键为string,值为intstd::map<std::string, int> mymap;// 插入键值对mymap["apple"] = 5;mymap["banana"] = 3;mymap.insert({"orange", 7});// 访问元素std::cout << "apple的数量: " << mymap["apple"] << std::endl;// 遍历std::cout << "所有元素: " << std::endl;for (auto& pair : mymap) { // pair是键值对std::cout << pair.first << ": " << pair.second << std::endl;}// 查找元素auto it = mymap.find("banana");if (it != mymap.end()) {std::cout << "找到: " << it->first << ": " << it->second << std::endl;}// 修改值mymap["apple"] = 10;// 删除元素mymap.erase("orange");return 0;
}
3. multiset(多重集合)
原理
multiset与set类似,区别在于允许存储重复元素。
特点
- 允许重复元素
- 自动排序
- 插入、删除、查找操作效率高,O(log n)
适用场景
- 需要存储可能重复的元素
- 需要自动排序
- 如统计元素出现次数
代码示例
#include <set>
#include <iostream>int main() {// 创建multisetstd::multiset<int> mset;// 插入元素(可以重复)mset.insert(20);mset.insert(10);mset.insert(20);mset.insert(30);mset.insert(20);// 大小std::cout << "大小: " << mset.size() << std::endl;// 遍历std::cout << "所有元素: ";for (auto val : mset) {std::cout << val << " ";}std::cout << std::endl;// 统计元素出现次数int count = mset.count(20);std::cout << "20出现的次数: " << count << std::endl;// 删除所有等于20的元素mset.erase(20);return 0;
}
4. multimap(多重映射)
原理
multimap与map类似,区别在于允许键重复,一个键可以对应多个值。
特点
- 允许键重复
- 自动按键排序
- 插入、删除、查找操作效率高,O(log n)
适用场景
- 一个键需要对应多个值的情况
- 如:一个学生(键)有多门成绩(值)
代码示例
#include <map>
#include <string>
#include <iostream>int main() {// 创建multimapstd::multimap<std::string, int> mmap;// 插入键值对(键可以重复)mmap.insert({"Alice", 90});mmap.insert({"Bob", 85});mmap.insert({"Alice", 95});mmap.insert({"Charlie", 88});// 遍历所有元素std::cout << "所有成绩: " << std::endl;for (auto& pair : mmap) {std::cout << pair.first << ": " << pair.second << std::endl;}// 查找特定键的所有值std::string name = "Alice";auto range = mmap.equal_range(name);std::cout << name << "的所有成绩: ";for (auto it = range.first; it != range.second; ++it) {std::cout << it->second << " ";}std::cout << std::endl;return 0;
}
三、无序关联容器
无序关联容器基于哈希表实现,元素无序,但查找、插入、删除操作平均效率更高。
1. unordered_set(无序集合)
原理
unordered_set存储唯一元素,内部基于哈希表实现,通过哈希函数快速定位元素。
特点
- 元素唯一
- 元素无序
- 平均查找、插入、删除效率为O(1),最坏情况O(n)
- 不支持基于顺序的操作(如lower_bound)
适用场景
- 需要快速查找、插入、删除
- 不关心元素顺序
- 元素适合作为哈希键
代码示例
#include <unordered_set>
#include <iostream>int main() {// 创建unordered_setstd::unordered_set<int> uset;// 插入元素uset.insert(30);uset.insert(10);uset.insert(20);uset.insert(20); // 重复元素,不会被插入// 大小std::cout << "大小: " << uset.size() << std::endl;// 遍历(无序)std::cout << "所有元素: ";for (auto val : uset) {std::cout << val << " ";}std::cout << std::endl;// 查找元素auto it = uset.find(20);if (it != uset.end()) {std::cout << "找到元素: " << *it << std::endl;}// 桶相关操作std::cout << "桶数量: " << uset.bucket_count() << std::endl;std::cout << "负载因子: " << uset.load_factor() << std::endl;return 0;
}
2. unordered_map(无序映射)
原理
unordered_map存储键值对,键唯一,内部基于哈希表实现。
特点
- 键唯一
- 元素无序
- 平均查找、插入、删除效率为O(1)
- 相比map,更适合频繁查找操作
适用场景
- 需要快速的键值对查找
- 不关心键的顺序
- 如缓存、字典等
代码示例
#include <unordered_map>
#include <string>
#include <iostream>int main() {// 创建unordered_mapstd::unordered_map<std::string, std::string> umap;// 插入键值对umap["apple"] = "苹果";umap["banana"] = "香蕉";umap.insert({"orange", "橙子"});// 访问元素std::cout << "apple: " << umap["apple"] << std::endl;// 遍历(无序)std::cout << "所有元素: " << std::endl;for (auto& pair : umap) {std::cout << pair.first << ": " << pair.second << std::endl;}// 查找元素auto it = umap.find("banana");if (it != umap.end()) {std::cout << "找到: " << it->first << ": " << it->second << std::endl;}return 0;
}
3. unordered_multiset和unordered_multimap
这两个容器分别与multiset和multimap对应,区别在于它们基于哈希表实现,元素无序,但平均操作效率更高。使用方法与multiset和multimap类似,这里不再赘述。
四、容器适配器
容器适配器是对现有容器的封装,提供特定的接口,不支持迭代器。
1. stack(栈)
原理
stack基于其他容器(默认是deque)实现,提供后进先出(LIFO)的操作接口。
特点
- 后进先出
- 只能访问栈顶元素
- 主要操作:push(入栈)、pop(出栈)、top(访问栈顶)
适用场景
- 需要后进先出的场景
- 如函数调用栈、表达式求值、括号匹配等
代码示例
#include <stack>
#include <iostream>int main() {// 创建stackstd::stack<int> stk;// 入栈stk.push(10);stk.push(20);stk.push(30);// 大小std::cout << "栈大小: " << stk.size() << std::endl;// 访问栈顶元素std::cout << "栈顶元素: " << stk.top() << std::endl;// 出栈stk.pop();std::cout << "出栈后栈顶: " << stk.top() << std::endl;// 遍历栈(需要弹出所有元素)std::cout << "栈元素(从顶到底): ";while (!stk.empty()) {std::cout << stk.top() << " ";stk.pop();}std::cout << std::endl;return 0;
}
2. queue(队列)
原理
queue基于其他容器(默认是deque)实现,提供先进先出(FIFO)的操作接口。
特点
- 先进先出
- 只能访问队头和队尾元素
- 主要操作:push(入队)、pop(出队)、front(队头)、back(队尾)
适用场景
- 需要先进先出的场景
- 如任务调度、广度优先搜索等
代码示例
#include <queue>
#include <iostream>int main() {// 创建queuestd::queue<int> q;// 入队q.push(10);q.push(20);q.push(30);// 大小std::cout << "队列大小: " << q.size() << std::endl;// 访问队头和队尾std::cout << "队头: " << q.front() << ", 队尾: " << q.back() << std::endl;// 出队q.pop();std::cout << "出队后队头: " << q.front() << std::endl;// 遍历队列(需要弹出所有元素)std::cout << "队列元素(顺序): ";while (!q.empty()) {std::cout << q.front() << " ";q.pop();}std::cout << std::endl;return 0;
}
3. priority_queue(优先队列)
原理
priority_queue基于其他容器(默认是vector)实现,内部通常使用堆排序算法,保证每次取出的元素是优先级最高的(默认最大元素)。
特点
- 元素按优先级排序,默认最大元素优先
- 主要操作:push(插入)、pop(删除最高优先级元素)、top(访问最高优先级元素)
- 插入和删除操作效率为O(log n)
适用场景
- 需要按优先级处理元素的场景
- 如任务调度(优先级高的任务先执行)、Dijkstra算法等
代码示例
#include <queue>
#include <iostream>int main() {// 创建priority_queue,默认最大元素优先std::priority_queue<int> pq;// 插入元素pq.push(30);pq.push(10);pq.push(50);pq.push(20);// 大小std::cout << "优先队列大小: " << pq.size() << std::endl;// 访问最高优先级元素(最大元素)std::cout << "最高优先级元素: " << pq.top() << std::endl;// 遍历优先队列(需要弹出所有元素)std::cout << "元素(按优先级): ";while (!pq.empty()) {std::cout << pq.top() << " ";pq.pop();}std::cout << std::endl;// 最小元素优先的优先队列std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;min_pq.push(30);min_pq.push(10);min_pq.push(50);std::cout << "最小优先队列元素: ";while (!min_pq.empty()) {std::cout << min_pq.top() << " ";min_pq.pop();}std::cout << std::endl;return 0;
}
容器选择指南
选择合适的容器对程序性能至关重要,以下是一些选择建议:
- 随机访问需求:优先选择vector、deque、array
- 频繁在两端操作:选择deque、list
- 频繁在任意位置插入/删除:选择list、forward_list
- 需要键值对映射:
- 有序:map/multimap
- 无序且查找频繁:unordered_map/unordered_multimap
- 需要元素唯一且有序:set
- 需要元素唯一且无序:unordered_set
- 需要后进先出:stack
- 需要先进先出:queue
- 需要按优先级处理:priority_queue
- 元素数量固定:array
总结
C++标准库提供了丰富多样的容器,每种容器都有其独特的内部实现、优缺点和适用场景。熟练掌握这些容器的特性,能够帮助我们编写更高效、更简洁的代码。在实际开发中,应根据具体需求(如访问方式、插入删除频率、内存占用等)选择最合适的容器,以达到最佳的性能表现。