【问题标题】:How to make a vector of unique path names memory-effectively如何有效地制作唯一路径名的向量
【发布时间】:2012-09-06 08:16:22
【问题描述】:

我有一个路径名列表,其中一些标有“包含子树”标志。 我需要迭代所有路径,包括子树,但每个唯一路径只需要一次。

所以,如果我有这样的目录树:

C:\Models\
C:\Models\A
C:\Models\A\1
C:\Models\B
C:\Models\B\1

和 2 个带有子树的选择路径(输入):

{"C:\Models\", true}
{"C:\Models\A", true}

迭代时需要避免以下路径重复:

C:\Models\
C:\Models\A
C:\Models\A\1
C:\Models\B
C:\Models\B\1
C:\Models\A   *** Duplicate ***
C:\Models\A\1 *** Duplicate ***

我决定使用vector + set:

std::vector<std::string> vecPaths;       // For iterating
std::set<std::string>    setUniquePaths; // For duplicates check

但这是记忆无效的决定,因为每条路径将被呈现 2 次 1) 在向量中和 2) 在集合中。

如何在不复制这些字符串的情况下提供字符串唯一性?

关于任务定义的注意事项:

  • INPUT 是一对序列 {string path, bool includeSubtre}
  • OUTPUT 是一个路径向量,一个快照,用于未来的迭代。

【问题讨论】:

  • 为此类事情使用设置通常是不错的决定。此外,您可以排序然后删除重复。 std::list::sort + std::list::unique,或std::sort + std::unique
  • 也许是Trie?例如code.google.com/p/patl
  • 向量是否已经包含所有路径并且您只想对其进行迭代,或者您想在迭代时填充向量(使用某种文件系统迭代器)?在后一种情况下,只需删除向量并单独使用集合。在前一种情况下,请保留您的向量并使用 std::set&lt;std::vector&lt;std::string&gt;::iterator&gt; 重复。
  • 您可以遍历set,所以甚至有理由首先使用vector

标签: c++ performance stl


【解决方案1】:

到目前为止,cmets 主要关注内存中的数据结构,但重要的是要记住目录遍历非常受 IO 限制。给定输入

{"C:\Models\", true}
{"C:\Models\A", true}

您想跳过整个第二个目录遍历。因此,您不想在最后消除重复项。再举一个例子,给定

{"C:\Models\A", true}
{"C:\Models\", true}

您想在第二次枚举期间跳过 A 子树。

因此,在所有已知路径名中使用 两个 std::set&lt;std::string&gt;,一个用于非递归枚举目录,一个用于您已枚举的目录。在递归枚举期间,跳过第二组中已经存在的任何子树。在输入结束时,您可以简单地合并这两个集合。

【讨论】:

    【解决方案2】:

    如果您只是要迭代 std::vector&lt;std::string&gt; 而不添加或删除元素(在单次迭代期间),那么为什么不使用一组指针/迭代器来复制重复项:

    std::vector<std::string> subtreePaths = //the ones you want to iterate for
    ...
    std::set<std::vector<std::string>::const_iterator> setUniquePaths;
    for(auto iterS=subtreePaths.begin(); iterS!=subtreePaths.end(); ++iterS)
        for(auto iterP=vecPaths.begin(); iterP!=vecPaths.end(); ++iterP)
            if(matches(*iterP, *iterS) && setUniquePaths.insert(iterP).second)
                std::cout << *iterP << std::endl;    //or whatever
    

    (当然,auto 是 C++11,请随意将其替换为符合 C++98/03 的相应迭代器类型)。

    但也许我误解了你真正想要实现的目标。


    编辑:如果您还没有在 vecPaths 向量中拥有所有现有路径,并且您实际上想以某种方式迭代您的真实文件系统,并使用所有找到(和重复清理)路径,那么我上面的方法当然是垃圾,因为它假设对所有路径的已知向量仅进行基于字符串的迭代。

    但如果是这种情况,您可以完全删除向量并使用单个 std::set&lt;std::string&gt; 来收集您遇到的所有路径(现在自动唯一)。不需要额外的向量。

    【讨论】:

    • 如果我完全删除向量,设置是否会按字母顺序迭代?
    • 我最好只使用set。它确实按字母顺序迭代source
    • @DenisPavlyukov 是的,std::set 保证被排序(通常是二叉搜索树),与 C++11 的 std::unordered_set(通常是哈希表)相反。但是您必须注意,顺序是由字符串的底层字符类型决定的,这很可能会将所有小写字符排在所有大写字符之前。当然,您可以通过向 std::set 提供自定义排序谓词来覆盖此顺序,默认情况下它只是 &lt; 运算符。
    【解决方案3】:

    如果有很多条目,我会构建一个包含所有唯一目录名称(而不是路径)的向量,并使用树(例如 http://tree.phi-sci.com/)通过一系列 ID 将所有看到的路径存储到向量中.要确定是否已经看到现有目录,请使用哈希映射为当前路径中的每个目录名称构建 ID 序列。如果路径完全匹配,请跳过它。如果没有,则将相关节点添加到树中以反映新路径。注意:这可能会导致树中的多个节点引用同一个 ID。

    代码如下:

    std::vector< std::string > directories; // THIS IS THE INPUT!
    std::vector< std::string > directory_names;
    std::unordered_map< std::string, size_t > name_to_id_map;
    tree< size_t > directory_paths;
    for (auto idir = directories.begin(); idir != directories.end(); ++idir) {
        // Convert directories to a sequence of IDs (if new names are found, add
        // them to 'directory_names' and 'name_to_id_map'.  This is pretty mechanical code.
        std::vector< size_t > id_sequence = convert( *idir );
    
        // Walk the tree looking for this ID sequence.
        tree<size_t>::sibling_iterator current_tree_node;
        bool found = true;
        for (auto iid = id_sequence.begin(); iid != id_sequence.end(); ++iid) {
           if ( found ) {
              if ( !directory_paths.is_valid( current_tree_node ) ) {
                 // Find a match among the roots of the tree.  Note: There might be a more elegant way to do this.
                 tree<size_t>::sibling_iterator iroot( directory_paths.head );
                 tree<size_t>::sibling_iterator iroot_end( directory_paths.feet );
                 ++iroot_end;
    
                 // Note: If the tree is sorted, we can use equal_range!
                 current_tree_node = std::find( iroot, iroot_end, *iid );
                 found = ( current_tree_node != iroot_end );
              }
              else {
                 // Find a match among the siblings of 'current_tree_node'.
                 tree<size_t>::sibling_iterator ichild = directory_paths.begin_child( current_tree_node );
                 tree<size_t>::sibling_iterator ichild_end = directory_paths.end_child( current_tree_node );
    
                 // Note: If the tree is sorted, we can use equal_range!
                 current_tree_node = std::find( ichild, ichild_end, *iid );
                 found = ( current_tree_node != ichild_end );
              }
           }
    
           if ( !found ) {
              // Add this node to the tree as a child of current_tree_node.
              if ( directory_paths.is_valid( current_tree_node ) ) {
                 current_tree_node = directory_paths.insert_after( current_tree_node, *iid );
              }
              else if ( !directory_paths.empty() ) {
                 current_tree_node = directory_paths.insert_after( directory_paths.feet, *iid );
              }
              else {
                 current_tree_node = directory_paths.set_head( *iid );
              }
           }
        }
    
        if ( !found ) {
           // This directory path (i.e. *idir) has not been seen before.
           ...
        }
     }
    

    例如,以下输入将创建 5 个唯一名称(C:、Models、A、1、B)。

    C:\Models\
    C:\Models\A
    C:\Models\A\1
    C:\Models\B
    C:\Models\B\1
    

    处理完第一行后,树将有两个节点。 处理完第二行后,树将具有三个节点。 处理完第 3 行后,树将有四个节点。 处理完第 4 行后,树将有五个节点。 处理完第 5 行后,树将有六个节点。

    如果我碰巧遇到:C:\Models\1\B,则不会向“directory_names”(或“name_to_id_map”)添加新条目,但树现在将有八个节点。

    我相信这个实现非常节省内存,因为 1) directory_names 只存储子字符串,而不是完整路径,以及 2) 永远不会为共享同一路径的一部分的两个目录创建多个字符串。本质上,在处理每个新目录时,只存储有关名称和路径的唯一信息(不包括 'name_to_id_map' 的开销,这对于实现适当的运行时与内存平衡似乎很重要)。

    注意:我不太明白您所说的“和 2 个带有子树(输入)的选择路径”是什么意思。

    【讨论】:

    • 我的意思是作为输入你有整个文件系统+目录列表,其中一些被标记为继续子树。这是我们允许用户选择目录列表的方式。
    • 对这个解决方案是否可行有任何想法吗?我认为它接近表示目录树的最低理论内存要求。通常,只有当输入集非常大时,人们才会担心记忆的有效性。
    • 你是对的,在对内存产生任何恐慌之前分析输入数据量会更好。事实证明,在我的情况下,我不会有超过 1000 个输入路径名。顺便说一句,我会试试你的决定。
    【解决方案4】:

    您可以使用boost::variant 来存储目录节点。每个节点要么是一个文件,要么是一个目录:

    typedef boost::variant<
          std::string
        , std::map<std::string, boost::recursive_variant_>
        > Tree;
    

    map 确保目录中没有重复名称。

    您可能需要添加函数来填充和遍历这个递归数据结构。

    【讨论】:

    • 它可能会工作,但我不能使用boost,我不知道boost::recursive_variant是什么
    【解决方案5】:

    使用 shared_ptr。但是字符串可以实现为 COW,因此无需担心副本。

    【讨论】:

    • std::string 不再是 COW,因为 C++11。 COW 和多线程不能相处。
    • 同意@MSalters,我不能依赖懒惰的复制。
    • @PropellerMan,我不能在我的项目中使用 boost。但如果可以,std::set&lt;shared_ptr&lt;std::string&gt;&gt; 将如何计算字符串的哈希码?
    • @DenisPavlyukov:std::set&lt;T&gt; 不使用哈希码。它依赖于std::less&lt;T&gt;,对于T==shared_ptr&lt;string&gt;,它不是字母顺序而是内存顺序。
    • @DenisPavlyukov:这是常见的实现。更具体地说,通常它是一棵红黑树。不过,就标准而言,任何将std::set 排序在指定复杂性范围内的东西都可以。 W.r.t.字母顺序,这有点棘手,因为一些非英语语言有一些不同的规则。例如。 std::string 会将“ae”排在“ad”之后,但在德语中则相反(因为“ae”等同于“ä”)
    猜你喜欢
    • 2020-12-06
    • 2018-11-20
    • 1970-01-01
    • 1970-01-01
    • 2014-05-05
    • 2021-05-27
    • 2012-12-05
    • 2019-12-11
    • 2016-09-25
    相关资源
    最近更新 更多