Алгоритм сортировки естественным слиянием (Natural Merge Sort): принцип работы

Модуль "Сортировки" • урок 10 из 17 •

Также упоминается как: Естественное двухпутевое слияние

Суть урока. Естественное слияние (Natural Merge Sort) - это адаптивная оптимизация классической сортировки слиянием. Вместо того, чтобы слепо делить массив пополам до единичных элементов, этот алгоритм ищет уже отсортированные последовательности (серии, runs) в исходном массиве и сливает их.

Время работы в лучшем случае (почти отсортированный массив) составляет O(N), а в худшем - O(N log N).
Историческая справка: Рождение Timsort

Идея "естественного слияния" легла в основу алгоритма Timsort (создан Тимом Питерсом в 2002 году), который сегодня является стандартом для Python, Java и JavaScript.

Читать дальше

Тим Питерс, один из ключевых разработчиков Python (и автор знаменитого "Дзена Питона"), заметил важную вещь: в реальном мире данные почти никогда не бывают абсолютно случайными. В массивах часто уже есть отсортированные куски (например, логи отсортированы по времени, но с небольшими сбоями). Классическая сортировка слиянием игнорирует этот факт, выполняя лишнюю работу.

Timsort работает как гениальный пылесос: он идет по массиву и собирает "естественные серии" (runs). Если серия убывает, он мгновенно разворачивает ее в памяти. Если серия слишком короткая (меньше так называемого minrun), он достраивает ее до нужной длины с помощью быстрой и компактной сортировки вставкой. Затем он начинает хитро сливать эти серии, поддерживая баланс длин серий в специальном стеке (чтобы слияние шло эффективно).

Интересный факт: Алгоритм Timsort считался безупречным более 10 лет. Но в 2015 году европейские исследователи, использующие формальную верификацию кода (доказательство теорем), обнаружили глубоко запрятанный баг в логике балансировки стека Timsort. Баг мог привести к падению программы при сортировке специально подобранных массивов огромного размера. Ошибку быстро исправили во всех языках программирования, но этот случай вошел в историю как пример важности математического доказательства корректности алгоритмов.

Идея метода сортировки

Естественное слияние работает "снизу вверх" (bottom-up), не используя рекурсию. Мы проходим по массиву и объединяем смежные отсортированные серии.

Алгоритм сортировки естественным слиянием: выделение и слияние серий
Сортировка естественным слиянием: распознавание естественно упорядоченных непрерывных серий и их попарное слияние.

Интерактивная модель демонстрирует суть естественного слияния: алгоритм ищет в массиве уже готовые упорядоченные серии и объединяет их.

Тренажер: алгоритм сортировки естественным слиянием

Отсортируем массив 4 7 2 5 1 6 3, находя и сливая готовые серии.

серии массива первая серия вторая серия результат слияния
Проход: 1 Слияний: 0 Шаг: 0 / 0
[Tim]
Timsort
Сортировка Timsort
Адаптивный гибрид вставок и слияния: находит естественные серии, добирает их вставкой и эффективно сливает.
Разобрать по шагам в большой версии →
HDD
External Merge Sort
Внешняя сортировка
Для данных, не влезающих в оперативную память: нарезаем файл на отсортированные прогоны, затем сливаем их через буфер.
Разобрать по шагам в большой версии →

Анализ сложности

Прелесть этого алгоритма в его адаптивности. Если подать на вход уже отсортированный массив, алгоритм за один проход определит, что серия всего одна, и завершит работу за время O(N). Классический Merge Sort все равно бы потратил O(N log N).

В худшем случае (массив отсортирован по убыванию) алгоритм будет находить серии длиной в 1 элемент и, по сути, сведется к классической восходящей сортировке слиянием. Время работы будет O(N log N).

Реализация на C

Естественное слияние: код целиком C, 40 строк
// Функция возвращает индекс конца текущей серии
int GetNextRun(int arr[], int n, int start) {
  int i = start;
  while (i < n - 1 && arr[i] <= arr[i + 1]) {
    i++;
  }
  return i;
}

// Процедура слияния (Merge) аналогична классической
// void Merge(int arr[], int left, int mid, int right) { ... }

void NaturalMergeSort(int arr[], int n) {
  int num_runs = 0;
  
  // Пока не останется всего одна серия на весь массив
  do {
    num_runs = 0;
    int i = 0;
    
    while (i < n) {
      // Находим первую серию
      int end1 = GetNextRun(arr, n, i);
      num_runs++;
      
      // Если первая серия дошла до конца массива - выходим
      if (end1 == n - 1) break;
      
      // Находим вторую серию
      int end2 = GetNextRun(arr, n, end1 + 1);
      
      // Сливаем их
      Merge(arr, i, end1, end2);
      num_runs++; // Считаем получившуюся общую серию
      
      i = end2 + 1; // Сдвигаем указатель для следующих пар
    }
    
  } while (num_runs > 1);
}

Коротко: главное

  • Естественное слияние - адаптивная оптимизация классической сортировки слиянием.
  • Вместо слепого деления алгоритм сканирует массив и находит уже отсортированные последовательности (серии).
  • Слияние найденных серий позволяет отсортировать почти упорядоченный массив за время O(N).
  • В худшем случае (обратный порядок) время работы составляет O(N log N).
  • Именно этот принцип лежит в основе гибридного алгоритма Timsort (стандарт в Python).

Проверьте себя

Чему равно время работы Естественного слияния на уже отсортированном массиве?

Как вам урок? Спасибо за отзыв!