【问题标题】:Advantages and disadvantages of using a concatenated key rather than nested Map Container in C++在 C++ 中使用连接键而不是嵌套的 Map Container 的优缺点
【发布时间】:2014-03-26 21:23:49
【问题描述】:

我得到了一个项目来研究当前用于存储数据的容器的替代方案,以提高其效率。

当前的设计包含 4 个这样的嵌套地图 map< string, map< string, map< int, map< string, string> > > >

让我们将每个数据字段命名为CompanyDepartmentID_of_employeeName

目前检索给定CompanyDeptID 的员工姓名的时间复杂度为 O(log N) 和更准确地说,它涉及三个查找。

目前空间复杂度不是问题。

我最初的选择如下:

  • 使用嵌套对来表示CompanyDeptId,然后使用这个嵌套对作为映射的键。不过,这似乎不太容易阅读。
  • 我考虑使用tuplestruct 而不是嵌套对,这与我阅读的内容基本上没有什么不同。创建new struct EmployeeKey 后,将包含CompanyDeptID 的字段。我可以将它用作Keymap。 (我想我也必须编写自定义比较和小于运算符)。
  • 通过将int 转换为string 并将它们连接起来,使用company+Dept+ID 中的连接键。然后将此密钥提供给map<ConcatenatedKey, Data>
  • 使用Boost.MultiIndex。尽管这似乎是最好的选择,但我放弃了这个选择,因为我发现它有点复杂。

提供更多必要的信息。这个 Container 通常用于检索最终的嵌套数据,这就是我总结使用连接键方法的原因。我的问题基本上是,使用这种连接字符串有什么注意事项吗?这是一个糟糕的设计还是我们应该避免的?

据我了解,这将改善查找时间,仍然保持对数,但执行一次而不是四次查找,因此这似乎是一种改进。

【问题讨论】:

  • 如果您正在寻找实际性能,则字符串长度会发挥作用。还有什么是最重要的:插入、查找或删除?
  • 你让我们阅读这个广泛的问题,但学习multi index boost library 太复杂了?您可以尝试使用 unordered_map 对字符串进行哈希处理以提高性能
  • "据我了解,这将缩短查找时间,..." log(abc) = log(a) + log(b ) + 对数 (c)。只是说。
  • 如果您使用连接键,请确保以不会混淆子键的方式进行操作。例如,考虑“Department X5”中的员工“501”和“Department X”中的另一个员工“5501”的人为示例,他们恰好都被命名为“Jane Doe”。
  • 幸运的是,他们不会混淆,因为 ID 和 Dept 对长度有严格的规定。 @Zeta 你完全正确,这并不能提高性能...@captain giraffe 插入和查找是最重要的!

标签: c++ stl containers


【解决方案1】:

由于std::map<> 是一棵红黑树,它仍然是二叉树,因此查找速度与哈希图相比并没有那么快——尤其是在条目数很大的情况下。

使用std::unordered_map<> (a hashmap) 将提供更好的性能,假设散列分布良好。我推荐 fnv 或 MurmurHash3,因为它们的值分布最好。

现在,谈论嵌套容器 - 你不应该永远永远做这样的事情!整体性能可能非常糟糕,内存使用量肯定会非常大,因为它本质上是一个 4 维 RB-tree:

让我们把它放在上下文中,你有 20 个公司,每个公司有 5 个部门,每个部门有 12 个 EmployeeID,每个 EmployeeID 映射到 <Name, some_string> 的映射(最后一点似乎有点多余,不是吗?想想?)。

  1. 每个 Company 叶节点是一个 std::map => 20 个 std::map 实例
  2. 每个部门叶节点是一个 std::map => 20 + 20*5 = 120 个 std::map 实例
  3. 每个 EmployeeID 叶节点是一个 std::map => 120 + 20*5*12 = 1320 个 std::map 实例
  4. 每个名称叶节点是一个 std::map => 1320 + 20*5*12*1 = 2520 个 std::map 实例

所以你看,嵌套容器是非常危险的,因为即使只有一个小数据集,你最终也会得到大量的容器实例。这具有非常糟糕的性能,尤其是在对象被销毁或插入新元素时。

我的建议:使用 EmployeeKey 结构和 std::unordered_map。这将为您提供良好的查找速度,并且只有一个 std::unordered_map 实例。

struct EmployeeKey
{
    int         CompanyID;  // if you want speed, CompanyID is the way to go
    std::string Department;
    int         EmployeeID;
    std::string Name;

    inline bool operator==(const EmployeeKey& key) const {
        return CompanyID != key.CompanyID && ... /* etc */;
    }
};

template<> struct hash<EmployeeKey> {
    size_t operator()(const EmployeeKey& key) const {
        /* perform hash combine here */
    }
};

这应该足以让您入门。最终布局如下所示:

std::unordered_map<EmployeeKey, std::string> EmployeeData;
// usage:
auto it = EmployeeData.find(selectedEmployee);
if (it != EmployeeData.end())
    it->second = "Good employee";

如果您确实必须“加快”您的查找,请记住,如果 CompanyID 是 [0 .. N] 中的整数,则可以使用 std: :vector 快速获得正确公司的一级索引:

std::vector<std::unordered_map<EmployeeKey2, std::string>> EmployeeData;
// usage:
auto& companyMap = EmployeeData[EBuyNLargeCorp]; // or [selectedCompany(0..N)]
auto it = companyMap.find(selectedEmployee);
if (it != companyMap.end())
    it->second = "Good employee!";

其中 EmployeeKey2 将缺少 CompanyID 字段,而 selectedCompany 将是向量中的索引。但这只是您为获得真正关键的性能提升而做的事情。

【讨论】:

    【解决方案2】:

    您似乎忘记使用正确的工具来解决正确的问题。您尝试使用地图模拟数据库。更简单的解决方案是使用真正的数据库,SQLite3 易于集成,因为它与文件一起使用。

    您将能够以有效的方式查询大量不同的信息。您甚至可以使用外部工具调查数据库。

    如果你仍然不想使用数据库,可以将每个表想象成一个向量,id 是索引。最后一个表是 id 元组到值的映射,但我不建议这样做,因为它会更难获得不同类型的信息。

    下面是一个 DB 示例,说明我多年没有编写 SQL,可能有更好的设计,例如,您还可以添加一个表来为公司注册有效部门并添加约束以捕获无效注册员工到公司的一个失踪部门。

    SQL Fiddle

    SQLite (SQL.js) 架构设置

    CREATE TABLE Company(
         id integer primary key autoincrement, 
         name varchar(20) not null unique
    );
    
    INSERT INTO Company (name) values ("google");
    INSERT INTO Company (name) values ("facebook");
    
    CREATE TABLE Department(
         id integer primary key autoincrement, 
         name varchar(20) not null unique
    );
    
    INSERT INTO Department (name) values ( "research");
    INSERT INTO Department (name) values ( "development");
    INSERT INTO Department (name) values ( "marketing");
    INSERT INTO Department (name) values ( "hell");
    
    CREATE TABLE Employee
    (
         social_id integer primary key, 
         name varchar(20) not null
    );
    
    INSERT INTO Employee  values ( 1,"mark");
    INSERT INTO Employee  values ( 2,"john");
    INSERT INTO Employee  values ( 3,"david");
    
    CREATE TABLE Assigment(
         emp_id  primary key references Employee(social_id), /* employee have only one job */
         comp_id not null references Company(id), 
         dep_id  not null references Department(id)
    );
    
    INSERT INTO Assigment select 1,c.id,d.id from Company c join Department d where (c.name='google' and d.name='hell');
    INSERT INTO Assigment select 2,c.id,d.id from Company c join Department d where (c.name='google' and d.name='marketing');
    INSERT INTO Assigment select 3,c.id,d.id from Company c join Department d where (c.name='facebook' and d.name='research');
    

    查询 1

    SELECT c.name,d.name,e.name
        FROM assigment a JOIN company c ON a.comp_id=c.id
        JOIN department d ON d.id=dep_id
        JOIN employee e ON a.emp_id=social_id
    

    Results

    |     name |      name |  name |
    |----------|-----------|-------|
    |   google |      hell |  mark |
    |   google | marketing |  john |
    | facebook |  research | david |
    

    【讨论】:

    • 数据库如何回答这些查询?使用与此处讨论的方法和数据结构类似的方法和数据结构,但需要从磁盘中提取数据而不是使用内存中已有的数据。数据库可能是合适的,但不能仅仅基于问题中的描述。
    • 当然数据库不是什么神奇的东西,它们的内部结构远比串联一些BBT和一些哈希表要先进得多。它们抽象内部以提供更有用的服务(键唯一性约束、查询等),即使首先正确考虑您的表仍然很重要。如果你想强制数据库在内存中,也可以使用 sqlite。
    猜你喜欢
    • 2010-09-18
    • 2019-10-17
    • 1970-01-01
    • 2019-07-22
    • 2011-04-11
    • 1970-01-01
    • 2014-09-22
    • 2011-07-10
    • 2015-08-26
    相关资源
    最近更新 更多