Слияние, пересечение и запись на месте: каждому источнику свой указатель, ни один не откатывается назад. Основа слиянием и merge join в базах данных.
Ключевые тезисы
- Слияние двух отсортированных наборов за O(n + m) — сортировать заново незачем.
- Чтение и запись двумя индексами дают фильтрацию на месте с O(1) дополнительной памяти.
- Инвариант «пишущий не обгоняет читающего» — то, почему запись не портит непрочитанные данные.
Какую задачу решает
Два отсортированных набора нужно объединить, пересечь или сравнить. Склеить и отсортировать — значит выбросить уже имеющийся порядок и заплатить за него второй раз; перебрать пары — получить квадрат там, где хватает линии.
Приём сохраняет порядок: у каждого источника свой указатель, двигается тот, чей элемент обработан. В задачах записи роли другие — один индекс читает, второй пишет и отстаёт, — но правило то же: назад не возвращается никто, и потому всё умещается в один проход и дополнительной памяти.
Подробный разбор
Два отсортированных списка нужно объединить в один. В лоб: склеить и отсортировать — , хотя оба входа уже упорядочены и вся работа уходит впустую. Пересечь два списка перебором — и вовсе .
Вторая половина задач — про запись: удалить дубликаты, перенести нули в конец, отфильтровать элементы по условию. Очевидное решение создаёт новый массив, то есть дополнительной памяти, которой на больших данных может просто не быть.
Каждому источнику — свой указатель; на каждом шаге двигается только тот, чей элемент обработан. Ни один указатель не откатывается назад.
В задачах записи роли разделяются иначе: один указатель читает, второй пишет и отстаёт. Пишущий никогда не обгоняет читающего, поэтому запись не затирает непрочитанное — и буфер не нужен.
a: 1 3 7 i -- первый необработанный в a
b: 2 4 9 j -- первый необработанный в b| Шаг | a[i] | b[j] | Кого берём | Результат |
|---|---|---|---|---|
| 1 | 1 | 2 | a[i] | 1 |
| 2 | 3 | 2 | b[j] | 1 2 |
| 3 | 3 | 4 | a[i] | 1 2 3 |
| 4 | 7 | 4 | b[j] | 1 2 3 4 |
| 5 | 7 | 9 | a[i] | 1 2 3 4 7 |
| 6 | — | 9 | хвост b | 1 2 3 4 7 9 |
a: 1 1 2 2 3
read = 1, write = 1: a[1]=1 == a[0] -> пропускаем
read = 2, write = 1: a[2]=2 != a[0] -> a[1] = 2, write = 2
read = 3, write = 2: a[3]=2 == a[1] -> пропускаем
read = 4, write = 2: a[4]=3 != a[1] -> a[2] = 3, write = 3
ответ: первые write = 3 элемента -- 1 2 3i = j = 0
out = []
while i < len(a) and j < len(b):
if a[i] <= b[j]: # <= сохраняет стабильность
out.append(a[i]); i += 1
else:
out.append(b[j]); j += 1
out.extend(a[i:]) # ровно один из хвостов непустой
out.extend(b[j:])
return out- Время — : каждый элемент обрабатывается один раз.
- Память — на результат при слиянии и при записи на месте.
- Инвариант слияния: в
outуже лежат все элементы, меньшие обоих текущих; минимум оставшихся — этоa[i]илиb[j]. - Инвариант записи:
write <= read, поэтому запись не портит ещё не прочитанное.
Оба массива упорядочены, значит минимум из всех необработанных элементов обязательно стоит на одном из : внутри каждого массива всё остальное больше. Выбирая меньший из a[i] и b[j], мы каждый раз добавляем в ответ действительно минимальный оставшийся элемент — этого достаточно, чтобы результат оказался отсортированным.
В задачах записи корректность следует из неравенства write <= read, которое сохраняется на каждом шаге: пишущий сдвигается не чаще читающего. Значит, ячейка, в которую мы пишем, либо уже прочитана, либо это та же ячейка, что и читаемая, — потерять данные невозможно.
Знак сравнения решает вопрос стабильности: при a[i] <= b[j] элементы из первого массива при равенстве идут раньше. Для слиянием это и есть источник её устойчивости.
| Сигнал в условии | Что применять |
|---|---|
| Два (или k) отсортированных набора, нужно объединить | слияние указателями |
| «Пересечение», «разность» отсортированных списков | тот же цикл, в ответ идут совпадения |
| «Сделайте на месте», «дополнительная память — константа» | чтение и запись двумя индексами |
| «Сохраните относительный порядок» | стабильная фильтрация записью на месте |
| «Является ли строка подпоследовательностью другой» | указатель по образцу и по строке |
| Данные не помещаются в память, но приходят по порядку | потоковое слияние, как в внешней |
| Вариант | Задача | Особенность |
|---|---|---|
| Слияние | шаг слиянием | хвосты дописываются после основного цикла |
| Пересечение и разность | общие или уникальные элементы | равенство двигает оба указателя |
| Слияние на месте | массив с запасом в конце | идти справа налево, чтобы не затирать |
| Удаление дубликатов | отсортированный массив | write хранит длину ответа |
| Стабильная фильтрация | перенос нулей, удаление по условию | порядок оставшихся сохраняется |
| k списков сразу | слияние k отсортированных | по головам: |
Прикладная версия того же приёма — merge join в базах данных и пересечение постинг-списков в поиске: два отсортированных потока, два курсора, один проход. Отличие только в том, что данные приходят с диска, а не из памяти.
- Забытые хвосты. После основного цикла один из массивов почти всегда не исчерпан.
- Слияние на месте слева направо. Запись затирает ещё не прочитанные элементы; идти нужно с конца.
- Потеря стабильности.
<вместо<=меняет порядок равных элементов — для по нескольким ключам это ошибка. - Дубликаты при пересечении. Если в обоих списках есть повторы, нужно решить, сколько раз элемент попадает в ответ, — и сдвигать указатели соответственно.
- k списков попарно. Слияние по одному даёт ; по головам даёт .
| Приём | Когда он лучше | Цена |
|---|---|---|
| Хеш-таблица | входы не отсортированы, порядок в ответе не нужен | памяти |
| Куча | источников много (k списков, k потоков) | на элемент |
| Бинарный поиск | один список сильно короче другого | вместо |
| Сортировка | входы не упорядочены и порядок нужен |
Если один список в тысячу раз короче другого, слияние проигрывает: выгоднее для каждого элемента короткого списка искать место в длинном . Это тот случай, когда хуже, чем .
- Почему после основного цикла слияния достаточно дописать оба хвоста, не проверяя, какой именно непустой?
- Слияние на месте в массив с запасом: с какого конца идти и почему?
- Удаление дубликатов на месте: что означает значение
writeв конце работы? - Пересечение двух отсортированных списков с повторами — сколько раз элемент должен попасть в ответ и как это обеспечить?
- Когда выгоднее не сливать, а искать ?
Чем заменяют
Видеолекции
Тайм-кода именно на эту тему в записях нет. Но глава «Алгоритмы и структуры данных» разобрана в курсе целиком — с той оговоркой, что место в записи придётся искать самому.
Связанные темы
Два указателя и окно
Two Pointers and Sliding Window90%
Два указателя: семейство приёмов · Алгоритмы и структуры данныхЗонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.
Opposite-Direction Pointers90%
Встречные указатели · Алгоритмы и структуры данныхДва индекса сходятся с концов отсортированного массива. На каждом шаге отбрасывается конец, который заведомо не даст лучшего ответа, — и вместе с ним целая полоса пар.
Sliding Window90%
Скользящее окно · Алгоритмы и структуры данныхОкно между двумя границами: правая расширяет, левая чинит. Состояние не пересчитывается, а обновляется инкрементально — отсюда линейное время на задачах о подотрезках.
Fast and Slow Pointers90%
Быстрый и медленный указатель · Алгоритмы и структуры данныхДва указателя идут в одну сторону с разной скоростью. Если структура зациклена, быстрый догоняет медленного — цикл находится без журнала посещённых узлов.
Prefix Sums90%
Префиксные суммы · Алгоритмы и структуры данныхОдин предварительный проход превращает сумму на любом отрезке в одно вычитание. Тот же приём вместе с хеш-таблицей находит подотрезок с заданной суммой за линейное время.
Monotonic Stack and Deque90%
Монотонный стек и дек · Алгоритмы и структуры данныхПриём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в скользящем окне — за один проход.
Всё, что названо в описании, разобрано здесь же или в соседней теме — переходы под каждым определением.
Минимум на указателях
Наименьший необработанный элемент всегда стоит на одном из двух курсоров.
Разбор ниже: Почему это работаетИнвариант записи
Пишущий индекс не обгоняет читающий, поэтому данные не затираются.
Разбор ниже: Почему это работаетСтабильность
Знак сравнения решает, чьи равные элементы идут первыми.
Разбор ниже: Ограничения и типичные ошибкиГлава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.