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

时间:2012-01-19 01:02:59

标签: algorithm sorting 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

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

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

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

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

2 个答案:

答案 0 :(得分: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

并合并排序(缩写: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并不是唯一的懒人。

答案 1 :(得分:9)

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

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

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

[

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

merge_sort THUNK0

现在我需要模式匹配。 THUNK匹配(x:[])或不匹配。但我还不知道。所以我会评估那个thunk。我让那个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在其第一个参数中是 strict ...这意味着我必须完全评估 THUNK3。好的......深呼吸......

len = length THUNK0 `div` 2

现在这是一个令人恼火的案例。要在THUNK0上计算length(这是一个列表),我必须扩展整个旋转。我不必实际计算内部的值,但我确实需要充实整个列表的结构。当然,这是一次完成一个模式匹配,确定它是[]还是(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 s的第一个元素)。还有另外一场比赛的战斗员数量增加了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_sortinsert_sort完成时不需要执行所有工作以开始生成结果。