Компараторы и лямбды
Как отсортировать не так, как по умолчанию. Синтаксис лямбд, захват переменных и требование, нарушение которого роняет sort.
5 мин
Сортировка по умолчанию идёт по возрастанию. Всё остальное — по убыванию, по второму полю, по модулю, по частоте — задаётся компаратором.
Лямбда за минуту
Лямбда — функция, записанная прямо на месте использования.
sort(a.begin(), a.end(), [](int x, int y) { return x > y; });
Разбор по частям: [] — что захватить из окружения, (int x, int y) — аргументы, { ... } — тело. Тип возвращаемого значения выводится сам; если нужно указать явно, пишут -> bool перед телом.
Отдельную функцию писать не запрещено, просто длиннее:
bool byDescending(int x, int y) { return x > y; }
sort(a.begin(), a.end(), byDescending);
Захват
В квадратных скобках указывается, что лямбда видит снаружи.
| запись | смысл |
|---|---|
[] |
ничего — только аргументы |
[&] |
всё по ссылке |
[=] |
всё по значению (копия) |
[&a] |
только a, по ссылке |
[a] |
только a, копией |
Типичный случай — сортировка индексов по значениям чужого массива:
vector<int> order(n);
iota(order.begin(), order.end(), 0); // 0, 1, 2, ...
sort(order.begin(), order.end(),
[&](int i, int j) { return a[i] < a[j]; });
Получили порядок индексов, при котором a возрастает, не трогая сам a. Приём нужен, когда исходный порядок ещё пригодится.
В олимпиадном коде обычно пишут [&] и не думают. В большом проекте так делать не стоит: захват по ссылке живёт, пока живы переменные, и лямбда, пережившая их, обращается к освобождённой памяти.
Рекурсивная лямбда
Лямбда не знает своего имени, поэтому обычная рекурсия в ней не работает. Обходной путь до C++23 — передавать себя аргументом:
auto dfs = [&](auto&& self, int v, int parent) -> void {
for (int to : graph[v])
if (to != parent) self(self, to, v);
};
dfs(dfs, 0, -1);
Тип возвращаемого значения тут указывать обязательно — сам он не выведется. С C++23 появился this auto&& self, и передавать вручную больше не нужно.
Зачем вообще: лямбда видит все локальные переменные через [&], и не приходится тащить массивы в глобальную область или передавать их параметрами.
Строгий порядок — не рекомендация
Компаратор обязан задавать строгий порядок. На практике это значит: comp(x, x) всегда ложно, и если comp(x, y) истинно, то comp(y, x) ложно.
Нарушение выглядит невинно:
sort(a.begin(), a.end(), [](int x, int y) { return x <= y; }); // так нельзя
На массиве из ста тысяч одинаковых элементов это даёт ошибку сегментации. Не «неверный порядок», а падение: внутренняя реализация sort полагается на строгость, чтобы не выходить за границы массива, и без неё уходит за них.
Проверено на GCC: vector<int> a(100000, 7) плюс такой компаратор — segmentation fault. Собранная с -D_GLIBCXX_DEBUG та же программа сообщает внятно:
Error: comparison doesn't meet irreflexive requirements, assert(!(a < a)).
Ещё один флаг в пользу того, чтобы держать санитайзеры под рукой.
Сравнение по нескольким полям надёжнее всего писать через tie:
sort(v.begin(), v.end(), [](const Person& a, const Person& b) {
return tie(a.grade, a.name) < tie(b.grade, b.name);
});
Кортежи сравниваются лексикографически и строго — нарушить порядок здесь невозможно.
Частая задача «по одному полю возрастая, по другому убывая» решается минусом или перестановкой сторон:
return tie(a.grade, b.score) < tie(b.grade, a.score); // grade вверх, score вниз
Готовые компараторы
Для убывания лямбда не нужна:
sort(a.begin(), a.end(), greater<int>()); // или greater<>() с C++14
sort(a.rbegin(), a.rend()); // то же самое через обратные итераторы
Второй вариант короче, но обратные итераторы легко перепутать. Третий путь — отсортировать обычно и вызвать reverse: стоит линию поверх , то есть практически ничего.
Компаратор для контейнера
У sort компаратор — аргумент, у set, map и priority_queue — часть типа:
set<int, greater<int>> descending; // множество по убыванию
priority_queue<int, vector<int>, greater<int>> minHeap; // куча минимума
Запись priority_queue неудобна и запоминается плохо, но нужна постоянно: по умолчанию куча выдаёт максимум, а в алгоритме Дейкстры и почти везде ещё нужен минимум.
Свой компаратор для контейнера — структура с operator():
struct ByLength {
bool operator()(const string& a, const string& b) const {
if (a.size() != b.size()) return a.size() < b.size();
return a < b;
}
};
set<string, ByLength> words;
Сравнение по длине, а при равной длине — лексикографически. Вторая строка обязательна: без неё две строки одинаковой длины считались бы равными, и set оставил бы только одну.
Через tie это записать не выйдет: tie принимает ссылки, а a.size() — временное значение, и код не скомпилируется. Здесь нужен либо явный if, либо make_tuple с копированием.
Это же и есть ответ на вопрос, зачем структуре operator(): объект с ним ведёт себя как функция, и его можно передать туда, где ждут функцию, — но, в отличие от функции, он может хранить состояние.