c++构造一个双重数据结构,可以使用vector或者map,例如vector<vector >以及map<int, map<int, string> >,又或者是vector<map<int, string> >,分别比较这三者的优缺点。
一、三种结构的对比分析
1. vector<vector<string>>(二维动态数组)
优点
- 初始化简单:支持嵌套初始化列表(C++11+)。
vector<vector<string>> v = {{"a", "b"}, {"c", "d"}};
- 下标访问直观:通过连续整数下标访问,时间复杂度为 O(1)。
string val = v[0][1];
- 内存连续:内层
vector 的内存连续,缓存局部性好,遍历效率高。 - 预分配内存:支持外层和内层的
reserve,减少动态扩容开销。v.reserve(100);
v[0].reserve(100);
缺点
- 键限制:必须使用连续整数作为下标(如
0, 1, 2, ...),无法处理稀疏或非整数键。 - 内存冗余:若行或列稀疏(如大部分位置为空),内存浪费严重。
- 插入/删除中间元素低效:内层
vector 中间插入/删除的时间复杂度为 O(n)。
2. map<int, map<int, string>>(嵌套关联容器)
优点
缺点
- 访问效率低:内外层访问时间复杂度均为 O(log n)(红黑树查找)。
- 内存碎片化:内存非连续,缓存不友好,遍历速度慢。
- 初始化复杂:需要逐层插入键值对。
m[0].emplace(1, "a");
3. vector<map<int, string>>(混合结构:外层数组 + 内层映射)
优点
- 外层连续访问:外层
vector 支持快速整数下标访问(O(1))。 - 内层键灵活性:内层
map 支持稀疏或非连续键。v[0][100] = "a";
- 预分配外层内存:外层
vector 可预分配。v.reserve(100);
缺点
二、详细对比表格
| 特性 | vector<vector<string>> | map<int, map<int, string>> | vector<map<int, string>> |
|---|
| 初始化复杂度 | 简单(嵌套初始化列表) | 复杂(逐层插入键值对) | 中等(外层需预分配或填充默认值) |
| 键类型 | 连续整数 | 任意整数(支持稀疏) | 外层连续整数,内层任意整数 |
| 内存连续性 | 内外层均连续 | 内外层均不连续 | 外层连续,内层不连续 |
| 访问时间复杂度 | 外层 O(1),内层 O(1) | 外层 O(log n),内层 O(log n) | 外层 O(1),内层 O(log n) |
| 插入/删除效率 | 尾部高效,中间低效(O(n)) | 高效(O(log n)) | 外层尾部高效,内层高效 |
| 内存效率 | 高(密集数据) | 低(树节点额外开销) | 中等(外层连续,内层树节点) |
| 适用场景 | 密集二维数据(如矩阵) | 稀疏数据(如键范围大且随机) | 外层连续,内层稀疏(如行-列映射) |
三、实际使用建议
1. 优先选择 vector<vector<string>> 的情况
2. 优先选择 map<int, map<int, string>> 的情况
3. 优先选择 vector<map<int, string>> 的情况
四、总结
- 密集数据 + 连续键:选择
vector<vector<T>>,内存效率高,访问速度快。 - 稀疏数据 + 非连续键:选择
map<int, map<int, T>>,灵活但内存开销大。 - 混合场景(外层连续,内层稀疏):选择
vector<map<int, T>>,平衡内存和灵活性。
最终选择需根据 数据密度、键的连续性、访问模式 和 内存限制 综合权衡。
下面是代码示例:
一、使用 vector<vector<string>>
1. 不预分配内存
代码示例
#include <vector>
#include <string>
std::vector<std::vector<std::string>> vv;
vv.push_back(std::vector<std::string>());
vv[0].push_back("num0_index0");
vv[0].push_back("num0_index1");
vv.push_back(std::vector<std::string>());
vv[1].push_back("num1_index0");
vv[1].push_back("num1_index1");
特点
- 优点:代码简单,无需提前知道数据规模。
- 缺点:
- 多次调用
push_back 会导致 vector 动态扩容,可能触发多次内存分配和数据拷贝。 - 访问未初始化的外层或内层元素时会导致未定义行为(如直接访问
vv[2][0])。
2. 预分配内存
代码示例
#include <vector>
#include <string>
std::vector<std::vector<std::string>> vv;
vv.resize(2);
vv[0].resize(2);
vv[1].resize(2);
vv[0][0] = "num0_index0";
vv[0][1] = "num0_index1";
vv[1][0] = "num1_index0";
vv[1][1] = "num1_index1";
特点
- 优点:
- 内存一次性分配,避免多次扩容,提升性能。
- 支持直接通过下标访问(如
vv[0][0]),无需担心越界。
- 缺点:
- 需要提前知道数据规模。
- 若实际数据量小于预分配大小,可能浪费内存。
二、使用 map<int, map<int, string>>
1. 不预分配内存
代码示例
#include <map>
#include <string>
std::map<int, std::map<int, std::string>> mm;
mm[0][0] = "num0_index0";
mm[0][1] = "num0_index1";
mm[1][0] = "num1_index0";
mm[1][1] = "num1_index1";
特点
- 优点:
- 代码简洁,无需预分配内存。
- 支持稀疏键(如
mm[100][200]),内存按需分配。
- 缺点:
- 每次插入新键时,
map 内部需要调整红黑树结构,时间复杂度为 O(log n)。 - 内存不连续,遍历速度较慢。
2. 伪预分配内存(仅初始化结构)
代码示例
#include <map>
#include <string>
std::map<int, std::map<int, std::string>> mm;
mm[0];
mm[1];
mm[0][0] = "num0_index0";
mm[0][1] = "num0_index1";
mm[1][0] = "num1_index0";
mm[1][1] = "num1_index1";
特点
- 优点:外层键预先插入,避免后续插入时的重复查找。
- 缺点:
- 内层键仍按需分配,无法完全预分配内存。
- 对性能提升有限,
map 的内存分配本质仍是动态的。
三、两种方法的对比
1. vector<vector<string>> 的预分配 vs 不预分配
| 特性 | 不预分配内存 | 预分配内存 |
|---|
| 内存分配 | 多次动态分配(可能扩容) | 一次性分配 |
| 访问安全性 | 可能越界(需手动检查) | 安全(已预分配大小) |
| 代码复杂度 | 简单(逐元素插入) | 中等(需提前设置大小) |
| 适用场景 | 数据规模未知或动态增长 | 数据规模已知且固定 |
2. map<int, map<int, string>> 的伪预分配 vs 不预分配
| 特性 | 不预分配内存 | 伪预分配内存 |
|---|
| 内存分配 | 完全按需分配 | 仅外层键预插入 |
| 访问效率 | 每次插入需 O(log n) | 外层键插入后内层仍需 O(log n) |
| 代码复杂度 | 简单(直接赋值) | 稍复杂(多一步外层预插) |
| 适用场景 | 稀疏数据或键范围未知 | 外层键范围已知 |
四、实际建议
选择 vector<vector<string>> 的情况
- 数据密集且键连续(如矩阵、表格)。
- 需要快速随机访问(时间复杂度 O(1))。
- 预分配内存:已知数据规模时优先使用
resize。
选择 map<int, map<int, string>> 的情况
- 键稀疏或非连续(如外层键为 0、100、200)。
- 内存按需分配:数据规模未知或动态增长时更灵活。
- 伪预分配:仅在外层键范围明确时使用
mm[0] 提前插入外层键。
示例代码总结
std::vector<std::vector<std::string>> vv;
vv.resize(2);
vv[0].resize(2);
vv[1].resize(2);
vv[0][0] = "num0_index0";
std::map<int, std::map<int, std::string>> mm;
mm[0][0] = "num0_index0";