Skip to content

Latest commit

 

History

History
152 lines (123 loc) · 8.64 KB

lecture5.md

File metadata and controls

152 lines (123 loc) · 8.64 KB

Лекция 5. Ассоциативные контейнеры

Итерация по контейнерам

Напоминаем, что есть несколько способа проитерироваться по контейнерам:

  1. завести индекс, а после инкрементировать/декрементировать его
    for (size_t i = 0; i < number.size(); ++i) {
        if (number[i] % 2) {
            number[i] *= 2;
        }
        std::cout << number[i] << std::endl;
    }
  1. Завести итератор, а после инкрементировать/декрементировать его в for
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        if (*it % 2) {
            *it *= 2;
        }
        std::cout << *it << std::endl;
    }
  1. range-based for
    for (auto& number : numbers) {
        if (number % 2) {
            number *= 2;
        }
        std::cout << number << std::endl;
    }

В чем различия:

  1. В некоторых ситуациях у нас будут повторятся одинаковые куски кода (a[i]), что нарушает правило "don't repeat yourself".
  2. Можно перепутать данный итератор с другим, но в целом код уже более читаемый.
  3. Код лишен недостатков выше, мы можем работать уже с данным элементом. Код эквивалентен второму варианту, если обозначить auto& i = *it. Получается, что мы можем позвать range-based for от любого объекта, у которого есть методы begin() и end(), которые возвращают "итератор" (есть *, инкрементирование и !=). К сожалению, для более универсального кода, работающего с любым контейнером, придется использовать только второй или третий вариант (т.к. может не быть оператора []). Если хотите использовать контейнер с произвольным доступом, то лучше использовать 2-ой вариант.

Лучше передавать auto по ссылке, но для int это некритично.


Функция std::advance() перемещает итератор, передаваемый ей в качестве аргумента. При этом смещение может производиться в прямом (или обратном) направлении сразу на несколько элементов. Функция работает с большинством операторов, которые могут позволять такие смещения.

void std::advance (InputIterator& pos, Dist n)

Ассоциативные контейнеры

Есть два типа контейнеров:

  1. set - множество элементов

  2. 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?

  1. с помощью конструктора.
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); 

Память в unordered

Контейнеры устроены на основе hash map и берется остаток от N (количество bucket-ов). От объекта вычисляется hash, благодаря которому объект записывается в ячейку (в bucket). Если хэши у ключей совпадают, то в одной ячейке будет находиться несколько объектов, и прямым проходом с помощью == unordered определяет, какой именно элемент нам нужен. Если становится высока вероятность коллизий, то происходит перехэширование: меняется N и ключи раскладываются по-новому.

У большинства стандартных типов определен hash. Если же мы пишем свой класс, то нужно написать свой 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 передадим ключ, то удалятся все элементы с таким ключом. Если передадим итератор, то удалится только данный элемент, а метод вернет итератор на следующий за ним элемент.