В информатике сортировка слиянием (также часто пишется как mergesort и merge-sort [2] ) — это эффективный, универсальный и основанный на сравнении алгоритм сортировки . Большинство реализаций производят стабильную сортировку , что означает , что относительный порядок равных элементов одинаков на входе и выходе. Сортировка слиянием — это алгоритм «разделяй и властвуй» , изобретенный Джоном фон Нейманом в 1945 году. [3] Подробное описание и анализ сортировки слиянием снизу вверх появились в отчете Голдстайна и фон Неймана еще в 1948 году. [4]
Концептуально сортировка слиянием работает следующим образом:
Пример кода на языке C, использующего индексы для алгоритма сортировки слиянием сверху вниз, который рекурсивно разбивает список (в этом примере называемый запусками ) на подсписки, пока размер подсписка не станет 1, а затем объединяет эти подсписки для получения отсортированного списка. Шаг копирования назад избегается с помощью чередования направления слияния на каждом уровне рекурсии (за исключением первоначального одноразового копирования, которого тоже можно избежать).
В качестве простого примера рассмотрим массив с двумя элементами. Элементы копируются в B[], а затем объединяются обратно в A[]. Если элементов четыре, то при достижении дна уровня рекурсии отдельные элементы из A[] объединяются в B[], а затем на следующем более высоком уровне рекурсии эти двухэлементные элементы объединяются в A[]. Этот шаблон продолжается на каждом уровне рекурсии.
// Массив A[] содержит элементы для сортировки; массив B[] — это рабочий массив. void TopDownMergeSort ( A [], B [], n ) { CopyArray ( A , 0 , n , B ); // однократное копирование A[] в B[] TopDownSplitMerge ( A , 0 , n , B ); // сортировка данных из B[] в A[] } // Разделить A[] на 2 прогона, отсортировать оба прогона в B[], объединить оба прогона из B[] в A[] // iBegin включительно; iEnd не включает (A[iEnd] не входит в набор). void TopDownSplitMerge ( B [], iBegin , iEnd , A []) { if ( iEnd - iBegin <= 1 ) // если размер прогона == 1 return ; // считать его отсортированным // разделить прогон длиной более 1 элемента на две части iMiddle = ( iEnd + iBegin ) / 2 ; // iMiddle = средняя точка // рекурсивно сортировать оба прогона из массива A[] в B[] TopDownSplitMerge ( A , iBegin , iMiddle , B ); // сортировать левый прогон TopDownSplitMerge ( A , iMiddle , iEnd , B ); // сортируем правый прогон // объединяем полученные прогоны из массива B[] в A[] TopDownMerge ( B , iBegin , iMiddle , iEnd , A ); } // Левая исходная половина — A[ iBegin:iMiddle-1]. // Правая исходная половина — A[iMiddle:iEnd-1 ]. // Результат — B[ iBegin:iEnd-1 ]. void TopDownMerge ( B [], iBegin , iMiddle , iEnd , A []) { i = iBegin , j = iMiddle ; // Пока в левой или правой прогонах есть элементы... for ( k = iBegin ; k < iEnd ; k ++ ) { // Если левая прогонка существует и <= существующей правой прогонке. if ( i < iMiddle && ( j >= iEnd || A [ i ] <= A [ j ])) { B [ k ] = A [ i ]; i = i + 1 ; } else { B [ k ] = A [ j ]; j = j + 1 ; } } } void CopyArray ( A [], iBegin , iEnd , B []) { for ( k = iBegin ; k < iEnd ; k ++ ) B [ k ] = A [ k ]; }
Сортировка всего массива выполняется с помощью TopDownMergeSort(A, B, length(A)) .
Пример кода на языке C, использующего индексы для алгоритма сортировки слиянием снизу вверх, который обрабатывает список как массив из n подсписков (в этом примере называемых сериями ) размером 1 и итеративно объединяет подсписки туда и обратно между двумя буферами:
// массив A[] содержит элементы для сортировки; массив B[] — это рабочий массив void BottomUpMergeSort ( A [], B [], n ) { // Каждый 1-элементный отрезок в A уже «отсортирован». // Последовательно создаем более длинные отсортированные отрезки длиной 2, 4, 8, 16..., пока весь массив не будет отсортирован. for ( width = 1 ; width < n ; width = 2 * width ) { // Массив A заполнен отрезками длиной width. for ( i = 0 ; i < n ; i = i + 2 * width ) { // Объединить два отрезка: A[i:i+width-1] и A[i+width:i+2*width-1] в B[] // или скопировать A[i:n-1] в B[] ( if (i+width >= n) ) BottomUpMerge ( A , i , min ( i + width , n ), min ( i + 2 * width , n ), B ); } // Теперь рабочий массив B заполнен отрезками длины 2*width. // Копируем массив B в массив A для следующей итерации. // Более эффективная реализация поменяла бы роли A и B. CopyArray ( B , A , n ); // Теперь массив A заполнен отрезками длины 2*width. } } // Левый отрезок — A[iLeft :iRight-1]. // Правый отрезок — A[iRight:iEnd-1]. void BottomUpMerge ( A [], iLeft , iRight , iEnd , B []) { i = iLeft , j = iRight ; // Пока в левом или правом отрезке есть элементы... for ( k = iLeft ; k < iEnd ; k ++ ) { // Если левый отрезок существует и <= существующему правому отрезку. if ( i < iRight && ( j >= iEnd || A [ i ] <= A [ j ])) { B [ k ] = A [ i ]; i = i + 1 ; } else { B [ k ] = A [ j ]; j = j + 1 ; } } } void CopyArray ( B [], A [], n ) { for ( i = 0 ; i < n ; i ++ ) A [ i ] = B [ i ]; }
Псевдокод для алгоритма сортировки слиянием сверху вниз, который рекурсивно делит входной список на меньшие подсписки до тех пор, пока подсписки не будут тривиально отсортированы, а затем объединяет подсписки, возвращаясь вверх по цепочке вызовов.
function merge_sort( list m) is // Базовый случай. Список из нуля или одного элемента сортируется по определению. если длина m ≤ 1, то вернуть m // Рекурсивный случай. Сначала разделим список на подсписки одинакового размера , // состоящие из первой половины и второй половины списка. // Это предполагает, что списки начинаются с индекса 0. var left := пустой список var right := пустой список для каждого x с индексом i в m do if i < (длина m)/2 then добавить x слева еще добавить x справа // Рекурсивно сортируем оба подсписка. слева := merge_sort(слева) справа := merge_sort(справа) // Затем объединим отсортированные подсписки. возврат слияния (слева, справа)
В этом примере функция merge объединяет левый и правый подсписки.
функция merge(left, right) — это var result := пустой список пока левое не пусто и правое не пусто do if first(left) ≤ first(right) then добавить первый (слева) к результату слева := остаток(слева) еще добавить первый(справа) к результату справа := остальное(справа) // Элементы left могут быть как left, так и right; используйте их. // (Фактически будет выполнен вход только в один из следующих циклов.) while left is not empty do добавить первый (слева) к результату слева := остаток(слева) пока справа не пусто делать добавить первый(справа) к результату справа := остальное(справа) вернуть результат
Псевдокод для алгоритма сортировки слиянием снизу вверх, который использует небольшой массив фиксированного размера ссылок на узлы, где array[i] — это либо ссылка на список размером 2 i, либо nil . node — это ссылка или указатель на узел. Функция merge() будет похожа на ту, что показана в примере списков слияния сверху вниз, она объединяет два уже отсортированных списка и обрабатывает пустые списки. В этом случае merge() будет использовать node для своих входных параметров и возвращаемого значения.
функция merge_sort( голова узла ) — это // вернуть, если список пустой если head = nil , то вернуть nil var node array[32]; изначально все nil var node result var node next var int i результат := голова // объединить узлы в массив пока результат ≠ ноль сделать следующий := результат.следующий; результат.следующий := ноль для (i = 0; (i < 32) && (array[i] ≠ nil); i += 1) сделать результат := merge(массив[i], результат) массив[i] := ноль // не выходить за конец массива если я = 32 , то я -= 1 массив[i] := результат результат := следующий // объединить массив в один список результат := ноль для (i = 0; i < 32; i += 1) сделать результат := merge(массив[i], результат) вернуть результат
Псевдокод, подобный Haskell , показывающий, как сортировка слиянием может быть реализована на таком языке с использованием конструкций и идей функционального программирования .
merge_sort :: [ a ] -> [ a ] merge_sort ( [ ] ) = [] merge_sort ( [ x ] ) = [ x ] merge_sort ( xs ) = merge ( merge_sort ( left ), merge_sort ( right )) где ( left , right ) = split ( xs , length ( xs ) / 2 ) merge :: ( [ a ], [ a ]) -> [ a ] merge ( [ ] , xs ) = xs merge ( xs , [ ] ) = xs merge ( x : xs , y : ys ) | if x ≤ y = x : merge ( xs , y : ys ) | else = y : merge ( x : xs , ys )
При сортировке n объектов сортировка слиянием имеет среднюю и худшую производительность O ( n log n ) сравнений. Если время выполнения (количество сравнений) сортировки слиянием для списка длины n равно T ( n ), то рекуррентное соотношение T ( n ) = 2 T ( n /2) + n следует из определения алгоритма (применяем алгоритм к двум спискам размером в два раза меньше исходного списка и добавляем n шагов, предпринятых для слияния двух полученных списков). [5] Закрытая форма следует из основной теоремы для рекуррентных уравнений «разделяй и властвуй» .
Число сравнений, выполняемых сортировкой слиянием в худшем случае, задается числами сортировки . Эти числа равны или немного меньше ( n ⌈ lg n ⌉ − 2 ⌈lg n ⌉ + 1), что находится между ( n lg n − n + 1) и ( n lg n + n + O(lg n )). [6] Лучший случай сортировки слиянием занимает примерно половину итераций, чем худший случай. [7]
Для больших n и случайно упорядоченного входного списка ожидаемое (среднее) число сравнений сортировки слиянием приближается к α · n меньше, чем в худшем случае, где
В худшем случае сортировка слиянием использует примерно на 39% меньше сравнений, чем быстрая сортировка в среднем случае, а с точки зрения ходов сложность сортировки слиянием в худшем случае составляет O ( n log n ) — такая же сложность, как и в лучшем случае быстрой сортировки. [7]
Сортировка слиянием более эффективна, чем быстрая сортировка для некоторых типов списков, если к сортируемым данным можно эффективно обращаться только последовательно, и поэтому она популярна в таких языках, как Lisp , где структуры данных с последовательным доступом очень распространены. В отличие от некоторых (эффективных) реализаций быстрой сортировки, сортировка слиянием является стабильной сортировкой.
Наиболее распространенная реализация сортировки слиянием не сортирует на месте; [8] поэтому для сохранения отсортированных выходных данных необходимо выделить объем памяти, соответствующий размеру входных данных (см. ниже варианты, для которых требуется только n /2 дополнительных пробелов).
Естественная сортировка слиянием похожа на сортировку слиянием снизу вверх, за исключением того, что эксплуатируются любые естественно возникающие прогоны (отсортированные последовательности) во входных данных. Могут эксплуатироваться как монотонные, так и битонные (чередующиеся вверх/вниз) прогоны, при этом списки (или эквивалентные ленты или файлы) являются удобными структурами данных (используемыми как очереди FIFO или стеки LIFO ). [9] В сортировке слиянием снизу вверх отправная точка предполагает, что каждый прогон имеет длину в один элемент. На практике случайные входные данные будут иметь много коротких прогонов, которые просто случайно отсортированы. В типичном случае естественной сортировке слиянием может не потребоваться столько проходов, потому что для слияния требуется меньше прогонов. В лучшем случае входные данные уже отсортированы (т. е. представляют собой один прогон), поэтому естественной сортировке слиянием нужно сделать только один проход по данным. Во многих практических случаях присутствуют длинные естественные прогоны, и по этой причине естественная сортировка слиянием используется как ключевой компонент Timsort . Пример:
Начало: 3 4 2 1 7 5 8 9 0 6Выберите пробежки: (3 4)(2)(1 7)(5 8 9)(0 6)Объединить: (2 3 4)(1 5 7 8 9)(0 6)Объединить: (1 2 3 4 5 7 8 9)(0 6)Объединить: (0 1 2 3 4 5 6 7 8 9)
Формально естественная сортировка слиянием называется оптимальной по количеству запусков, где — количество запусков в минус один.
Турнирные сортировки с заменой и выбором используются для сбора начальных запусков для внешних алгоритмов сортировки.
Вместо объединения двух блоков за раз, пинг-понговое слияние объединяет четыре блока за раз. Четыре отсортированных блока одновременно объединяются во вспомогательном пространстве в два отсортированных блока, затем два отсортированных блока объединяются обратно в основную память. Это позволяет исключить операцию копирования и сократить общее количество ходов вдвое. Ранняя реализация общедоступного слияния четырех блоков была разработана WikiSort в 2014 году, позже в том же году метод был описан как оптимизация для сортировки с терпением и назван пинг-понговым слиянием. [10] [11] Quadsort реализовал метод в 2020 году и назвал его quad merge. [12]
Одним из недостатков сортировки слиянием, реализованной на массивах, является ее потребность в рабочей памяти O ( n ) . Было предложено несколько методов уменьшения памяти или полной реализации сортировки слиянием на месте :
Внешняя сортировка слиянием практична для запуска с использованием дисковых или ленточных накопителей, когда данные для сортировки слишком велики для размещения в памяти . Внешняя сортировка объясняет, как сортировка слиянием реализуется с использованием дисковых накопителей. Типичная сортировка с использованием ленточных накопителей использует четыре ленточных накопителя. Весь ввод-вывод последовательный (за исключением перемоток в конце каждого прохода). Минимальная реализация может обойтись всего двумя буферами записей и несколькими программными переменными.
Называя четыре ленточных накопителя как A, B, C, D, с исходными данными на A, и используя только два буфера записи, алгоритм похож на реализацию снизу вверх, используя пары ленточных накопителей вместо массивов в памяти. Основной алгоритм можно описать следующим образом:
Вместо того, чтобы начинать с очень коротких запусков, обычно используется гибридный алгоритм , где начальный проход считывает много записей в память, выполняет внутреннюю сортировку для создания длинного запуска, а затем распределяет эти длинные запуски в выходной набор. Этот шаг позволяет избежать многих ранних проходов. Например, внутренняя сортировка 1024 записей сэкономит девять проходов. Внутренняя сортировка часто бывает большой, потому что она имеет такое преимущество. Фактически, существуют методы, которые могут сделать начальные запуски длиннее доступной внутренней памяти. Один из них, «снегоочиститель» Кнута (основанный на двоичной минимальной куче ), генерирует запуски вдвое длиннее (в среднем), чем размер используемой памяти. [18]
С некоторыми накладными расходами приведенный выше алгоритм можно модифицировать для использования трех лент. Время выполнения O ( n log n ) также может быть достигнуто с использованием двух очередей , или стека и очереди, или трех стеков. В другом направлении, используя k > двух лент (и O ( k ) элементов в памяти), мы можем сократить количество операций с лентами в O (log k ) раз, используя k/2-way merge .
Более сложная сортировка слиянием, оптимизирующая использование ленточного (и дискового) накопителя, — это полифазная сортировка слиянием .
На современных компьютерах локальность ссылок может иметь первостепенное значение в оптимизации программного обеспечения , поскольку используются многоуровневые иерархии памяти . Были предложены версии алгоритма сортировки слиянием, поддерживающие кэш , операции которого были специально выбраны для минимизации перемещения страниц в кэш памяти машины и из него. Например,Алгоритм сортировки слиянием плитки прекращает разбиение подмассивов, когда достигаются подмассивы размера S, где S — количество элементов данных, помещающихся в кэш ЦП. Каждый из этих подмассивов сортируется с помощью алгоритма сортировки на месте, такого каксортировка вставкой, чтобы предотвратить подкачки памяти, а затем обычная сортировка слиянием завершается стандартным рекурсивным способом. Этот алгоритм продемонстрировал лучшую производительность[ необходим пример ]на машинах, которые выигрывают от оптимизации кэша. (LaMarca & Ladner 1997)
Сортировка слиянием хорошо распараллеливается благодаря использованию метода «разделяй и властвуй» . За эти годы было разработано несколько различных параллельных вариантов алгоритма. Некоторые параллельные алгоритмы сортировки слиянием тесно связаны с последовательным алгоритмом слияния сверху вниз, в то время как другие имеют другую общую структуру и используют метод слияния K-way .
Последовательную процедуру сортировки слиянием можно описать в две фазы: фазу деления и фазу слияния. Первая состоит из множества рекурсивных вызовов, которые многократно выполняют один и тот же процесс деления до тех пор, пока подпоследовательности не будут тривиально отсортированы (содержат один или ни одного элемента). Интуитивный подход заключается в распараллеливании этих рекурсивных вызовов. [19] Следующий псевдокод описывает сортировку слиянием с параллельной рекурсией с использованием ключевых слов fork и join :
// Сортируем элементы от lo до hi (исключая) массива A. Алгоритм mergesort(A, lo, hi) — если lo+1 < hi, то // Два или более элементов. середина := ⌊(низ + хай) / 2⌋ fork mergesort(A, lo, mid) сортировка слиянием(A, середина, привет) присоединиться слияние(A, lo, mid, hi)
Этот алгоритм является тривиальной модификацией последовательной версии и плохо распараллеливается. Поэтому его ускорение не очень впечатляет. Он имеет диапазон , что является всего лишь улучшением на по сравнению с последовательной версией (см. Введение в алгоритмы ). Это в основном из-за метода последовательного слияния, поскольку он является узким местом параллельного выполнения.
Лучшего параллелизма можно достичь, используя алгоритм параллельного слияния . Кормен и др. представляют бинарный вариант, который объединяет две отсортированные подпоследовательности в одну отсортированную выходную последовательность. [19]
В одной из последовательностей (более длинной, если неравная длина) выбирается элемент среднего индекса. Его положение в другой последовательности определяется таким образом, что эта последовательность осталась бы отсортированной, если бы этот элемент был вставлен в эту позицию. Таким образом, известно, сколько других элементов из обеих последовательностей меньше, и можно вычислить положение выбранного элемента в выходной последовательности. Для созданных таким образом частичных последовательностей меньших и больших элементов алгоритм слияния снова выполняется параллельно, пока не будет достигнут базовый случай рекурсии.
Следующий псевдокод демонстрирует модифицированный метод сортировки параллельным слиянием с использованием алгоритма параллельного слияния (взятого из работы Кормена и др.).
/** * A: Входной массив * B: Выходной массив * lo: нижняя граница * hi: верхняя граница * выкл.: смещение */алгоритм parallelMergesort(A, lo, hi, B, off) — это len := hi - lo + 1 если len == 1 тогда B[выкл] := A[ло] иначе пусть T[1..len] будет новым массивом середина := ⌊(низ + хай) / 2⌋ середина := середина - ло + 1 fork parallelMergesort(A, lo, mid, T, 1) parallelMergesort(A, середина + 1, привет, T, середина' + 1) присоединиться parallelMerge(T, 1, mid', mid' + 1, len, B, off)
Чтобы проанализировать рекуррентное отношение для наихудшего случая, рекурсивные вызовы parallelMergesort должны быть включены только один раз из-за их параллельного выполнения, получая
Подробную информацию о сложности процедуры параллельного слияния см. в разделе Алгоритм слияния .
Решение этого повторения дается формулой
Этот параллельный алгоритм слияния достигает параллелизма , что намного выше параллелизма предыдущего алгоритма. Такая сортировка может хорошо работать на практике, если сочетать ее с быстрой стабильной последовательной сортировкой, такой как сортировка вставкой , и быстрым последовательным слиянием в качестве базового случая для слияния небольших массивов. [20]
Кажется произвольным ограничивать алгоритмы сортировки слиянием методом бинарного слияния, поскольку обычно доступно p > 2 процессоров. Лучшим подходом может быть использование метода слияния K-way , обобщения бинарного слияния, в котором отсортированные последовательности объединяются. Этот вариант слияния хорошо подходит для описания алгоритма сортировки на PRAM . [21] [22]
Учитывая неотсортированную последовательность элементов, цель состоит в том, чтобы отсортировать последовательность с доступными процессорами . Эти элементы распределяются равномерно между всеми процессорами и сортируются локально с использованием последовательного алгоритма сортировки . Следовательно, последовательность состоит из отсортированных последовательностей длины . Для упрощения пусть будет кратным , так что для .
Эти последовательности будут использоваться для выполнения выбора мультипоследовательности/выбора разделителя. Для алгоритм определяет элементы разделителя с глобальным рангом . Затем соответствующие позиции в каждой последовательности определяются с помощью бинарного поиска и, таким образом, далее разбиваются на подпоследовательности с .
Кроме того, элементы назначаются процессору , то есть все элементы между рангом и рангом , которые распределены по всем . Таким образом, каждый процессор получает последовательность отсортированных последовательностей. Тот факт, что ранг элементов-разделителей был выбран глобально, обеспечивает два важных свойства: с одной стороны, был выбран таким образом, чтобы каждый процессор мог по-прежнему работать с элементами после назначения. Алгоритм идеально сбалансирован по нагрузке . С другой стороны, все элементы на процессоре меньше или равны всем элементам на процессоре . Следовательно, каждый процессор выполняет p -way merge локально и, таким образом, получает отсортированную последовательность из своих подпоследовательностей. Из-за второго свойства не нужно выполнять никаких дополнительных p -way-merge, результаты нужно только объединить в порядке номера процессора.
В простейшей форме, учитывая отсортированные последовательности, равномерно распределенные по процессорам, и ранг , задача состоит в том, чтобы найти элемент с глобальным рангом в объединении последовательностей. Следовательно, это можно использовать для разделения каждой из них на две части по индексу разделителя , где нижняя часть содержит только элементы, которые меньше , в то время как элементы, большие , расположены в верхней части.
Представленный последовательный алгоритм возвращает индексы разделений в каждой последовательности, например, индексы в последовательностях , имеющих глобальный ранг меньше и . [23]
алгоритм msSelect(S : Массив отсортированных последовательностей [S_1,..,S_p], k : int) для i = 1 до p do (l_i, r_i) = (0, |S_i|-1) пока существует i: l_i < r_i делать// выбрать элемент Pivot в S_j[l_j], .., S_j[r_j], выбрать случайный j равномерноv := pickPivot(S, l, r)для i = 1 до p сделать m_i = binarySearch(v, S_i[l_i, r_i]) // последовательноесли m_1 + ... + m_p >= k , то // m_1+ ... + m_p — глобальный ранг v r := m // векторное назначениееще л := м вернуться л
Для анализа сложности выбрана модель PRAM . Если данные равномерно распределены по всем , p-кратное выполнение метода binarySearch имеет время выполнения . Ожидаемая глубина рекурсии такая же, как в обычном Quickselect . Таким образом, общее ожидаемое время выполнения составляет .
Примененный к параллельной многоканальной сортировке слиянием, этот алгоритм должен быть вызван параллельно, так что все элементы-разделители ранга для будут найдены одновременно. Затем эти элементы-разделители могут быть использованы для разбиения каждой последовательности на части с тем же общим временем выполнения .
Ниже приведен полный псевдокод алгоритма параллельной многоканальной сортировки слиянием. Мы предполагаем, что существует барьерная синхронизация до и после выбора многопоследовательности, так что каждый процессор может правильно определить элементы разделения и раздел последовательности.
/** * d: Несортированный массив элементов * n: Количество элементов * p: Количество процессоров * вернуть отсортированный массив */алгоритм parallelMultiwayMergesort(d : Array, n : int, p : int) is o := new Array[0, n] // выходной массив для i = 1 до p выполняется параллельно // каждый процессор параллельно S_i := d[(i-1) * n/p, i * n/p] // Последовательность длины n/psort(S_i) // сортировка локально синхронизироватьv_i := msSelect([S_1,...,S_p], i * n/p) // элемент с глобальным рангом i * n/p синхронизировать(S_i,1, ..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // разбить s_i на подпоследовательности o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // объединить и присвоить выходному массиву вернуться о
Во-первых, каждый процессор локально сортирует назначенные элементы, используя алгоритм сортировки со сложностью . После этого элементы разделителя должны быть рассчитаны за время . Наконец, каждая группа разделителей должна быть объединена параллельно каждым процессором со временем выполнения с использованием последовательного алгоритма слияния p-way . Таким образом, общее время выполнения определяется как
.
Алгоритм многоканальной сортировки слиянием очень масштабируем благодаря своей высокой способности к параллелизации, что позволяет использовать много процессоров. Это делает алгоритм жизнеспособным кандидатом для сортировки больших объемов данных, таких как те, которые обрабатываются в компьютерных кластерах . Кроме того, поскольку в таких системах память обычно не является ограничивающим ресурсом, недостаток сложности пространства сортировки слиянием незначителен. Однако в таких системах становятся важными другие факторы, которые не учитываются при моделировании на PRAM . Здесь необходимо учитывать следующие аспекты: Иерархия памяти , когда данные не помещаются в кэш процессоров, или накладные расходы на обмен данными между процессорами, которые могут стать узким местом, когда к данным больше нельзя получить доступ через общую память.
Сандерс и др. представили в своей статье синхронный параллельный алгоритм для многоуровневой многоканальной сортировки слиянием, который делит процессоры на группы размером . Все процессоры сначала сортируют локально. В отличие от одноуровневой многоканальной сортировки слиянием, эти последовательности затем разделяются на части и назначаются соответствующим группам процессоров. Эти шаги повторяются рекурсивно в этих группах. Это уменьшает коммуникацию и, в частности, позволяет избежать проблем с множеством небольших сообщений. Иерархическая структура базовой реальной сети может использоваться для определения групп процессоров (например, стоек , кластеров ,...). [22]
Сортировка слиянием была одним из первых алгоритмов сортировки, где было достигнуто оптимальное ускорение, с Ричардом Коулом, использовавшим умный алгоритм подвыборки для обеспечения слияния O (1). [24] Другие сложные параллельные алгоритмы сортировки могут достигать тех же или лучших временных границ с меньшей константой. Например, в 1991 году Дэвид Пауэрс описал параллелизованную быструю сортировку (и связанную с ней радиксную сортировку ), которая может работать за время O (log n ) на параллельной машине с произвольным доступом (PRAM) CRCW с n процессорами, выполняя неявное разбиение. [25] Пауэрс далее показывает, что конвейерная версия битонической слияния Батчера за время O ((log n ) 2 ) на сети сортировки бабочкой на практике фактически быстрее, чем его сортировки O (log n ) на PRAM, и он приводит подробное обсуждение скрытых накладных расходов в сравнении, радиксной и параллельной сортировки. [26]
Хотя пирамидальная сортировка имеет те же временные ограничения, что и сортировка слиянием, она требует только Θ(1) вспомогательного пространства вместо Θ( n ) сортировки слиянием. На типичных современных архитектурах эффективные реализации быстрой сортировки обычно превосходят сортировку слиянием для сортировки массивов на основе ОЗУ. [27] Быстрая сортировка предпочтительна, когда размер сортируемых данных меньше, поскольку сложность пространства для быстрой сортировки составляет O(log n ), она помогает лучше использовать локальность кэша, чем сортировка слиянием (со сложностью пространства O(n)). [27] С другой стороны, сортировка слиянием является стабильной сортировкой и более эффективна при обработке последовательных носителей с медленным доступом. Сортировка слиянием часто является наилучшим выбором для сортировки связанного списка : в этой ситуации сравнительно легко реализовать сортировку слиянием таким образом, что для нее потребуется всего лишь Θ(1) дополнительного пространства, а медленная производительность случайного доступа связанного списка делает некоторые другие алгоритмы (например, быструю сортировку) плохо работающими, а другие (например, пирамидальную сортировку) вообще невозможными.
Начиная с Perl 5.8, сортировка слиянием является алгоритмом сортировки по умолчанию (в предыдущих версиях Perl это была быстрая сортировка). [28] В Java методы Arrays.sort() используют сортировку слиянием или настроенную быструю сортировку в зависимости от типов данных, а для эффективности реализации переключаются на сортировку вставкой, когда сортируется менее семи элементов массива. [29] Ядро Linux использует сортировку слиянием для своих связанных списков. [30]
Timsort , настроенный гибрид сортировки слиянием и сортировки вставкой, используется в различных программных платформах и языках, включая платформы Java и Android [31] и используется в Python с версии 2.3; с версии 3.11 политика слияния Timsort была обновлена до Powersort. [32]