合并排序比插入排序更快的方式让我感到困惑

问题描述 投票:13回答:2

刚刚用Haskell进行排序算法弄湿了我的脚。我已经实现了插入排序和合并排序

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]

它们都在短暂延迟后开始打印结果,但是合并排序打印速度更快。众所周知,大数据集的合并排序比插入排序快得多。我认为这将由他们如何给出结果(如长时间延迟与短暂延迟)来显示,而不是如何打印结果。是因为我在插入排序中使用了foldr吗?背后是什么?

EDIT:谢谢。自从我开始学习Haskell以来,我就听说过懒惰的评估,但是却掌握了它。有人会用一个较小的数据集来说明更多吗,例如[5,2,6,3,1,4]?由于第一个元素最后出现,如何在用foldr完成排序之前输出结果?

algorithm sorting haskell lazy-evaluation
2个回答
14
投票

[幕后是懒惰的评估。排序列表的开始是在排序完成之前确定的,因此可以在工作完成之前将其输出。由于合并排序速度更快,因此合并排序列表的打印速度更快。

根据要求:如何进行[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 = mgmerge_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并不是唯一的懒惰。


9
投票

好的,这是细分。您要我打印出来:

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

我碰巧知道这是一个列表。首先,我将打印出一个大括号

[

然后,我将寻找列表的第一个元素,将其打印出来,然后用逗号隔开。这意味着我必须开始计算该表达式,直到我能弄清楚列表的第一个元素是什么。

merge_sort THUNK0

现在我需要进行模式匹配。 THUNK匹配(x:[])或不匹配。但是我还不知道。因此,我将对此进行评估。我让那个家伙产生前两个随机数(100000)。现在,我知道它与第一个定义不匹配,所以我将第二个定义设为merge_sort

merge_sort keys = merge THUNK1 THUNK2 -- keys = THUNK0

这很容易...这只是合并的调用。我将扩展该定义。糟糕,可能有three

个不同的模式可以匹配。我想我应该稍微评估一下THUNK1,看看它是否与第一个定义的模式匹配,(x:xs)
merge_sort (take THUNK3 THUNK0)

再次返回merge_sort,是吗?这意味着我需要对(take THUNK3 THUNK0)进行评估,以判断其是否与(x:[])相匹配。哦,CRAP。 take在其第一个参数中为strict

...这意味着我必须完全评估 THUNK3。好...深呼吸...
len = length THUNK0 `div` 2

现在这是一个令人讨厌的案例。要计算THUNK0(列表)上的length,我必须扩展整个SPINE。我不必实际计算内部的值,但是我需要充实整个列表的结构。当然,这一次完成一次模式匹配,确定是[]还是(x:xs)。但总体而言,length是“严格脊椎”。

短暂停留,使我充实了100000个元素列表的脊线

Phew,完成了。现在我知道了长度,这意味着我知道了len = 500000。完全评估THUNK0! !我在哪里?

merge_sort (take 500000 THUNK3)

依此类推。 merge_sort将继续尝试尽可能地变得懒惰。对merge_sort的递归调用将尽可能地延迟。最终,要确定最外层merge_sort的第一个元素,我们将需要知道两次对merge_sort的递归调用的第一个元素。并且要知道其中的第一个元素...我们将需要后续递归调用的第一个元素,等等。因此,大约需要完成[[O(n)工作,因为每个元素都需要进行评估(执行每个数字的随机数生成)。

然后,将其视为锦标赛。每个元素都与另一个元素配对。 “获胜”(最低)元素移至下一轮(成为递归调用最低merge_sort的第一个元素)。还有另一场比赛,其中有1/2战斗员,而those的1/2(总数的1/4)则进入下一轮,依此类推。结果也就是

O(n )

工作,因为(n / 2)个比较是在第一个回合中执行的,而随后的回合则缩小得太快,以至于没有意义。 (总和1/2 + 1/4 + 1/8 ...收敛为1,表示总共进行了n次比较。)总而言之,O(n)工作需要执行才能最终产生第一个元素。需要为后续元素执行其他工作,但最终的工作总量为

O(n log(n))

现在与insert_sort进行对比。它也会在
O(n)
时间内生成第一个元素,但是在最坏的情况下,

total

的工作量为O(n 2:“插入”将列表中的元素逐一放入已排序列表中,并且每次都可能遍历此已排序列表。
© www.soinside.com 2019 - 2024. All rights reserved.