Какой самый быстрый способ изменить ключ элемента внутри std::map

#c #performance #dictionary #binary-tree #std

#c #Производительность #словарь #двоичное дерево #std

Вопрос:

Я понимаю причины, по которым нельзя просто сделать это (перебалансировка и прочее):

 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. Отсоедините if от дерева (не освобождайте)

  3. Перебалансировка

  4. Измените ключ внутри отдельного узла

  5. Вставьте узел обратно в дерево

  6. Перебалансировка

Комментарии:

1. Да, это неэффективно. Используйте другую структуру данных, если это не соответствует варианту использования

2. @sehe, я не думаю, что это проблема со структурой данных, если бы я собирался создать свою собственную, я бы в итоге получил то же красно-черное дерево с той лишь разницей, что у него был бы метод, который повторно использовал бы узел вместо выделения и перераспределения.

3. «1. обходит дерево три раза ( баланс) вместо дважды ( баланс)» — это дважды вместо одного раза… обход не требуется для end() .

4. @TonyDelroy Я считаю, что operator[] является третьим.

5. @luizfls: есть обход для find , затем erase(i) принимает итератор в качестве аргумента (не значение, подлежащее удалению), избегая другого обхода, но конечному insert действительно нужно выполнить второй обход, чтобы найти новое место для вставки (так как при изменении ключа он потенциально будет находиться в несвязанном положении — если вы знаете, что новая вставка должна быть очень близка в дереве к старой, вы можете предоставить подсказку о вставке).

Ответ №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 Говард добавляет: ].

В то время мы были слишком близки к FDIS, чтобы рассматривать это изменение. Однако я считаю это очень полезным (и вы, по-видимому, согласны), и я хотел бы перенести это в TR2. Возможно, вы могли бы помочь, найдя и уведомив своего представителя в Национальном органе C , что это функция, которую вы хотели бы видеть.

Обновить

Это не точно, но я думаю, что есть хороший шанс, что мы увидим эту функцию в C 17! 🙂

Комментарии:

1. Я попробую связаться с ним (не уверен, с чего начать, но уверен, что Google поможет). История, стоящая за моим вопросом, заключается в том, что я разрабатывал такую карту для компании, которая предпочитает не использовать внешние библиотеки (встроенные системы), и свойство RB trees, заключающееся в том, что они сохраняют свои элементы отсортированными, и быстрая смена ключа оказалась решающей в некоторых случаях. Я хотел сохранить API таким же, как в std :: map, но, к моему удивлению, не нашел ничего, что решило бы эту проблему. Итак, я попытаюсь реализовать то, что вы предложили по ссылке, спасибо.

2. Пожалуйста, не стесняйтесь обращаться ко мне напрямую, если вам нужна помощь в определении вашего представителя NB. Примечание для всех остальных: шансы включить это в стандарт сводятся к нулю, если это поддерживают только два человека. Если вы хотите, чтобы эта функциональность была добавлена в упорядоченные и неупорядоченные ассоциативные контейнеры, тогда лоббируйте это! Не ждите до 2016 года, а затем жалуйтесь, когда узнаете, что его там нет. Сейчас самое время действовать.

3. @HowardHinnant, эм, ты пытался перенести это в C 11? Как насчет C 14?

4. @Dewfy: erase уничтожает объект на карте и освобождает узел, содержащий этот объект. emplace_hint выделяет новый узел и создает новый объект в этом узле из аргументов emplace_hint . Каждая из этих функций-членов определена изолированно, и у них нет способа узнать, когда они используются в указанной вами комбинации. Вот последнее предложение по этому вопросу, и новая редакция выйдет через несколько недель: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0083r1.pdf

5. @LukasBarth: Вы совершенно правы! И это будет настолько хорошо, насколько возможно. Цикл извлечения / вставки будет использовать существующий узел, избегая любого выделения / освобождения (за исключением любого вашего переназначения ключа).

Ответ №3:

Вы можете опустить копирование значения;

 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); } , вероятно, подойдет.

2. @Peter: Если копирование или присвоение value_type является проблемой производительности, std::swap<value_type> должно быть специализированным, чтобы быть более эффективным, чем одиночное присвоение в m[22] = it->second; .

3. @Peter: если ваш value_type является таким простым типом, то исключение копирования является нано-оптимизацией в этом контексте.

4. @Viktor, верно, я думал в общем случае.

5. @aschepler, я знаю, что это просто придирчивый выбор, но я считаю, что любая специализация на std:: swap обязательно должна была бы выполнить (чтобы быть правильной) одно избыточное присвоение обратно ему-> второе. В любом случае, я согласен с последним замечанием Виктора в том, что этот дополнительный asssignment был наименее беспокоящим.

Ответ №4:

Ключи в STL maps должны быть неизменяемыми.

Похоже, что, возможно, другая структура данных или структуры могли бы иметь больше смысла, если у вас такая большая изменчивость на ключевой стороне ваших пар.

Ответ №5:

Вы не можете.

Как вы заметили, это невозможно. Карта организована таким образом, что вы можете эффективно изменять значение, связанное с ключом, но не наоборот.

Вы только взгляните на Boost.MultiIndex и, в частности, его Эмулирующие стандартные разделы контейнера. Повышение.Контейнеры с несколькими индексами обеспечивают эффективное обновление.

Ответ №6:

Вы должны оставить распределение распределителю. 🙂

Как вы говорите, при изменении ключа может потребоваться большая перебалансировка. Именно так работает дерево. Возможно, 22 — это первый узел в дереве, а 33 — последний? Что мы знаем?

Если важно избегать выделения, возможно, вам следует попробовать vector или deque? Они выделяют большими порциями, поэтому они экономят на количестве вызовов распределителя, но вместо этого потенциально тратят память. У всех контейнеров есть свои компромиссы, и вам решать, какой из них обладает основным преимуществом, которое вам нужно в каждом конкретном случае (при условии, что это вообще имеет значение).

Для любителей приключений:
Если вы точно знаете, что изменение ключа не влияет на порядок, и вы никогда, никогда не ошибетесь, небольшая const_cast позволила бы вам изменить ключ в любом случае.

Комментарии:

1. спасибо за ввод, но вопрос на самом деле не в том, какую структуру данных использовать. Скорее вопрос в том (если есть отрицательный ответ на мой первоначальный): почему нет API для эффективного выполнения этого, если теоретически кажется, что нет причин, по которым этого не должно быть. Псевдоалгоритм прост: найдите узел; отсоедините его от дерева; перебалансируйте; измените ключ в отсоединенном узле; вставьте обратно; перебалансируйте;

2. @Peter — Возможно, повторный ввод ключа не является фундаментальной операцией на карте? Я думаю, что это также избавляет ключ от необходимости назначать, что позволило бы использовать в качестве ключа несколько дополнительных типов.

Ответ №7:

Если вы знаете, что новый ключ действителен для положения на карте (его изменение не изменит порядок), и вы не хотите дополнительной работы по удалению и добавлению элемента на карту, вы можете использовать const_cast для изменения ключа, как в unsafeUpdateMapKeyInPlace приведенном ниже:

 template <typename K, typename V, typename It>
bool isMapPositionValidForKey (const std::map<K, V>amp; m, It it, K key)
{
    if (it != m.begin() amp;amp; 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>amp; m, typename std::map<K, V>::iteratoramp; it, K newKey)
{
    assert (isMapPositionValidForKey (m, it, newKey));
    const_cast<Kamp;> (it->first) = newKey;
}
  

Если вам нужно решение, которое изменяется на месте только тогда, когда это допустимо, и в противном случае изменяет структуру карты:

 template <typename K, typename V>
void updateMapKey (const std::map<K, V>amp; m, typename std::map<K, V>::iteratoramp; 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));
}