c++用vector和map构造双重数据结构比较

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>>(嵌套关联容器)

优点
  • 键灵活性:支持任意整数键(包括稀疏或非连续键)。
    m[100][200] = "value"; // 直接插入稀疏键
    
  • 动态内存管理:无需预分配内存,内存按需分配。
  • 有序遍历:按键升序遍历,支持范围查询(如 lower_bound)。
    for (auto& outer_pair : m) {
        for (auto& inner_pair : outer_pair.second) {
            // 按键升序访问
        }
    }
    
缺点
  • 访问效率低:内外层访问时间复杂度均为 O(log n)(红黑树查找)。
  • 内存碎片化:内存非连续,缓存不友好,遍历速度慢。
  • 初始化复杂:需要逐层插入键值对。
    m[0].emplace(1, "a"); // 需要手动初始化内层 map
    

3. vector<map<int, string>>(混合结构:外层数组 + 内层映射)

优点
  • 外层连续访问:外层 vector 支持快速整数下标访问(O(1))。
  • 内层键灵活性:内层 map 支持稀疏或非连续键。
    v[0][100] = "a"; // 外层是连续下标,内层是任意整数键
    
  • 预分配外层内存:外层 vector 可预分配。
    v.reserve(100); // 预分配外层容量
    
缺点
  • 内层访问低效:内层 map 访问时间复杂度为 O(log n)
  • 内存混合模式:外层连续内存 + 内层碎片化内存,缓存局部性介于前两者之间。
  • 初始化稍复杂:外层 vector 需要预分配或填充默认 map
    vector<map<int, string>> v(10); // 初始化 10 个空 map
    

二、详细对比表格

特性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>> 的情况

  • 需求:密集的二维数据(如矩阵、表格),且键为连续整数。
  • 场景举例:图像像素存储、二维网格计算。
  • 优化建议
    vector<vector<string>> data;
    data.reserve(rows);          // 预分配外层
    for (auto& row : data) {
        row.reserve(cols);       // 预分配内层
    }
    

2. 优先选择 map<int, map<int, string>> 的情况

  • 需求:内外层键均为稀疏或非连续整数。
  • 场景举例:稀疏矩阵(如社交网络的邻接表)。
  • 优化建议
    // 使用 emplace 避免重复查找
    m[outer_key].emplace(inner_key, "value");
    

3. 优先选择 vector<map<int, string>> 的情况

  • 需求:外层键为连续整数,内层键稀疏。
  • 场景举例:学生成绩表(外层是班级编号,内层是学生ID)。
  • 优化建议
    vector<map<int, string>> classes(10); // 10 个班级
    classes[0][101] = "Alice";           // 班级 0,学号 101
    

四、总结

  • 密集数据 + 连续键:选择 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]
vv[0].push_back("num0_index0");            // vv[0][0]
vv[0].push_back("num0_index1");            // vv[0][1]
vv.push_back(std::vector<std::string>()); // 插入外层 vv[1]
vv[1].push_back("num1_index0");            // vv[1][0]
vv[1].push_back("num1_index1");            // vv[1][1]
特点
  • 优点:代码简单,无需提前知道数据规模。
  • 缺点
    • 多次调用 push_back 会导致 vector 动态扩容,可能触发多次内存分配和数据拷贝。
    • 访问未初始化的外层或内层元素时会导致未定义行为(如直接访问 vv[2][0])。

2. 预分配内存

代码示例
#include <vector>
#include <string>

// 定义与初始化分开
std::vector<std::vector<std::string>> vv;  // 定义

// 预分配内存(初始化结构)
vv.resize(2);              // 预分配外层大小为2
vv[0].resize(2);           // 预分配内层 vv[0] 大小为2
vv[1].resize(2);           // 预分配内层 vv[1] 大小为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];  // 插入外层键 0
mm[1];  // 插入外层键 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] 提前插入外层键。

示例代码总结

// vector<vector<string>>(预分配)
std::vector<std::vector<std::string>> vv;
vv.resize(2);
vv[0].resize(2);
vv[1].resize(2);
vv[0][0] = "num0_index0"; // 安全高效

// map<int, map<int, string>>(不预分配)
std::map<int, std::map<int, std::string>> mm;
mm[0][0] = "num0_index0"; // 灵活但稍慢
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值