Урок курса
Сортировка слиянием: разбиение, слияние и рекуррентность
Алгоритмы и структуры данных на PythonВ прошлом уроке мы разобрали, как рекурсия разворачивается в стек вызовов и как трассировать глубину до базового случая. В сортировке слиянием глубина рекурсии задаёт число слоёв слияний: на каждом слое суммарно обрабатывается n элементов, поэтому Θ(log n) слоёв дают Θ(n log n) работы.
Шаг разбиения: рекурсивное деление массива до базового случая
Разбиение не перемещает и не сравнивает элементы: оно вычисляет индекс середины и запускает рекурсивные вызовы для двух половин.
def merge_sort(arr: list, lo: int, hi: int) -> None:
"""Сортирует arr[lo:hi] на месте (слияние использует буфер)."""
if hi - lo <= 1: # базовый случай: 0 или 1 элемент
return
mid = lo + (hi - lo) // 2 # floor((lo+hi)/2), без переполнения
merge_sort(arr, lo, mid) # левая часть: размер floor(n/2)
merge_sort(arr, mid, hi) # правая часть: размер ceil(n/2)
merge(arr, lo, mid, hi) # объединяем две отсортированные половины
Здесь n = hi - lo. Левая часть [lo, mid) содержит ровно (hi - lo) // 2 = ⌊n/2⌋ элементов, правая [mid, hi) — оставшиеся ⌈n/2⌉. При нечётном n правая часть на один элемент длиннее.
Максимальная глубина. Каждый рекурсивный вызов уменьшает размер подзадачи примерно вдвое. Путь от корня до листа (подмассив длины 1) проходит через ⌈log₂ n⌉ делений, а сам лист — это ещё один уровень. Итого максимальная глубина стека для n ≥ 1 равна ⌈log₂ n⌉ + 1. Для n = 4 это 3 уровня: корень → два вызова размера 2 → четыре вызова размера 1.
Важный момент: «глубина» здесь считается как число одновременно активных фреймов на пути от корня до текущего листа, а не общее число всех вызовов. Всего вызовов будет порядка 2n − 1 (бинарное дерево с n листьями), но в стеке одновременно живёт не более ⌈log₂ n⌉ + 1 фреймов.
Операция mid = lo + (hi - lo) // 2 занимает O(1) на вызов. Линейная работа с элементами выполняется не при разбиении, а при последующем слиянии.
Шаг слияния: два указателя, стабильность и стоимость Θ(hi − lo)
После того как обе половины отсортированы рекурсивно, нужно соединить их в один упорядоченный подмассив. Именно здесь происходит вся линейная работа алгоритма.
def merge(arr: list, lo: int, mid: int, hi: int) -> None:
"""Сливает arr[lo:mid] и arr[mid:hi] — обе части уже отсортированы."""
buf = []
i, j = lo, mid
while i < mid and j < hi:
if arr[i] <= arr[j]: # <= гарантирует стабильность
buf.append(arr[i])
i += 1
else:
buf.append(arr[j])
j += 1
# дописываем остаток той части, которая не исчерпана
buf.extend(arr[i:mid])
buf.extend(arr[j:hi])
arr[lo:hi] = buf # копируем результат обратно
Два указателя i и j движутся по левой и правой частям. На каждой итерации цикла один из них продвигается вперёд, поэтому цикл выполнит не более (mid - lo) + (hi - mid) = hi - lo итераций. Хвостовые extend добавляют оставшиеся элементы без сравнений. Итоговое копирование arr[lo:hi] = buf — ещё hi - lo операций. Суммарная стоимость одного слияния: Θ(hi − lo) по времени.
Почему <= обеспечивает стабильность. Стабильность означает, что элементы с одинаковыми ключами сохраняют исходный взаимный порядок. При arr[i] <= arr[j] равный элемент из левой части всегда попадает в буфер раньше равного элемента из правой. Поскольку левая часть в дереве рекурсии соответствует элементам с меньшими исходными индексами, их порядок сохраняется. Замените <= на < — и алгоритм перестанет быть стабильным: при равенстве правый элемент пойдёт первым.
Сколько памяти тратит один merge. Буфер buf в момент слияния имеет размер hi - lo. После копирования результата обратно в arr буфер освобождается — он не живёт дольше своего вызова merge.
Пиковая память всей сортировки определяется самым крупным одновременно живущим буфером. Рекурсивные ветви выполняются последовательно, а не параллельно: сначала полностью завершается левый поддерево, потом правый. Значит, в момент слияния на верхнем уровне (размер буфера n) все буферы вложенных вызовов уже освобождены. Самый большой буфер — финальный, размером n. Глубина стека вызовов — O(log n) фреймов. Итоговая пиковая дополнительная память: O(n) + O(log n) = O(n).
Буферы разных уровней не суммируются по всему дереву: буфер дочернего слияния освобождается до слияния родительского подмассива.
Дерево разбиений и слияний для [5, 2, 4, 1]
Для массива [5, 2, 4, 1] можно явно посчитать работу на каждом слое слияний.
Вход: arr = [5, 2, 4, 1], n = 4.
Уровень 0 (корень): один вызов merge_sort([5, 2, 4, 1]). Разбиение даёт [5, 2] и [4, 1].
Уровень 1: два вызова.
merge_sort([5, 2])→ разбиение на[5]и[2].merge_sort([4, 1])→ разбиение на[4]и[1].
Уровень 2 (листья): четыре вызова размера 1 — все мгновенно возвращаются по базовому случаю. Сравнений: 0.
Теперь слияния идут снизу вверх:
Слияние на уровне 1, левое: [5] и [2]. Указатели: 5 > 2, берём 2; левая часть исчерпана, добавляем 5. Итог: [2, 5]. Сравнений: 1.
Слияние на уровне 1, правое: [4] и [1]. 4 > 1, берём 1; добавляем 4. Итог: [1, 4]. Сравнений: 1.
Слияние на уровне 0: [2, 5] и [1, 4].
- 2 > 1 → берём 1,
j=1. - 2 ≤ 4 → берём 2,
i=1. - 5 > 4 → берём 4,
j=2, правая исчерпана. - Дописываем 5.
Итог: [1, 2, 4, 5]. Сравнений: 3.
Сводка по уровням:
| Уровень | Слияний | Размеры буферов | Сравнений |
|---|---|---|---|
| 2 (листья) | — | — | 0 |
| 1 | 2 | 2 + 2 | 1 + 1 = 2 |
| 0 | 1 | 4 | 3 |
| Итого | 5 |
На уровне 1 суммарный размер буферов 2 + 2 = 4 = n. На уровне 0 — 4 = n. Суммарная работа на каждом уровне пропорциональна n. Уровней всего ⌈log₂ 4⌉ + 1 = 3 (включая листья), активных слияний — 2: уровни 1 и 0. Это согласуется с Θ(log n) слоёв слияний и Θ(n) работы на каждом — Θ(n log n) суммарно.
Пять сравнений — точное число для этого входа. Асимптотическая оценка говорит о росте при увеличении n, а не о точном числе операций на фиксированном примере.

Рекуррентность T(n) = 2T(n/2) + Θ(n) и теорема мастера (случай 2)
Рекуррентность складывается из двух рекурсивных подзадач и линейного слияния.
Составляем рекуррентность. При вызове merge_sort(arr, lo, hi) для подмассива длины n = hi - lo > 1 происходит следующее:
- Два рекурсивных вызова для подмассивов размеров ⌊n/2⌋ и ⌈n/2⌉ — суммарная стоимость
T(⌊n/2⌋) + T(⌈n/2⌉). - Один шаг слияния стоимостью Θ(n).
- Само разбиение (вычисление
mid) — O(1), поглощается Θ(n).
Точная рекуррентность: T(n) = T(⌊n/2⌋) + T(⌈n/2⌉) + Θ(n), база T(0) = T(1) = Θ(1).
Для асимптотического анализа разницей между ⌊n/2⌋ и ⌈n/2⌉ можно пренебречь, и рекуррентность записывается в стандартной форме:
T(n) = 2T(n/2) + Θ(n)
Важно: стоимость шага слияния Θ(n) не зависит от исходного порядка элементов — указатели всегда проходят по всему подмассиву. Поэтому эта рекуррентность одинакова для лучшего, среднего и худшего случаев.
Теорема мастера, случай 2. Общая форма: T(n) = aT(n/b) + f(n). В нашем случае:
a = 2(два рекурсивных вызова),b = 2(каждый для половины),f(n) = Θ(n)(стоимость слияния).
Вычисляем критический показатель: log_b a = log₂ 2 = 1, значит n^{log_b a} = n^1 = n.
Условие второго случая: f(n) = Θ(n^{log_b a}), то есть f(n) = Θ(n). Это в точности выполнено.
Заключение теоремы мастера для случая 2:
T(n) = Θ(n^{log_b a} · log n) = Θ(n · log n) = Θ(n log n)
Геометрический смысл: при каждом переходе на следующий уровень дерева рекурсии число подзадач удваивается, но их суммарный размер остаётся n. Это равновесие — f(n) не растёт и не убывает относительно n^{log_b a} — характеризует второй случай. Именно поэтому логарифм появляется как множитель: он считает число уровней, на каждом из которых выполняется Θ(n) работы.
Если бы стоимость слияния была Θ(n²) — работа на нижних уровнях доминировала бы, и мы попали бы в третий случай. Если бы слияние стоило O(1) — доминировала бы рекурсия, первый случай. Merge sort попадает ровно в середину.
Задача 1
Для массива длиной n = 8 дерево рекурсии сортировки слиянием имеет 4 уровня (уровень 0 — корень, уровень 3 — листья). На каком уровне выполняются слияния пар одиночных элементов?
Задача 2
Турнир летописцев
В Великом Архиве хранятся летописи множества королевств — свитки, заполненные датами важнейших событий. Раз в год Хранитель Архива проводит Турнир летописцев: он упорядочивает записи внутри каждого свитка, чтобы история читалась от древнейших событий к новейшим. Для этого он следует строгому ритуалу, завещанному предками, — ритуалу разделения и слияния.
Ритуал устроен так: Хранитель берёт свиток и делит его ровно пополам, получая два свитка поменьше. С каждой из половин он поступает точно так же — снова и снова, пока в руках не окажутся свитки с единственной записью. Затем начинается обратный путь: Хранитель сливает пары свитков в один, каждый раз сравнивая самую первую ещё не перенесённую запись из левого свитка с самой первой ещё не перенесённой записью из правого и перекладывая ту, чья дата раньше. Если даты совпадают, первой переносится запись из левого свитка — так древний порядок равных записей не нарушается, и летопись остаётся справедливой к первоисточнику.
Хранитель дорожит своим временем и ведёт строгий учёт: после каждого Турнира он записывает, сколько сравнений дат было сделано в ходе всего ритуала. Это число служит мерой трудоёмкости и позволяет сравнивать свитки между собой — чем меньше сравнений потребовалось, тем удачнее оказался исходный порядок записей.
Вам предстоит воспроизвести ритуал в точности так, как его совершает Хранитель, и выяснить, в каком порядке окажутся записи после завершения всех слияний, а также подсчитать общее число сравнений дат, выполненных в ходе этого священного действа.
Формат ввода
В первой строке записано n (1 ≤ n ≤ 200 000). Во второй строке записаны n целых дат. Записи различаются исходными номерами от 1 до n. При нечётной длине левая половина содержит ⌊n/2⌋ записей.
Формат вывода
В первой строке выведите число сравнений дат. Во второй строке выведите исходные номера записей после стабильной сортировки.
Пример
Ввод:
5 4 2 2 5 1
Вывод:
8 5 2 3 1 4
Пояснение
Записи с одинаковой датой 2 имеют исходные номера 2 и 3 и остаются в этом порядке.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
