Подтвердить что ты не робот

Способ merge-sort быстрее, чем вставлять-сортировать головоломки меня

Просто мои ноги были мокрые в алгоритме сортировки с Haskell. Я реализовал insert-sort и merge-sort

insert_sort :: (Ord a, Show a) => [a] -> [a]
insert_sort keys = foldr f [] keys
           where f key []        = [key]
                 f key acc       = insert key acc
                 insert y []     = [y]
                 insert y (x:xs)
                     | x < y     = x : insert y xs
                     | otherwise = y : x : xs

merge_sort :: (Ord a, Show a) => [a] -> [a]
merge_sort (x:[]) = [x]
merge_sort keys   = merge  (merge_sort (take len keys)) (merge_sort (drop len keys))
      where len         = length keys `div` 2
            merge :: [a] -> [a] -> [a]
            merge (x:xs) []     = (x:xs)
            merge []     (y:ys) = (y:ys)
            merge (x:xs) (y:ys) = if x <= y
                                  then x : merge (xs) (y:ys)
                                  else y : merge (x:xs) ys

Вот как я сравнил их эффективность:

insert_sort $ take 100000 $ randomRs (1,100000) $ mkStdGen 1 ::[Int]
merge_sort $ take 100000 $ randomRs (1,100000) $ mkStdGen 1 ::[Int]

Оба из них начинают печатать результаты после короткой задержки, но сортировка слияния выполняется намного быстрее. Как мы знаем, merge-sort намного быстрее, чем сортировка вставки для больших наборов данных. Я думал, что это будет показано тем, как они дают результаты (например, длинная задержка по сравнению с короткими), а не то, как они печатают результаты. Это потому, что я использую foldr в insert-sort? Что за сценой?

EDIT: спасибо ребята. Я слышал о ленивой оценке, так как я начал изучать Haskell, но все-таки понял это. Кто-нибудь проиллюстрирует немного больше небольшим набором данных, скажем [5,2,6,3,1,4]? Как можно выводить результаты до завершения сортировки с помощью foldr, так как наконец появляются первые элементы?

4b9b3361

Ответ 1

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

В соответствии с запросом: как сортировка [5,2,6,3,1,4] продолжается. Я использую insert_sort = foldr ins [] для краткости.

insert_sort [5,2,6,3,1,4]
  = foldr ins [] [5,2,6,3,1,4]
  = 5 `ins` foldr ins [] [2,6,3,1,4]
  = 5 `ins` 2 `ins` [6,3,1,4] ...
  = 5 `ins` 2 `ins` 6 `ins` 3 `ins` 1 `ins` 4 `ins` []
  = 5 `ins` 2 `ins` 6 `ins` 3 `ins` 1 `ins` (4:[])
  = 5 `ins` 2 `ins` 6 `ins` 3 `ins` (1:4:[])
  = 5 `ins` 2 `ins` 6 `ins` (1 : (3 `ins` (4:[])))
  = 5 `ins` 2 `ins` (1 : (6 `ins` (3 `ins` (4:[]))))
  = 5 `ins` (1 : (2 `ins` (6 `ins` (3 `ins` (4:[])))))
  = 1 : (5 `ins` (2 `ins` (6 `ins` (3 `ins` (4:[])))))  -- now 1 can be output
  = 1 : (5 `ins` (2 `ins` (6 `ins` (3:4:[]))))
  = 1 : (5 `ins` (2 `ins` (3 : (6 `ins` (4:[])))))
  = 1 : (5 `ins` (2 : (3 : (6 `ins` (4:[])))))
  = 1 : 2 : (5 `ins` (3 : (6 `ins` (4:[]))))            -- now 2 can be output
  = 1 : 2 : 3 : (5 `ins` (6 `ins` (4:[])))              -- now 3
  = 1 : 2 : 3 : (5 `ins` (4:6:[]))
  = 1 : 2 : 3 : 4 : (5 `ins` (6:[]))                    -- now 4
  = 1 : 2 : 3 : 4 : 5 : 6 : []                          -- done

И сортировка слияния (аббревиатуры: merge = mg, merge_sort = ms):

merge_sort [5,2,6,3,1,4]
  = mg (ms [5,2,6]) (ms [3,1,4])
  = mg (mg (ms [5]) (ms [2,6])) (mg (ms [3]) (ms [1,4]))
  = mg (mg [5] (mg [2] [6])) (mg [3] (mg [1] [4]))
  = mg (mg [5] [2,6]) (mg [3] [1,4])
  = mg (2 : mg [5] [6]) (1 : mg [3] [4])
  = 1 : mg (2 : mg [5] [6]) (mg [3] [4])                -- now 1 can be output
  = 1 : mg (2 : mg [5] [6]) [3,4]
  = 1 : 2 : mg (mg [5] [6]) [3,4]                       -- now 2 can be output
  = 1 : 2 : mg [5,6] [3,4]
  = 1 : 2 : 3 : mg [5,6] [4]                            -- now 3
  = 1 : 2 : 3 : 4 : mg [5,6] []                         -- now 4
  = 1 : 2 : 3 : 4 : 5 : 6 : []                          -- now 5 and 6

По общему признанию, я сделал несколько коротких сокращений, но Haskell - не единственный ленивый.

Ответ 2

ОК здесь провалится. Вы хотите, чтобы я распечатывал:

merge_sort $ take 100000 $ randomRs (1,100000) $ mkStdGen 1 ::[Int]

Я знаю, что это список. Итак, сначала я распечатаю открытую скобку

[

Затем я буду искать первый элемент списка, распечатать его, а затем запятую. Это означает, что я должен начать оценивать это выражение, пока не смогу выяснить, что такое первый элемент списка.

merge_sort THUNK0

Ну, теперь мне нужно сопоставить шаблон. Либо THUNK соответствует (x:[]), либо нет. Но я еще не знаю. Поэтому я немного оцениваю этот бит. Я делаю, что thunk производит первые два случайных числа (из 100000). Теперь я знаю, что это не соответствует первому определению, поэтому я беру второе определение merge_sort.

merge_sort keys = merge THUNK1 THUNK2 -- keys = THUNK0

Хорошо, что это достаточно просто... это просто призыв к слиянию. Я расширю это определение. О, черт возьми, есть три разных шаблона, которые могут совпадать. Думаю, я должен немного оценить THUNK1 и посмотреть, совпадает ли он с первым шаблоном определения, (x:xs)

merge_sort (take THUNK3 THUNK0)

Вернемся к merge_sort снова, не так ли? Это означает, что мне нужно оценить (take THUNK3 THUNK0) достаточно, чтобы определить, соответствует ли она (x:[]) или нет. О CRAP. take строго в своем первом аргументе... это означает, что я должен полностью оценить THUNK3. Хорошо... глубокие вдохи...

len = length THUNK0 `div` 2

Теперь вот раздражающий случай. Чтобы вычислить length на THUNK0 (который является списком), мне нужно расширить WHOLE SPINE. Мне не нужно вычислять значения внутри внутри, но мне нужно определить структуру всего списка. Это, конечно же, выполняется по одному шаблону за раз, определяя, является ли оно [] или (x:xs). Но в целом length является "строгим строгом".

короткая пауза, в то время как я формирую позвоночник списка из 100 000 элементов

Фу, получилось. Теперь я знаю длину, что означает, что я знаю len = 500000. THUNK0, наконец, полностью оценен! Уф! Где я?

merge_sort (take 500000 THUNK3)

И так далее. merge_sort будет продолжать стараться быть максимально ленивым. Рекурсивные вызовы merge_sort будут максимально ленивыми. В конце концов, чтобы определить самый первый элемент внешнего merge_sort, нам нужно будет узнать самый первый элемент обоих рекурсивных вызовов merge_sort. И чтобы знать первый элемент этих... нам понадобится первый элемент последующих рекурсивных вызовов и т.д. Таким образом, будет выполнена работа O (n), поскольку каждый элемент должен быть оценен (выполнение генерации случайных чисел для каждого).

Тогда подумайте об этом, как о турнире. Каждый элемент соединен с другим элементом. "Победившие" (самые низкие) элементы переходят к следующему раунду (становясь первым элементом рекурсивного вызова к младшему merge_sort s). Существует еще одно соревнование с участием 1/2 бойцов, и 1/2 из них (1/4 от общего числа) переходят к следующему раунду и т.д. Это также оказывается работой O (n), поскольку ( n/2) сравнения выполняются в течение первого раунда, а последующие раунды становятся слишком быстрыми, чтобы быть значительными. (Сумма 1/2 + 1/4 + 1/8... сходится при 1, что означает, что выполняется n сравнений.)

В общем, необходимо выполнить работу O (n), чтобы, наконец, создать первый элемент. Для последующих элементов необходимо выполнить дополнительную работу, но общий объем работы заканчивается O (n log (n)).


Теперь сравним это с insert_sort. Просто подумайте о том, как это работает: он перемещает список и "вставляет" каждый элемент в отсортированный список. Это означает, что вы не можете точно знать, что первый элемент сортировки, пока вы не выполнили последний бит работы, и вставили последний элемент (который мог быть самым низким) в отсортированный список.

Надеюсь, это наглядно иллюстрирует, как merge_sort не нужно выполнять всю работу, чтобы начать создавать результаты, а insert_sort делает.