【问题标题】:What is the fastest way to change a key of an element inside std::map更改 std::map 中元素键的最快方法是什么
【发布时间】:2011-08-10 06:08:07
【问题描述】:

我理解为什么一个人不能这样做(再平衡和其他东西):

iterator i = m.find(33);

if (i != m.end())
  i->first = 22;

但到目前为止,更改键的唯一方法(我知道)是从树中删除节点,然后用不同的键插入值:

iterator i = m.find(33);

if (i != m.end())
{
  value = i->second;
  m.erase(i);
  m[22] = value;
}

这对我来说似乎相当低效,原因有很多:

  1. 遍历树三次(+平衡)而不是两次(+平衡)

  2. 另一个不必要的值副本

  3. 不必要的释放,然后重新分配树内的节点

我发现分配和释放是这三个中最差的。我错过了什么还是有更有效的方法来做到这一点?

我认为,理论上应该是可能的,所以我认为改变不同的数据结构是不合理的。这是我想到的伪算法:

  1. 在树中找到我要更改其键的节点。

  2. 如果从树中分离(不要释放)

  3. 再平衡

  4. 更改分离节点内的键

  5. 将节点重新插入树中

  6. 再平衡

【问题讨论】:

  • 是的,它效率低下。如果不适合用例,请使用不同的数据结构
  • @sehe,我不认为这是数据结构的问题,如果我要创建自己的,我最终会得到相同的红黑树,只有不同之处有一个方法可以重用节点而不是分配和重新分配。
  • @Chowlett,谢谢,我会记住的。
  • “1.遍历树三次(+平衡)而不是两次(+平衡)” - 它是两次而不是一次......end()不需要遍历。
  • @TonyDelroy 我相信 operator[] 是第三个。

标签: c++ performance dictionary binary-tree std


【解决方案1】:

在 C++17 中,新的 map::extract 函数可让您更改密钥。
示例:

std::map<int, std::string> m{ {10, "potato"}, {1, "banana"} };
auto nodeHandler = m.extract(10);
nodeHandler.key() = 2;
m.insert(std::move(nodeHandler)); // { { 1, "banana" }, { 2, "potato" } }

【讨论】:

    【解决方案2】:

    大约 18 个月前,我在这里提出了您的关联容器算法:

    http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#839

    查找标记为:[ 2009-09-19 Howard 添加:] 的评论。

    当时,我们离 FDIS 太近了,无法考虑这一变化。但是我认为它非常有用(你显然同意),我想把它放到 TR2 中。也许您可以通过查找并通知您的 C++ 国家机构代表这是您希望看到的功能来提供帮助。

    更新

    不确定,但我认为我们很有可能会在 C++17 中看到此功能! :-)

    【讨论】:

    • 我会尝试联系他(虽然不确定从哪里开始,但肯定 google 会提供帮助)。我的问题背后的故事是,我一直在为一家公司开发这样的地图——它不喜欢使用外部库(嵌入式系统)——而 RB 树的特性是它们保持其元素排序和快速键更改似乎是至关重要的在某些场合。我想保持与 std::map 中的 API 相同,但令我惊讶的是,没有发现任何可以解决这个问题的方法。因此,我将尝试实施您在链接中提出的建议,谢谢。
    • 如果您需要帮助确定您的 NB 代表,请随时直接与我联系。其他人请注意:如果只有两个人支持,将其纳入标准的可能性对我们不利。如果你想看到这个功能被添加到有序和无序的关联容器中,那就游说吧!不要等到 2016 年,当你发现它不存在时再抱怨。 现在是时候采取行动了。
    • @HowardHinnant,嗯,你是想把它引入 C++11 吗? C++14 呢?
    • @Dewfy: erase 破坏地图中的一个对象,并释放持有该对象的节点。 emplace_hint 分配一个新节点,并根据emplace_hint 的参数在该节点中构造一个新对象。这些成员函数中的每一个都是单独定义的,它们无法知道它们何时以您指定的组合使用。这是关于这个问题的最新提案,几周后将发布新版本:open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0083r1.pdf
    • @LukasBarth:你完全正确!这将尽可能好。提取/插入周期将使用现有节点,避免所有分配/解除分配(除了您的密钥重新分配可能做的任何事情)。
    【解决方案3】:

    value的复制可以省略;

    const int oldKey = 33;
    const int newKey = 22;
    const iterator it = m.find(oldKey);
    if (it != m.end()) {
      // Swap value from oldKey to newKey, note that a default constructed value 
      // is created by operator[] if 'm' does not contain newKey.
      std::swap(m[newKey], it->second);
      // Erase old key-value from map
      m.erase(it);
    }
    

    【讨论】:

    • @Viktor,嗯,不过,第二个 std::swap 做了两个赋值并引入了一个临时值(默认情况下)。但是 if (...) { m[22] = it->second; m.erase(它); } 可能会。
    • @Peter:如果复制或分配value_type 是一个性能问题,那么std::swap&lt;value_type&gt; 应该被专门化为比m[22] = it-&gt;second; 中的单个分配高效。
    • @Peter:如果你的 value_type 是一个如此简单的类型,那么在这种情况下省略复制是一种纳米优化。
    • @Viktor,真的,我在考虑一般情况。
    • @aschepler,我知道这只是一个挑剔的选择,但我相信任何对 std::swap 的专业化都必须(正确地)做一个冗余分配给它->second .无论如何,我同意 Viktors 最后一点,因为这个额外的任务是最不麻烦的。
    【解决方案4】:

    STL 映射中的键必须是不可变的。

    如果您在配对的关键方面存在如此大的波动性,似乎不同的数据结构或结构可能更有意义。

    【讨论】:

      【解决方案5】:

      你不能。

      如您所见,这是不可能的。地图的组织方式使您可以有效地更改与键关联的值,但不能反过来。

      您可以查看 Boost.MultiIndex,尤其是它的 Emulating Standard Container sections。 Boost.MultiIndex 容器具有高效的更新功能。

      【讨论】:

        【解决方案6】:

        您应该将分配留给分配器。 :-)

        正如您所说,当密钥发生变化时,可能会有很多重新平衡。这就是树的工作方式。也许 22 是树中的第一个节点,而 33 是​​最后一个?我们知道什么?

        如果避免分配很重要,也许您应该尝试使用向量或双端队列?它们分配更大的块,因此它们节省了对分配器的调用次数,但可能会浪费内存。所有容器都有其权衡取舍,由您决定哪一个具有您在每种情况下所需的主要优势(假设它很重要)。

        对于喜欢冒险的人:
        如果您确定更改密钥不会影响顺序并且您永远不会犯错误,那么一点 const_cast 无论如何都会让您更改密钥。

        【讨论】:

        • 感谢您的输入,但问题不在于使用哪种数据结构。而是问题是(如果对我原来的答案有否定的答案):如果理论上似乎没有理由不应该有,为什么没有 API 可以有效地做到这一点。伪算法很简单:找到节点;把它从树上拆下来;再平衡;更改分离节点中的密钥;插回;再平衡;
        • @Peter - 也许重新键入不是地图上的基本操作?我认为它还避免了键必须是可分配的,这将允许使用一些额外的类型作为键。
        【解决方案7】:

        如果您知道新键对地图位置有效(更改它不会更改顺序),并且您不希望将项目删除和添加到地图中的额外工作,您可以使用const_cast 更改密钥,如下面的unsafeUpdateMapKeyInPlace

        template <typename K, typename V, typename It>
        bool isMapPositionValidForKey (const std::map<K, V>& m, It it, K key)
        {
            if (it != m.begin() && std::prev (it)->first >= key)
                return false;
            ++it;
            return it == m.end() || it->first > key;
        }
        
        // Only for use when the key update doesn't change the map ordering
        // (it is still greater than the previous key and lower than the next key).
        template <typename K, typename V>
        void unsafeUpdateMapKeyInPlace (const std::map<K, V>& m, typename std::map<K, V>::iterator& it, K newKey)
        {
            assert (isMapPositionValidForKey (m, it, newKey));
            const_cast<K&> (it->first) = newKey;
        }
        

        如果您想要一个仅在有效时就地更改的解决方案,否则会更改地图结构:

        template <typename K, typename V>
        void updateMapKey (const std::map<K, V>& m, typename std::map<K, V>::iterator& it, K newKey)
        {
            if (isMapPositionValidForKey (m, it, newKey))
            {
                unsafeUpdateMapKeyInPlace (m, it, newKey);
                return;
            }
            auto next = std::next (it);
            auto node = m.extract (it);
            node.key() = newKey;
            m.insert (next, std::move (node));
        }
        

        【讨论】:

          猜你喜欢
          • 2019-06-30
          • 2018-12-19
          • 2023-01-12
          • 2010-11-23
          • 1970-01-01
          • 1970-01-01
          • 2011-04-02
          • 2016-12-20
          • 1970-01-01
          相关资源
          最近更新 更多