Это правильно реализованная сортировка слиянием в Haskell?

Я не смог найти свой код нигде в сети, поэтому не могли бы вы сказать мне, почему или почему функция myMergeSort не является сортировкой слиянием? Я знаю, что моя функция myMergeSort сортирует, но я не уверен, действительно ли она сортирует с использованием алгоритма сортировки слиянием или это другой алгоритм. Я только начал с Haskell несколько дней назад.

merge xs [] = xs
merge [] ys = ys
merge (x : xs) (y : ys)
    | x <= y = x : merge xs (y : ys)
    | otherwise = y : merge (x : xs) ys

myMergeSort :: [Int] -> [Int]
myMergeSort [] = []
myMergeSort (x:[]) = [x]
myMergeSort (x:xs) = foldl merge [] (map (\x -> [x]) (x:xs))

У меня нет вопросов о функции объединения.

Следующая функция mergeSortOfficial была представленным нам решением, я понимаю это, но не уверен, правильно ли я реализую алгоритм сортировки слиянием в моей функции myMergeSort или нет.

Официальное решение - реализация:

mergeSortOfficial [] = []
mergeSortOfficial (x : []) = [x]
mergeSortOfficial xs = merge
    (mergeSortOfficial (take ((length xs) ‘div‘ 2) xs))
    (mergeSortOfficial (drop ((length xs) ‘div‘ 2) xs))

person user3069376    schedule 11.03.2015    source источник
comment
Независимо от того, действительно ли mergeSortOfficial реализует MergeSort или нет, меня поражает то, что он пересчитывает длину списка (дважды!) при каждом вызове. Это очень неэффективно; длина должна быть вычислена один раз и навсегда в начале.   -  person jub0bs    schedule 11.03.2015
comment
Разве это не относится к обзору кода?   -  person Squidly    schedule 11.03.2015
comment
Сортировка слиянием делит ввод на две равные (или почти равные) части. Это важно. Вы всегда объединяете со списком из одного элемента. Это не имеет требуемой асимптотической сложности.   -  person n. 1.8e9-where's-my-share m.    schedule 11.03.2015
comment
@MrBones не совсем так. Этот код не работает для некоторого определения работы, поэтому вопрос здесь.   -  person n. 1.8e9-where's-my-share m.    schedule 11.03.2015
comment
Если вы хотите работать снизу вверх из списка одноэлементных списков, вы можете это сделать. У вас есть список списков. Объедините каждый элемент с четным номером с его соседом с нечетным номером. Повторяйте, пока у вас не останется только один список.   -  person n. 1.8e9-where's-my-share m.    schedule 11.03.2015
comment
Снизу вверх, безусловно, путь. Тогда вам вообще не нужно будет считать элементы.   -  person Carl    schedule 11.03.2015
comment
Я считаю, что это замаскированная сортировка вставками: слияния выполняются в форме merge list [x], что равносильно вставке x в упорядоченное list. Текущий код выглядит O(n^2).   -  person chi    schedule 11.03.2015
comment
стабильная восходящая сортировка слиянием: en.wikipedia.org/w/   -  person Will Ness    schedule 11.03.2015


Ответы (2)


myMergeSort не является правильной сортировкой слиянием. Однако это правильная сортировка вставками. Мы начинаем с пустого списка, затем вставляем элементы один за другим в правильную позицию:

myMergeSort [2, 1, 4, 3] == 
foldl merge [] [[2], [1], [4], [3]] ==
((([] `merge` [2]) `merge` [1]) `merge` [4]) `merge` [3] == 
(([2] `merge` [1]) `merge` [4]) `merge` [3]
([1, 2] `merge` [4]) `merge` [3] == 
[1, 2, 4] `merge` [3] == 
[1, 2, 3, 4]

Поскольку каждая вставка занимает линейное время, вся сортировка является квадратичной.

mergeSortOfficial технически правильно, но неэффективно. length занимает линейное время и вызывается на каждом уровне рекурсии для общей длины списка. take и drop также линейны. Общая сложность остается оптимальной n * log n, но мы делаем пару ненужных кругов.

Если мы придерживаемся слияния сверху вниз, мы могли бы добиться большего успеха, разделив список на список элементов с четными индексами и другой список с нечетными индексами. Разделение по-прежнему линейное, но это только один обход вместо двух (length, а затем take/drop в сортировке official).

split :: [a] -> ([a], [a])
split = go [] [] where
  go as bs []     = (as, bs)
  go as bs (x:xs) = go (x:bs) as xs

mergeSortOfficial :: [Int] -> [Int]
mergeSortOfficial [] = []
mergeSortOfficial (x : []) = [x]
mergeSortOfficial xs = 
  let (as, bs) = split xs in
    merge (mergeSortOfficial as) (mergeSortOfficial bs)

Как отметил УиллНесс в комментариях, приведенный выше split дает нестабильную сортировку. Мы можем использовать стабильную альтернативу:

import Control.Arrow

stableSplit :: [a] -> ([a], [a])
stableSplit xs = go xs xs where
    go (x:xs) (_:_:ys) = first (x:) (go xs ys)
    go xs     ys       = ([], xs)

Лучший способ, вероятно, сделать слияние снизу вверх. Это подход sort в Data.List занимает. Здесь мы объединяем последовательные пары списков, пока не останется только один список:

mergeSort :: Ord a => [a] -> [a]
mergeSort [] = []
mergeSort xs = mergeAll (map (:[]) xs) where
    mergePairs (x:y:ys) = merge x y : mergePairs ys
    mergePairs xs       = xs

    mergeAll [xs] = xs
    mergeAll xs   = mergeAll (mergePairs xs)

Data.List.sort работает в основном так же, как и выше, за исключением того, что он начинается с поиска нисходящих и восходящих последовательностей во входных данных, а не просто создания одноэлементных списков из элементов.

person András Kovács    schedule 11.03.2015
comment
с этим искривленным разделением сверху вниз сортировка не стабильна. Лучше разделить пополам, вообще не измеряя длину, перемещая два указателя, один со скоростью, в два раза превышающей другой. снизу вверх лучше всего, особенно. так как вы можете отделить запуски в начальном проходе, как это делается в список ордеров данных nubSort. - person Will Ness; 11.03.2015
comment
Спасибо за объяснение. Небольшая вещь: конечно, ваша вторая строка должна быть: foldl merge [] [[2], [1], [4], [3]] == вместо foldl merge [] [[1], [2] , [3], [4]] == - person user3069376; 11.03.2015
comment
@WillNess, это очень хорошая идея! Я добавил такой раскол в ответ. - person András Kovács; 11.03.2015
comment
@ AndrásKovács Где определено merge в вашем последнем блоке кода? - person jub0bs; 12.03.2015
comment
@Jubobs предполагается так же, как и в вопросе. - person András Kovács; 12.03.2015
comment
@ AndrásKovács Спасибо за разъяснение. - person jub0bs; 12.03.2015

Нет, это не сортировка слиянием. Это insertionSort, который по сути является тем же алгоритмом, что и bubbleSort, в зависимости от того, как вы на него смотрите. На каждом шаге список синглтона равен merged накопленному упорядоченному списку на данный момент, поэтому фактически вставляется элемент этого синглтона.

Как уже заметили другие комментаторы, чтобы получить mergeSort (и, в частности, его эффективность), необходимо неоднократно делить задачу на примерно равные части (а не на «один элемент» и «остальные») . «Официальное» решение дает довольно неуклюжий способ сделать это. мне очень нравится

foldr (\ x (ys, zs) -> (x : zs, ys)) ([], [])

как способ разделить список на две части, но не посередине, а на элементы в четных и нечетных позициях.

Если, как и я, вам нравится иметь структуру на видном месте, вы можете сделать упорядоченные списки Monoid.

import Data.Monoid
import Data.Foldable
import Control.Newtype

newtype Merge x = Merge {merged :: [x]}
instance Newtype (Merge x) [x] where
  pack = Merge
  unpack = merged

instance Ord x => Monoid (Merge x) where
  mempty = Merge []
  mappend (Merge xs) (Merge ys) = Merge (merge xs ys) where
    -- merge is as you defined it

И теперь у вас есть сортировка вставками только по

ala' Merge foldMap (:[]) :: [x] -> [x]

Один из способов получить структуру mergeSort по принципу «разделяй и властвуй» — сделать ее структурой данных: бинарными деревьями.

data Tree x = None | One x | Node (Tree x) (Tree x) deriving Foldable

Я не применял здесь инвариант балансировки, но мог бы. Дело в том, что та же операция, что и раньше, имеет другой тип

ala' Merge foldMap (:[]) :: Tree x -> [x]

который объединяет списки, собранные из древовидного расположения элементов. Чтобы получить указанные договоренности, подумайте "какие минусы для Tree?" и убедитесь, что вы сохраняете равновесие, используя ту же извилистость, которую я использовал в приведенной выше операции «разделения».

twistin :: x -> Tree x -> Tree x   -- a very cons-like type
twistin x None        = One x
twistin x (One y)     = Node (One x) (One y)
twistin x (Node l r)  = Node (twistin x r) l

Теперь у вас есть mergeSort путем построения двоичного дерева, а затем его слияния.

mergeSort :: Ord x => [x] -> [x]
mergeSort = ala' Merge foldMap (:[]) . foldr twistin None

Конечно, введение промежуточной структуры данных имеет любопытное значение, но вы можете легко вырезать его и получить что-то вроде

mergeSort :: Ord x => [x] -> [x]
mergeSort []   = []
mergeSort [x]  = [x]
mergeSort xs   = merge (mergeSort ys) (mergeSort zs) where
  (ys, zs) = foldr (\ x (ys, zs) -> (x : zs, ys)) ([], []) xs

где дерево стало рекурсивной структурой программы.

person pigworker    schedule 11.03.2015
comment
Это сортировка вставками, которая, по сути, представляет собой тот же алгоритм, что и пузырьковая сортировка, в зависимости от того, как вы на него смотрите. Как вы смотрите на него? Пузырьковая сортировка и сортировка вставками могут иметь квадратичную временную сложность в худшем случае, но это не один и тот же алгоритм. - person jub0bs; 11.03.2015
comment
Примечание: преимущество сортировки через Tree заключается в том, что она совершенно очевидно заканчивается (все рекурсивные вызовы являются структурными). Это может не иметь большого значения в Haskell, но это хорошо для таких языков, как Coq и Agda. - person András Kovács; 11.03.2015
comment
Это mappend ассоциативно? Я мог бы быть убежден, но это не очевидно. - person dfeuer; 11.03.2015
comment
Независимо от этого, это заставляет меня задаться вопросом, можно ли написать интересную инкрементную быструю сортировку для Data.Sequence, используя медиану медиан для выбора опорных точек и получая достаточно точности, чтобы построить 2-3 дерева. - person dfeuer; 11.03.2015
comment
@Jubobs У них одинаковая структура сравнений и один и тот же поток данных. См. этот мой связанный ответ. Мы называем это сортировкой вставками, когда концентрируемся на том, куда идет каждый вход, и мы называем ее пузырьковой сортировкой, когда фокусируемся на том, откуда поступает каждый выход, но это один и тот же процесс. - person pigworker; 11.03.2015