合并的方式 - 排序比插入排序更快困惑我

man*_*ang 13 sorting algorithm haskell lazy-evaluation

刚刚在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
Run Code Online (Sandbox Code Playgroud)

这是我如何比较他们的效率:

insert_sort $ take 100000 $ randomRs (1,100000) $ mkStdGen 1 ::[Int]
merge_sort $ take 100000 $ randomRs (1,100000) $ mkStdGen 1 ::[Int]
Run Code Online (Sandbox Code Playgroud)

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

编辑:大家好.自从我开始学习Haskell之后,我就听说过懒惰的评估,但却得到了它的支持.任何人都会用一个小数据集说明一点,比如说[5,2,6,3,1,4]?foldr由于第一个元素最后出现,如何在完成排序之前输出结果?

Dan*_*her 14

现场背后是懒惰的评价.排序列表的开始在排序完成之前确定,因此可以在完成工作之前输出.由于mergesort更快,因此合并排序列表打印得更快.

根据要求:排序如何[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
Run Code Online (Sandbox Code Playgroud)

并合并排序(缩写: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
Run Code Online (Sandbox Code Playgroud)

不可否认,我已经采取了一些捷径,但Haskell并不是唯一的懒人.


Dan*_*ton 9

好的,这是分解.你要我打印出来:

merge_sort $ take 100000 $ randomRs (1,100000) $ mkStdGen 1 ::[Int]
Run Code Online (Sandbox Code Playgroud)

我碰巧知道这是一个清单.首先,我打印出一个开放式支架

[
Run Code Online (Sandbox Code Playgroud)

然后我将查找列表的第一个元素,打印出来,然后是逗号.这意味着我必须开始评估该表达式,直到我能够弄清楚列表的第一个元素是什么.

merge_sort THUNK0
Run Code Online (Sandbox Code Playgroud)

那么现在我需要模式匹配.THUNK匹配(x:[])或不匹配.但我还不知道.所以我会评估那个thunk.我让那个thunk生成前两个随机数(100000个).现在我知道它与第一个定义不匹配,所以我采用了第二个定义merge_sort.

merge_sort keys = merge THUNK1 THUNK2 -- keys = THUNK0
Run Code Online (Sandbox Code Playgroud)

嗯,这很容易......这只是一个合并的呼吁.我会扩展这个定义.哦废话,这可能有三种不同的模式.我想我应该稍微评估THUNK1并查看它是否与第一个定义的模式匹配,(x:xs)

merge_sort (take THUNK3 THUNK0)
Run Code Online (Sandbox Code Playgroud)

merge_sort再次,我们是什么?这意味着我需要进行(take THUNK3 THUNK0)足够的评估以判断它是否匹配(x:[]).哦,CRAP.它的第一个论点take严格的 ......这意味着我必须全面评估 THUNK3.好的......深呼吸......

len = length THUNK0 `div` 2
Run Code Online (Sandbox Code Playgroud)

现在这是一个令人恼火的案例.要计算lengthTHUNK0(这是一个列表),我必须扩展整个旋转.我不必实际计算内部的值,但我确实需要充实整个列表的结构.当然,这是一次完成一个模式匹配,确定它是否[],或(x:xs).但总的来说,length是"脊柱严格".

短暂的停顿,同时我充实了100000元素列表的脊柱

Phew,完成了.现在我知道长度,这意味着我知道len = 500000.THUNK0 终于全面评估了!唷!我在哪儿?

merge_sort (take 500000 THUNK3)
Run Code Online (Sandbox Code Playgroud)

等等.merge_sort将继续努力尽可能地懒惰.递归调用merge_sort将尽可能地懒惰.最后,为了确定最外层的第一个元素merge_sort,我们需要知道两个递归调用的第一个元素merge_sort.并且要知道那些的第一个元素......我们将需要后续递归调用的第一个元素,等等.因此将完成O(n)工作,因为需要对每个元素进行求值(执行随机数生成)每一个人).

然后,把它想象成锦标赛.每个元素与另一个元素配对."获胜"(最低)元素移动到下一轮(成为递归调用最低merge_sorts 的第一个元素).还有另外一场比赛,战斗员数量的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.