如何最快地更改std::map内部元素的键

85

我理解为什么不能只这样做(重新平衡等)的原因:

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
@sehe,我认为这不是数据结构的问题,如果我要创建自己的数据结构,最终得到的红黑树也会相同,唯一的区别是它将具有一种方法,该方法将“重用”节点而不是分配和重新分配。 - Peter Jankuliak
“1.遍历树三次(+平衡),而不是两次(+平衡)” - 应该是两次而不是一次...对于end(),不需要进行任何遍历。 - Tony Delroy
@TonyDelroy 我相信 operator[] 是第三个。 - luizfls
@luizfls:对于 find,有一个遍历(traversal)的过程,然后 erase(i) 以迭代器作为参数(而不是要删除的值),避免了另一个遍历,但最终的 insert 需要进行第二次遍历来找到新位置插入(因为当键改变时,它可能在一个无关的位置 - 如果你知道新插入应该非常靠近树中的旧插入,可以提供插入提示)。 - Tony Delroy
@TonyDelroy 真的,你是对的! - luizfls
7个回答

93

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" } }

35

你可以省略对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);
}

1
@Viktor,嗯,仔细想想,std::swap执行两个赋值操作,并引入一个临时值(默认情况下)。但是,如果(...) { m[22] = it->second; m.erase(it); }可能会更好。 - Peter Jankuliak
@Peter:如果复制或分配value_type是一个性能问题,那么应该专门为std::swap<value_type>进行特化,以比m[22] = it->second;中的单个赋值更加高效。 - aschepler
@Peter:如果你的value_type是一个简单类型,那么在这种情况下省略复制只是一种微小的优化。 - Viktor Sehr
@Viktor,没错,我是在考虑一般情况。 - Peter Jankuliak
@aschepler,我知道这只是一个小毛病,但我相信任何对std::swap的特化都必须执行一个冗余赋值回到它->second(为了正确)。无论如何,我同意Viktors最后的注释,这个额外的赋值是最不麻烦的。 - Peter Jankuliak

34

大约18个月前,我在此处提交了关于关联容器的算法提议:

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

查找标记为:“[2009-09-19 Howard adds:]”的评论。

当时,我们离FDIS太近,无法考虑这种变化。 然而,我认为它非常有用(您显然也同意),我想将其加入到TR2中。 也许您可以帮助找到并通知您的C ++国家机构代表,这是您想要看到的功能。

更新

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


3
如果您需要帮助确定您的NB代表,请随时与我直接联系。向其他人说明:如果仅有两个人支持此功能,则将其纳入标准的可能性非常小。如果您想要在有序和无序的关联容器中添加此功能,请积极游说!不要等到2016年,然后当您发现它不存在时再抱怨。现在就是行动的时候。 - Howard Hinnant
@HowardHinnant,嗯,你是想将这个转换为C++11吗?那C++14呢? - ThomasMcLeod
@ThomasMcLeod:尝试并失败了:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3645.pdf 或许C++17会有所改变...除非有一大批急切的C++程序员发起革命,否则这很难实现。Alan在N3645中做得非常出色,实际上已经接近成功,但还差一点。 - Howard Hinnant
1
@Dewfy: erase会销毁映射中的对象,并释放持有该对象的节点。emplace_hint会分配一个新节点,并从emplace_hint的参数中构造一个新对象。这些成员函数都是独立定义的,它们无法知道在您指定的组合中使用时的情况。这是关于此问题的最新提案,新版本将在几周内发布:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0083r1.pdf - Howard Hinnant
1
@LukasBarth:你说得完全正确!这就是最好的方法了。提取/插入循环将使用现有节点,避免所有的分配/释放(除非你的键重新分配需要)。 - Howard Hinnant
显示剩余4条评论

8

STL maps中的键必须是不可变的。

如果您的键值对中的键具有较高的不稳定性,则似乎使用不同的数据结构可能更合适。


5
你不能这样做。
正如你所注意到的,这是不可能的。地图被组织成这样,以便你可以有效地更改与键相关联的值,但反过来却不行。
你可以查看Boost.MultiIndex,特别是它的模拟标准容器部分。Boost.MultiIndex容器具有高效的更新功能。

1

你应该把分配留给分配器。 :-)

正如你所说,当键值改变时,可能会有很多重新平衡的操作。这就是树的工作方式。也许22是树中的第一个节点,33是最后一个节点?我们知道什么呢?

如果避免分配很重要,也许你应该尝试使用vector或deque?它们以更大的块进行分配,因此可以节省调用分配器的次数,但可能会浪费内存。所有容器都有它们的权衡,取决于你在每种情况下需要哪个主要优势(假设这真的很重要)。

对于冒险者:
如果你确定更改键不会影响顺序,并且你永远不会犯错,那么一点const_cast就可以让你更改键。


感谢您的输入,但问题并不是关于使用哪种数据结构。相反,问题是(如果对我的原始问题没有否定的答案):为什么在理论上似乎没有任何理由不应该有有效地执行它的API。伪算法很简单:找到节点;将其从树中分离;重新平衡;更改分离节点中的键;插入回去;重新平衡; - Peter Jankuliak
@Peter - 也许重新键入不是地图上的基本操作?我认为它还可以保存键而无需可分配,这将允许使用更多类型作为键。 - Bo Persson

-1

如果您知道新键对于地图位置是有效的(更改它不会改变排序),并且您不想删除和添加项目到地图中,您可以使用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));
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接