Алгоритм сортировки слиянием (Merge Sort): принцип работы
Также упоминается как: Метод слияния, Простое двухпоточное слияние, Прямое слияние
Время работы всегда O(N log N), но алгоритм требует дополнительной памяти O(N).
Читать дальше
В 1945 году компьютеры (такие как ENIAC) программировались путем физического переключения кабелей. Джон фон Нейман разрабатывал архитектуру следующего поколения (EDVAC) с хранимой в памяти программой. Чтобы доказать, что новые компьютеры могут не только рассчитывать баллистические траектории, но и заниматься обработкой данных, он взялся за задачу сортировки.
Поскольку оперативной памяти (на ртутных линиях задержки) у EDVAC было катастрофически мало, фон Нейману нужен был алгоритм, который мог бы эффективно сортировать огромные массивы данных, находящиеся на медленных внешних носителях - магнитных лентах (или перфокартах). Сортировка слиянием идеально подошла для этого: она читает данные последовательно, не требуя случайного доступа к памяти.
Считается, что фон Нейман написал 23-страничный рукописный манускрипт (чернилами!), в котором до мельчайших подробностей расписал первую в истории реализацию сортировки слиянием восходящим методом (Bottom-up Merge Sort). Этот документ стал одной из первых в мире "программ" в современном понимании этого слова.
Идея метода сортировки
Процесс сортировки слиянием можно разбить на два главных шага:
- Разделение (Divide): Массив рекурсивно разбивается ровно пополам до тех пор, пока каждый подмассив не станет длиной в 1 элемент (массив из 1 элемента по определению отсортирован).
- Слияние (Merge): Берем два соседних отсортированных подмассива и объединяем их в один больший отсортированный. Для этого нам нужен дополнительный буферный массив такого же размера. Мы сравниваем первые элементы двух подмассивов и берем наименьший из них, сдвигая указатель.
В тренажере показана работа сортировки слиянием: массив дробится пополам, а затем отсортированные половинки аккуратно склеиваются.
Тренажер: алгоритм сортировки слиянием
Сольем отсортированные половины 1 4 5 и 2 3 6 в один массив.
Анализ сложности
В отличие от сортировки Хоара, у сортировки слиянием нет худшего случая в плане времени работы. Она всегда стабильно делит массив пополам и тратит ровно линейное время на слияние на каждом уровне. Глубина дерева рекурсии равна log₂N. Итоговая временная сложность строго равна O(N log N).
Однако за эту стабильность приходится платить оперативной памятью: алгоритму обязательно требуется дополнительный массив размером N для проведения слияния, что дает пространственную сложность O(N). Из-за этого классическую сортировку слиянием редко используют для сортировки примитивных типов в оперативной памяти (предпочитая алгоритмы сортировки "на месте", такие как сортировка Хоара или пирамидальная сортировка).
Зато сортировка слиянием идеально подходит для сортировки связных списков (где слияние можно делать перекидыванием указателей без дополнительной памяти) и для внешней сортировки (сортировки огромных файлов на диске).
Реализация на C
Сортировка слиянием: код целиком
// Вспомогательная функция слияния двух отсортированных частей
void Merge(int arr[], int left, int mid, int right) {
int i = left, j = mid + 1, k = 0;
// Динамически выделяем временный массив
int *temp = (int *)malloc((right - left + 1) * sizeof(int));
// Сравниваем элементы и сливаем в temp
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// Дописываем оставшиеся элементы из левой части
while (i <= mid) temp[k++] = arr[i++];
// Дописываем оставшиеся элементы из правой части
while (j <= right) temp[k++] = arr[j++];
// Копируем результат обратно в оригинальный массив
for (i = left, k = 0; i <= right; i++, k++) {
arr[i] = temp[k];
}
free(temp); // Обязательно освобождаем память!
}
// Основная рекурсивная функция
void MergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
// Рекурсивно сортируем левую и правую половины
MergeSort(arr, left, mid);
MergeSort(arr, mid + 1, right);
// Сливаем две отсортированные половины
Merge(arr, left, mid, right);
}
}
Коротко: главное
- Сортировка слиянием - классический алгоритм подхода "разделяй и властвуй".
- Массив рекурсивно делится пополам до единичных элементов, после чего отсортированные части сливаются.
- Временная сложность строго равна O(N log N) в любом случае (худшем, среднем, лучшем).
- Главный недостаток - необходимость выделения O(N) дополнительной памяти для слияния.
- Алгоритм устойчив и отлично подходит для сортировки связных списков и внешних файлов.
Проверьте себя
Какой главный недостаток у классической сортировки слиянием для массивов в оперативной памяти?