Алгоритмы слияния — это семейство алгоритмов , которые принимают на вход несколько отсортированных списков и создают на выходе один список, содержащий все элементы входных списков в отсортированном порядке. Эти алгоритмы используются в качестве подпрограмм в различных алгоритмах сортировки , наиболее известный из которых — сортировка слиянием .
Алгоритм слияния играет решающую роль в алгоритме сортировки слиянием , алгоритме сортировки на основе сравнения . Концептуально алгоритм сортировки слиянием состоит из двух этапов:
Алгоритм слияния неоднократно используется в алгоритме сортировки слиянием.
Пример сортировки слиянием приведен на иллюстрации. Он начинается с несортированного массива из 7 целых чисел. Массив разделен на 7 разделов; каждый раздел содержит 1 элемент и отсортирован. Затем отсортированные разделы объединяются для создания более крупных отсортированных разделов, пока не останется один раздел — отсортированный массив.
Объединение двух отсортированных списков в один может осуществляться в линейном времени и линейном или постоянном пространстве (в зависимости от модели доступа к данным). Следующий псевдокод демонстрирует алгоритм, который объединяет входные списки ( связанные списки или массивы ) A и B в новый список C. [1] [2] : 104 Заголовок функции возвращает первый элемент списка; «Удалить» элемент означает удалить его из списка, обычно путем увеличения указателя или индекса.
алгоритм merge(A, B) — это входные данные A, B: список возвращает список C := новый пустой список пока A не пусто и B не пусто, сделать , если head(A) ≤ head(B) , то добавить заголовок (A) к C уронить голову А еще добавить заголовок (B) к C уронить голову Б // К этому моменту либо A, либо B пусто. Осталось очистить другой входной список. пока A не пусто, сделайте добавить заголовок (A) к C уронить голову А пока B не пусто, сделайте добавить заголовок (B) к C уронить голову Б вернуть С
Когда входные данные представляют собой связанные списки, этот алгоритм можно реализовать для использования только постоянного объема рабочего пространства; указатели в узлах списков можно повторно использовать для бухгалтерского учета и для построения окончательного объединенного списка.
В алгоритме сортировки слиянием эта подпрограмма обычно используется для объединения двух подмассивов A[lo..mid] , A[mid+1..hi] одного массива A . Это можно сделать, скопировав подмассивы во временный массив, а затем применив описанный выше алгоритм слияния. [1] Выделения временного массива можно избежать, но за счет скорости и простоты программирования. Были разработаны различные алгоритмы слияния на месте, [3] иногда жертвуя привязкой к линейному времени для получения алгоритма O ( n log n ) ; [4] см. раздел «Сортировка слиянием» § Варианты для обсуждения.
k -способ слияния обобщает двоичное слияние до произвольного числа k отсортированных входных списков. Приложения слияния k -способов возникают в различных алгоритмах сортировки, включая терпеливую сортировку [5] и внешний алгоритм сортировки, который делит входные данные на k =1/М− 1 блок, который помещается в память, сортирует их один за другим, затем объединяет эти блоки. [2] : 119–120
Существует несколько решений этой проблемы. Наивное решение — выполнить цикл по k спискам, чтобы каждый раз выбирать минимальный элемент, и повторять этот цикл, пока все списки не станут пустыми:
В худшем случае этот алгоритм выполняет ( k −1)( n −к/2) сравнения элементов для выполнения своей работы, если в списках всего n элементов. [6] Его можно улучшить, сохраняя списки в очереди с приоритетами ( min-heap ), ключом которой является их первый элемент:
Поиск следующего наименьшего элемента для вывода (find-min) и восстановление порядка кучи теперь можно выполнить за время O (log k ) (более конкретно, 2⌊log k ⌋ сравнения [6] ), и вся проблема может быть решена так: решено за время O ( n log k ) (приблизительно 2 n ⌊log k ⌋ сравнений). [6] [2] : 119–120
Третий алгоритм решения проблемы — это решение «разделяй и властвуй» , основанное на алгоритме двоичного слияния:
Когда входные списки для этого алгоритма упорядочены по длине, сначала самый короткий, это требует меньше, чем n ⌈log k ⌉ сравнений, т. е. меньше половины числа, используемого алгоритмом на основе кучи; на практике он может быть таким же быстрым или медленным, как алгоритм на основе кучи. [6]
Параллельная версия алгоритма двоичного слияния может служить строительным блоком параллельной сортировки слиянием . Следующий псевдокод демонстрирует этот алгоритм в параллельном стиле «разделяй и властвуй» (адаптировано из Cormen et al. [7] : 800 ). Он работает с двумя отсортированными массивами A и B и записывает отсортированный вывод в массив C. Обозначение A[i...j] обозначает часть A от индекса i до j , исключая.
алгоритм merge(A[i...j], B[k...ℓ], C[p...q]) — это входные данные A, B, C: массив i, j, k, ℓ, p, q: индексы пусть m = j - i, п = ℓ - к если m < n , то поменяйте местами A и B // убедитесь, что A — больший массив: i, j по-прежнему принадлежат A; к, ℓ к B поменять местами м и н если m ≤ 0 , то вернуть // базовый случай, объединять нечего пусть r = ⌊(i + j)/2⌋ пусть s = двоичный поиск (A[r], B[k...ℓ]), пусть t = p + (r - i) + (s - k) С[т] = А[г] параллельно делаю merge(A[i...r], B[k...s], C[p...t]) merge(A[r+1...j], B[s...ℓ], C[t+1...q])
Алгоритм работает путем разделения A или B (в зависимости от того, что больше) на (почти) равные половины. Затем он разбивает другой массив на часть со значениями, меньшими, чем середина первого, и часть с большими или равными значениями. ( Подпрограмма двоичного поиска возвращает индекс в B , где было бы A [ r ] , если бы оно было в B ; что это всегда число между k и ℓ .) Наконец, каждая пара половин рекурсивно объединяется , и поскольку рекурсивные вызовы независимы друг от друга, их можно выполнять параллельно. Гибридный подход, в котором последовательный алгоритм используется для базового случая рекурсии, показал себя хорошо на практике [8]
Работа , выполняемая алгоритмом для двух массивов, содержащих в общей сложности n элементов, т. е. время работы его последовательной версии, равна O ( n ) . Это оптимально, поскольку в C необходимо скопировать n элементов . Чтобы вычислить диапазон алгоритма, необходимо вывести Рекуррентное соотношение . Поскольку два рекурсивных вызова слияния выполняются параллельно, необходимо учитывать только более затратный из двух вызовов. В худшем случае максимальное количество элементов в одном из рекурсивных вызовов не превышает, поскольку массив с большим количеством элементов идеально разбивается пополам. Добавляя стоимость двоичного поиска, мы получаем эту повторяемость как верхнюю границу:
Решение — это означает, что на идеальной машине с неограниченным количеством процессоров требуется столько же времени. [7] : 801–802.
Примечание. Процедура нестабильна : если равные элементы разделены разделением A и B , они будут чередоваться в C ; также замена A и B разрушит порядок, если одинаковые элементы распределены между обоими входными массивами. В результате при использовании этого алгоритма для сортировки сортировка становится нестабильной.
Существуют также алгоритмы, которые обеспечивают параллелизм в одном экземпляре слияния двух отсортированных списков. Их можно использовать в программируемых пользователем вентильных матрицах ( FPGA ), специализированных схемах сортировки, а также в современных процессорах с инструкциями с одной командой и несколькими данными ( SIMD ).
Существующие параллельные алгоритмы основаны на модификациях слияния битонического сортировщика или сортировки слиянием нечетно-четных . [9] В 2018 году Сайто М. и др. представила MMS [10] для FPGA, направленную на устранение канала передачи данных с многоцикловой обратной связью, который препятствовал эффективной конвейерной обработке в аппаратном обеспечении. Также в 2018 г. Папафилиппу П. и др. представила FLiMS [9] , которая улучшила использование аппаратного обеспечения и производительность, требуя только объединения этапов конвейера блоков сравнения и замены P/2 с параллелизмом P- элементов за цикл FPGA.
Некоторые языки программирования предоставляют встроенную или библиотечную поддержку объединения отсортированных коллекций .
В стандартной библиотеке шаблонов C ++ есть функция std::merge , которая объединяет два отсортированных диапазона итераторов , и std::inplace_merge , которая объединяет два последовательных отсортированных диапазона на месте . Кроме того, класс std::list (связанный список) имеет собственный метод слияния , который объединяет другой список в себя. Тип объединяемых элементов должен поддерживать оператор «меньше» ( < ) или должен быть снабжен пользовательским компаратором.
C++17 допускает различные политики выполнения, а именно последовательную, параллельную и параллельно-неупорядоченную. [11]
Стандартная библиотека Python (начиная с версии 2.6) также имеет функцию слияния в модуле heapq , которая принимает несколько отсортированных итераций и объединяет их в один итератор. [12]