Напоминаем, что есть несколько способа проитерироваться по контейнерам:
- завести индекс, а после инкрементировать/декрементировать его
for (size_t i = 0; i < number.size(); ++i) {
if (number[i] % 2) {
number[i] *= 2;
}
std::cout << number[i] << std::endl;
}
- Завести итератор, а после инкрементировать/декрементировать его в for
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
if (*it % 2) {
*it *= 2;
}
std::cout << *it << std::endl;
}
- range-based for
for (auto& number : numbers) {
if (number % 2) {
number *= 2;
}
std::cout << number << std::endl;
}
В чем различия:
- В некоторых ситуациях у нас будут повторятся одинаковые куски кода (a[i]), что нарушает правило "don't repeat yourself".
- Можно перепутать данный итератор с другим, но в целом код уже более читаемый.
- Код лишен недостатков выше, мы можем работать уже с данным элементом. Код эквивалентен второму варианту, если обозначить auto& i = *it. Получается, что мы можем позвать range-based for от любого объекта, у которого есть методы begin() и end(), которые возвращают "итератор" (есть *, инкрементирование и !=). К сожалению, для более универсального кода, работающего с любым контейнером, придется использовать только второй или третий вариант (т.к. может не быть оператора []). Если хотите использовать контейнер с произвольным доступом, то лучше использовать 2-ой вариант.
Лучше передавать auto по ссылке, но для int это некритично.
Функция std::advance() перемещает итератор, передаваемый ей в качестве аргумента. При этом смещение может производиться в прямом (или обратном) направлении сразу на несколько элементов. Функция работает с большинством операторов, которые могут позволять такие смещения.
void std::advance (InputIterator& pos, Dist n)
Есть два типа контейнеров:
-
set - множество элементов
-
map - "словарь", множество элементов (ключей), сопоставленных другому множеству значений
На ключах должен быть определен частичный порядок (<). У unordered контейнеров это определять не нужно.
std::map<std::string, size_t> word_count;
for (const auto& word : text) {
++word_count[word];
}
Разберем контейнеры на примере map.
У контейнера map есть оператор []
, возвращающий значение. Если мы изменим значение по ссылке, то оно изменится и в контейнере.
Если ключа нет, то оператор [] создает элемент с дефолтным значением по этому ключу и возвращает его.
Алгоритмическая сложность операций с контейнерами (обращение по ключу и т.д.) составляет O(logn)
.
word_count["a"] += 1; // word_count["a"] теперь равно единице (дефолтное значение - 0)
Есть синтаксис, позволяющий более наглядно обращаться к элементам map, называющийся structure-binding
for (const auto& [key, value] : text) {
std::cout << key << " " << value << std::endl;
}
У std::map есть метод find
, который ищет, есть ли данный ключ в map.
for (const auto& word : text) {
auto it = word_count.find(word);
if (word_count.end() != *it) {
/// auto& [key, value] = *it;
// ++value;
++(it->second); // обращение к полю указателя
}
}
У map есть метод at, который похож на [], не создающий новый элемент в map. Если элемента нет в map, то выбрасывается исключение. Преимущество его в том, что его можно использовать в константном контексте, т.к. это константный метод.
for (const auto& word : text) {
auto it = word_count.find(word);
if (word_count.end() != it) {
word_count.at(word);
}
}
Как проинициализировать const map?
- с помощью конструктора.
const std::map<std::string, size_t> word_count{ {"a", 1}, {"b", 2}};
Метод insert принимает пару, и вставляет {key, value} в map. Если в map уже есть данный элемент, то он не вставляет пару в мап. Возвращает этот метод пару {вставленный/существующий элемент, bool = была ли вставлена пара}.
word_count.insert(std::make_pair("a", 1)); // возвратит { {"a", 1}, true}, если в word_count не было такого ключа
Метод merge
смешивает два map'а между собой. Если ключи совпадают, то оставляет пару map'а слева.
word_count1.merge(word_count2);
Контейнеры устроены на основе hash map и берется остаток от N (количество bucket-ов). От объекта вычисляется hash, благодаря которому объект записывается в ячейку (в bucket). Если хэши у ключей совпадают, то в одной ячейке будет находиться несколько объектов, и прямым проходом с помощью == unordered определяет, какой именно элемент нам нужен. Если становится высока вероятность коллизий, то происходит перехэширование: меняется N и ключи раскладываются по-новому.
У большинства стандартных типов определен hash. Если же мы пишем свой класс, то нужно написать свой hash и оператор ==.
struct Student {
std::string name;
bool operator == (const Student& other) const {
return name == other.name;
}
}
struct StudentHash {
size_t operator()(const Student& student) const {
return std::hash<std::string> hash()(student.name);
}
}
std::unordered_map<Student, size_t, StudentHash> student_map;
Вместо оператора == можно определить функцию _Pred, которая будет сравнивать два элемента (пишется после hash).
Если мы хотим хранить несколько одинаковых ключей, то можно использовать multiset и multimap. Методы аналогичны.
Но в таком случае, если в метод erase передадим ключ, то удалятся все элементы с таким ключом. Если передадим итератор, то удалится только данный элемент, а метод вернет итератор на следующий за ним элемент.