4.6. Сравнение со сортировкой¶
Во многих задачах один и тот же результат можно получить разными путями:
- используя структуры данных (множества, хеш‑таблицы и т. п.);
- используя сортировку и линейный проход.
Иногда разница между подходами очень заметна на практике, даже если асимптотики выглядят одинаково.
Задача-пример: сколько общих элементов у двух списков¶
Даны два списка (или массива) A и B, каждый длины n. Нужно посчитать, сколько значений встречается и там, и там.
Пример:
A = [7, 1, 6, 10, 3]B = [4, 1, 10, 7]
Ответ: 3, потому что общие элементы — 1, 7 и 10.
Наивное решение — проверить все пары элементов — работает за O(n^2) и быстро становится слишком медленным. Дальше рассмотрим более быстрые подходы.
Алгоритм 1: упорядоченное множество (set)¶
- Добавим все элементы списка A в структуру
set(обычно это сбалансированное дерево). - Пройдём по B и для каждого элемента проверим, есть ли он в
set.
Поскольку операции insert/find в set работают за O(log n), итог:
- построение множества:
O(n log n) - проверки элементов B:
O(n log n)
Итого: O(n log n).
Плюс: элементы в set хранятся в отсортированном порядке.
Минус: структура сложнее (дерево), и из-за этого константы часто ощутимы.
Алгоритм 2: хеш‑множество (unordered_set)¶
Здесь делаем то же самое, но используем unordered_set (хеш‑таблица):
- Загружаем A в
unordered_set. - Для каждого элемента из B проверяем наличие.
В среднем insert/find работают за O(1), поэтому:
Итого: O(n) в среднем.
Плюс: обычно существенно быстрее дерева.
Минусы:
- порядок элементов не хранится;
- в редких случаях возможны «плохие» входы, когда из-за коллизий время может деградировать (в реальной практике на соревнованиях это обычно не проблема, но помнить стоит).
Алгоритм 3: сортировка + два указателя¶
Вместо структур данных можно:
- Отсортировать A и B.
-
Пройти по ним одновременно двумя указателями (как в «слиянии»):
-
если
A[i] == B[j]— нашли общий элемент, увеличиваем счётчик и двигаем оба указателя; - если
A[i] < B[j]— двигаемi; - иначе двигаем
j.
Сортировки занимают O(n log n), проход — O(n), значит итог:
O(n log n).
Зато после сортировки остаётся очень простой и быстрый линейный проход.
Сравнение скорости на практике¶
Если элементы списков — случайные целые числа (например, из диапазона 1 … 10^9), то на современном компьютере типичная картина может выглядеть так:
| n | Алг. 1 (set) | Алг. 2 (unordered_set) | Алг. 3 (sorting) |
|---|---|---|---|
| 10^6 | 1.6 s | 0.35 s | 0.22 s |
| 2·10^6 | 3.9 s | 0.9 s | 0.35 s |
| 3·10^6 | 6.0 s | 1.4 s | 0.55 s |
| 4·10^6 | 8.1 s | 1.9 s | 0.75 s |
| 5·10^6 | 10.4 s | 2.6 s | 0.95 s |
Что видно:
- Алгоритмы 1 и 2 отличаются только выбором структуры данных, но
unordered_setможет быть в 4–5 раз быстрее, потому что у дерева дорогие операции и указатели, а хеш‑таблица обычно проще. -
Самым быстрым оказывается Алгоритм 3 (сортировка). Хотя его асимптотика
O(n log n), он часто выигрывает за счёт того, что: -
сортировка реализована крайне оптимизировано;
- дальнейший проход — простой линейный цикл с хорошей локальностью памяти.
Вывод¶
В задачах на пересечения/подсчёты совпадений часто стоит помнить про альтернативу:
- unordered_set — отличный быстрый «универсальный» вариант.
- сортировка + два указателя — иногда ещё быстрее, особенно если после сортировки можно решить задачу почти полностью линейным проходом.
- set полезен, когда нужен порядок (например, ближайший элемент, lower_bound и т. п.), но ради одной проверки «есть/нет» он нередко проигрывает по времени.