Сортировка слиянием (Merge Sort) – один из классических алгоритмов «разделяй и властвуй». Она широко используется в учебных курсах и практических задачах благодаря предсказуемой сложности и простоте реализации. Ниже подробно рассмотрим принцип работы, оценку сложности, причины эффективности и примеры реализации на популярных языках программирования.
Основная идея алгоритма
Алгоритм Merge Sort состоит из двух основных этапов:
- Разделение – исходный массив рекурсивно делится пополам, пока не получим подмассивы длиной 1 (они уже отсортированы по определению).
- Слияние (merge) – отсортированные подмассивы последовательно объединяются в один отсортированный массив.
Таким образом, сортировка строится снизу вверх: сначала сортируются минимальные части, затем они «склеиваются» в всё более крупные отсортированные блоки.
Пошаговый процесс
Разделение массива
Для массива A[0 … n‑1] находим средний индекс mid = n/2 и рекурсивно вызываем сортировку для левой (A[0 … mid‑1]) и правой (A[mid … n‑1]) частей.
Слияние двух отсортированных подмассивов
Предположим, у нас есть два уже отсортированных массива L и R. Создаём результирующий массив result и два указателя i и j, początkowo равные 0. Затем:
- Сравниваем
L[i]иR[j]; - Меньший элемент копируем в
resultи сдвигаем соответствующий указатель; - Повторяем, пока один из массивов не закончится;
- Оставшиеся элементы второго массива просто дописываем в конец
result.
Полученный result – отсортированный массив, содержащий все элементы L и R.
Сборка результата
После завершения рекурсивных вызовов каждый уровень возвращает уже отсортированный подмассив, который на предыдущем уровне сливается с другим. В конце получаем полностью отсортированный исходный массив.
Оценка сложности
Временная сложность
На каждом уровне рекурсии мы выполняем слияние всех n элементов – это O(n) операций. Количество уровней рекурсии равно количеству раз, которое можно делить пополам массив, пока не дойдём до единицы: log₂ n. Следовательно, общая сложность:
T(n) = O(n)·O(log n) = O(n log n).
Эта оценка справедлива для лучшего, среднего и худшего случая, потому что деление всегда происходит пополам, а процесс слияния линейно зависит от размера подмассивов.
Пространственная сложность
Для слияния требуется дополнительный массив размером n (или два временных массива по n/2 каждый). Поэтому дополнительная память – O(n). Рекурсивный стек потребляет O(log n) места, но он не доминирует.
Почему Merge Sort считается эффективным?
- Предсказуемая производительность – гарантированная O(n log n) во всех случаях, unlike quicksort, который может деградировать до O(n²) при неудачном выборе опорного элемента.
- Устойчивость (stability) – равные элементы сохраняют свой исходный порядок, что важно при сортировке сложных записей по нескольким полям.
- Параллелизм – этапы разделения и слияния легко распараллеливаются: разные части массива можно сортировать независимо на разных ядрах или потоках.
- Эффективность для внешней сортировки – когда данные находятся на медленных носителях (диски, ленты) и доступ возможен только последовательно, Merge Sort минимизирует количество проходов, так как каждое слияние работает с уже отсортированными блоками.
- Простота доказательства корректности – рекуррентное соотношение
T(n) = 2·T(n/2) + O(n)легко решается мастер‑теоремой, что упрощает академический анализ.
Сравнение с другими популярными сортировками
| Алгоритм | Средняя сложность | Худший случай | Память | Устойчивость |
|---|---|---|---|---|
| Merge Sort | O(n log n) | O(n log n) | O(n) | Да |
| Quick Sort | O(n log n) | O(n²) | O(log n) | Нет (можно сделать устойчивым) |
| Heap Sort | O(n log n) | O(n log n) | O(1) | Нет |
| Insertion Sort | O(n²) | O(n²) | O(1) | Да |
Как видно, Merge Sort сочетает гарантированную логарифмическую линейную сложность с устойчивостью, хотя и требует дополнительной памяти.
Пример реализации
На Python
def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result
На C++
void merge(vector& a, int l, int m, int r) { int n1 = m — l + 1, n2 = r, m; vector L(n1), R(n2); for (int i = 0; i < n1; ++i) L[i] = a[l + i]; for (int j = 0; j < n2; ++j) R[j] = a[m + 1 + j]; int i = 0, j = 0, k = l; while (i < n1 && j < n2) { if (L[i] <= R[j]) a[k++] = L[i++]; else a[k++] = R[j++]; } while (i < n1) a[k++] = L[i++]; while (j < n2) a[k++] = R[j++]; } void mergeSort(vector & a, int l, int r) { if (l >= r) return; int m = l + (r, l) / 2; mergeSort(a, l, m); mergeSort(a, m + 1, r); merge(a, l, m, r); }
На Java
public static void mergeSort(int[] arr, int left, int right) {
if (left >= right) return;
int mid = left + (right ⎯ left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int n1 = mid ⎯ left + 1;
int n2 = right — mid;
int[] L = new int[n1];
int[] R = new int[n2];
System.arraycopy(arr, left, L, 0, n1);
System.arraycopy(arr, mid + 1, R, 0, n2);
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) arr[k++] = L[i++];
else arr[k++] = R[j++];
}
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}
Преимущества и недостатки
Преимущества
- Гарантированная O(n log n) сложность.
- Устойчивая сортировка.
- Хорошо подходит для внешней сортировки и работы с последовательными структурами данных (списки, файлы).
- Легко параллелизируется.
Недостатки
- Требует дополнительной памяти O(n) (не «in‑place»).
- Рекурсивная реализация может вызывать переполнение стека при очень больших n (можно заменить итеративным bottom‑up вариантом).
- На небольших массивах постоянные факторы делают её медленнее, чем простые алгоритмы вроде вставки; поэтому часто используют гибридные подходы (например, Timsort).
Когда стоит использовать Merge Sort?
- Когда необходима гарантированная производительность независимо от входных данных.
- Когда важна устойчивость сортировки (например, при сортировке записей по нескольким полям).
- При работе с внешними носителями, где доступ к данным последователен.
- В многопоточных или распределённых системах, где этапы разделения можно выполнять параллельно.
Merge Sort – это надёжный, предсказуемый и устойчивый алгоритм сортировки, который находит применение как в учебных целях, так и в реальных системах, требующих стабильной производительности. Несмотря на необходимость дополнительной памяти, его преимущества в виде гарантированной O(n log n) сложности, устойчивости и хорошей параллелизуемости делают его одним из столпов современной алгоритмической библиотеки.