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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了合并排序比插入排序更快的方式让我感到困惑相关的知识,希望对你有一定的参考价值。

刚刚用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完成排序之前输出结果?

答案

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

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

另一答案

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

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:“插入”将列表中的元素逐一放入已排序列表中,并且每次都可能遍历此已排序列表。

以上是关于合并排序比插入排序更快的方式让我感到困惑的主要内容,如果未能解决你的问题,请参考以下文章

优化的归并排序比快速排序更快

C 编程:如何实现插入排序?

Numpy 排序比 Matlab 排序慢得多

为啥处理排序数组比处理未排序数组更快?

对 Datatable 的排序方向箭头感到困惑

Quicksort与就地合并排序