Цикл for с диапазоном
Самый простой, удобный для восприятия способ пройтись по ассоциативному массиву на C++ — цикл for
с диапазоном:
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
for (const auto& pair : age_map) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
}
return 0;
}
Это чистый и понятный способ. Функцией const auto&
не создается лишних копий пар ключ-значение.
Итераторы
Для большего контроля над итерационным процессом применяются итераторы:
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
for (auto it = age_map.begin(); it != age_map.end(); ++it) {
std::cout << it->first << " is " << it->second << " years old." << std::endl;
}
return 0;
}
С ними появляется больше гибкости, например возможность начать с того или иного места в массиве или пройтись в обратном порядке.
Структурированные привязки
Чтобы воспользоваться более лаконичным синтаксисом, прибегают к структурированным привязкам версии C++17 и новее:
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
for (const auto& [name, age] : age_map) {
std::cout << name << " is " << age << " years old." << std::endl;
}
return 0;
}
Это четкий и удобный для восприятия человека способ напрямую добраться до ключа и значения.
Обход с помощью std::for_each
Любители и профессионалы функционального программирования прибегают к std::for_each
:
#include <iostream>
#include <map>
#include <algorithm>
int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
std::for_each(age_map.begin(), age_map.end(), [](const auto& pair) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
});
return 0;
}
Здесь к каждому элементу ассоциативного массива применяется функция.
Изменение значений при обходе
Чтобы изменять значения во время обхода ассоциативного массива, используется неконстантная ссылка:
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
for (auto& pair : age_map) {
pair.second += 1; // Каждому значению возраста добавляется «1»
}
// Выводится обновленный массив
for (const auto& pair : age_map) {
std::cout << pair.first << " is now " << pair.second << " years old." << std::endl;
}
return 0;
}
Внимание: изменяются значения, но не ключи, поскольку ключи ассоциативного массива — константы.
Реальный сценарий: счетчик частотности слов
Рассмотрим обход ассоциативного массива на практике:
#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <algorithm>
std::map<std::string, int> countWordFrequency(const std::string& text) {
std::map<std::string, int> frequency;
std::istringstream iss(text);
std::string word;
while (iss >> word) {
// Преобразуется в нижний регистр
std::transform(word.begin(), word.end(), word.begin(),
[](unsigned char c){ return std::tolower(c); });
// Убирается пунктуация
word.erase(std::remove_if(word.begin(), word.end(), ::ispunct), word.end());
// Добавляется частотность
++frequency[word];
}
return frequency;
}
void printWordFrequency(const std::map<std::string, int>& frequency) {
for (const auto& [word, count] : frequency) {
std::cout << word << ": " << count << std::endl;
}
}
int main() {
std::string text = "The quick brown fox jumps over the lazy dog. "
"The dog barks, and the fox runs away.";
auto frequency = countWordFrequency(text);
printWordFrequency(frequency);
return 0;
}
В этом примере демонстрируется, как ассоциативный массив проходится в задачах анализа текста.
Производительность
Итеративный обход больших ассоциативных массивов сказывается на производительности. Сравним итеративные техники:
#include <iostream>
#include <map>
#include <chrono>
#include <string>
const int MAP_SIZE = 1000000;
std::map<int, std::string> createLargeMap() {
std::map<int, std::string> large_map;
for (int i = 0; i < MAP_SIZE; ++i) {
large_map[i] = "Value" + std::to_string(i);
}
return large_map;
}
void benchmarkIteration(const std::string& method, const std::map<int, std::string>& m,
void (*iterFunc)(const std::map<int, std::string>&)) {
auto start = std::chrono::high_resolution_clock::now();
iterFunc(m);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << method << " took " << diff.count() << " seconds" << std::endl;
}
void rangeBasedForLoop(const std::map<int, std::string>& m) {
for (const auto& pair : m) {
// С «pair.first» и «pair.second» что-то делается
volatile auto x = pair.second.length();
}
}
void iteratorBasedLoop(const std::map<int, std::string>& m) {
for (auto it = m.begin(); it != m.end(); ++it) {
// С «it->first» и «it->second» что-то делается
volatile auto x = it->second.length();
}
}
void forEachLoop(const std::map<int, std::string>& m) {
std::for_each(m.begin(), m.end(), [](const auto& pair) {
// С «pair.first» и «pair.second» что-то делается
volatile auto x = pair.second.length();
});
}
int main() {
auto large_map = createLargeMap();
benchmarkIteration("Range-based for loop", large_map, rangeBasedForLoop);
benchmarkIteration("Iterator-based loop", large_map, iteratorBasedLoop);
benchmarkIteration("std::for_each", large_map, forEachLoop);
return 0;
}
В этом тесте сравнивается способы итеративного прохождения большого ассоциативного массива. Запустите тест у себя в системе и сравните, какой из них эффективнее.
Обход ассоциативного контейнера
Ассоциативные контейнеры, у которых на один ключ бывает по нескольку значений, проходятся по-другому:
#include <iostream>
#include <map>
int main() {
std::multimap<std::string, int> scores = {
{"Alice", 95},
{"Bob", 80},
{"Alice", 92},
{"Charlie", 88},
{"Bob", 85}
};
std::string current_name = "";
for (const auto& [name, score] : scores) {
if (name != current_name) {
if (!current_name.empty()) std::cout << std::endl;
std::cout << name << "'s scores: ";
current_name = name;
}
std::cout << score << " ";
}
std::cout << std::endl;
return 0;
}
В этом примере демонстрируется обход ассоциативного контейнера, когда на каждый ключ приходится несколько значений.
Типичные ошибки и как их избежать
- Изменение ключей при обходе. Ключи ассоциативного массива неизменны, это константы. Чтобы их изменить, элемент придется удалить и вставить повторно.
- Инвалидация итератора из-за добавления или удаления элементов. Будьте осторожны, изменяя ассоциативный массив при его обходе.
- Применение оператора
[]
в контекстахconst
. Если ключа нет, этим оператором вставляется новый элемент. При доступе только для чтения используйтеat()
илиfind()
. - Не учитывается, что ассоциативные массивы упорядочены, а стандартные упорядочены по ключам. Не нужна упорядоченность? Тогда для повышения производительности воспользуйтесь
std::unordered_map
. - Игнорирование стоимости доступа к элементам ассоциативного массива. Доступ к ним зависит от размера массива. Нужен частый доступ по индексу? Воспользуйтесь другим контейнером.
Заключение
В C++ ассоциативный массив проходится по-разному, в зависимости от конкретных сценариев.
Понимание этих способов — от простого цикла for
с диапазоном до гибких итераторов — важно для эффективного программирования на C++.
Выбирая способ итеративного прохождения, учитывайте конкретные требования своих задач. Нужно ли менять значения при обходе? Работаете ли вы с большим набором данных, где важна производительность, и в версии C++17 или новее с возможностью воспользоваться структурированными привязками?
Читайте также:
- C++: практическое руководство по множественным конструкторам
- Создание общей библиотеки Linux
- Как выводятся векторы на C++
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: How to Iterate Through a Map in C++