【问题标题】:Short Circuiting sort短路排序
【发布时间】:2009-12-01 21:27:10
【问题描述】:

我明白:

head (map (2**) [1..999999])

实际上只会评估 2**1,其余的都不会,但我正在阅读的书说:

head (sort somelist)

只需要在列表中找到最小的项目,因为这就是所有使用的。这是如何运作的?据我所知,使用我知道的排序算法(如冒泡排序)是不可能的。

我认为这可行的唯一方法是,如果排序算法遍历整个列表以寻找最小的项目,然后在没有该项目的列表上递归。对我来说,这听起来很慢。

这是排序函数的工作原理,还是有另一种我不知道的排序算法允许短路?

【问题讨论】:

    标签: sorting haskell lazy-evaluation


    【解决方案1】:

    这个:

    只需要在列表中找到最小的项目,因为这就是全部。

    ... 应该说函数只需要完成排序算法所需的最少工作量 即可找到最小元素。

    例如,如果我们使用快速排序作为我们的底层排序算法,那么head . quicksort 等价于称为“quickselect”的最佳(!)选择算法,这是最差的 -案例线性。此外,我们可以通过take k . quicksort实现k-quickselect。

    维基百科在其文章中提到了选择算法(我的重点):

    由于对排序的语言支持更为普遍,尽管在速度上存在劣势,但在许多环境中还是首选排序和索引的简单方法。 确实,对于惰性语言,如果您的排序足够惰性,这种简单化的方法甚至可以为 k 最小/最大排序(最大/最小值作为特例)获得尽可能好的复杂性。 p>

    快速排序在这种情况下运行良好,而 Haskell 中的默认排序(合并排序)的组合效果不佳,因为它所做的工作比返回排序列表的每个元素所严格需要的要多。正如this post on the Haskell mailing list 笔记:

    惰性快速排序能够产生

    中的前k个最小元素

    O(n + k log k) 总时间 [1]

    虽然惰性归并排序需要

    O(n + k log n) 总时间 [2]

    如需了解更多信息,请阅读this blog post

    【讨论】:

      【解决方案2】:

      如果您创建一个跟踪其参数的比较函数,例如在 GHCi 的命令行中:

      > :module + Data.List Debug.Trace
      > let myCompare x y = trace ("\tCmp " ++ show x ++ " " ++ show y) $ compare x y
      

      然后你可以自己查看行为:

      > sortBy myCompare "foobar"
      
      "     Cmp 'f' 'o'
            Cmp 'o' 'b'
            Cmp 'f' 'b'
            Cmp 'a' 'r'
            Cmp 'b' 'a'
      a     Cmp 'b' 'r'
      b     Cmp 'f' 'o'
            Cmp 'f' 'r'
      f     Cmp 'o' 'o'
            Cmp 'o' 'r'
      o     Cmp 'o' 'r'
      or"
      

      Haskell 正在懒惰地评估字符串,一次一个字符。在找到每个字符时打印左列,右列记录所需的比较,如“trace”所打印。

      请注意,如果您编译它,尤其是在优化的情况下,您可能会得到不同的结果。优化器运行一个严格分析器,它可能会注意到整个字符串都被打印出来了,所以急切地评估它会更有效率。

      那就试试

      > head $ sortBy myCompare "foobar"
      
            Cmp 'f' 'o'
            Cmp 'o' 'b'
            Cmp 'f' 'b'
            Cmp 'a' 'r'
            Cmp 'b' 'a'
      'a'
      

      如果您想了解其工作原理,请查看排序函数的源代码并在纸上手动评估“排序“foobar”。

      qsort [] = []
      qsort (x:xs) = qsort less ++ [x] ++ qsort greater
         where (less, greater) = partition (< x) xs
      

      所以

         qsort ('f':"oobar")
       = qsort ('b':"a") ++ "f" ++ qsort ('o':"or")
       = ("a" ++ "b") ++ "f" ++ qsort ('o':"or")
      

      现在我们已经做了足够的工作来发现“a”是结果中的第一项,而无需评估对“qsort”的其他调用。我省略了实际比较,因为它隐藏在对“分区”的调用中。实际上,“partition”也是惰性的,所以事实上,就我所展示的而言,另一个“qsort”的参数还没有被评估。

      【讨论】:

        【解决方案3】:

        您刚才描述的算法有一个特定的名称:“选择排序”。这是 O(n2) 所以它不是你能做的最快的事情。但是,如果您想要排序数组中的前“k”个元素,那么复杂度将是 O(kn),如果“k”足够小(如您的示例),这很好。

        请注意,您使用的是函数式语言中的纯函数。通过查看函数的组合方式,编译器很可能能够在这两种情况下为sort 生成优化代码。当你编写headsort 时,它可以很容易地推断出你想要最小的元素。

        【讨论】:

        • 这最后一部分不正确;编译器无法推断意图!
        • Porges:虽然编译器可以硬连线以在特定情况下分析意图,但您确实不需要推断意图。您需要机械地使用已证明的定理来证明代码的优化版本在数学上等于原始版本。函数式语言通过禁止副作用使这个定理更容易证明。
        • 可能,但我不知道有任何 Haskell 编译器会在优化过程中包含自动定理证明器。这种函数组合起作用的原因完全是由于 Haskell 的默认惰性特性。
        猜你喜欢
        • 1970-01-01
        • 2013-06-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-23
        • 2019-12-06
        • 1970-01-01
        相关资源
        最近更新 更多