解释和澄清Haskell计数排序

时间:2016-10-01 17:27:17

标签: algorithm haskell

我正在通过 Cormen et。 Al。,Algorithms算法导论,第3版。,但我也对Haskell感兴趣。第8.2节(第194页)包括计数排序。我对如何在haskell中实现它和许多算法感兴趣,因为它们经常使用数组访问和破坏性更新。我看了RosettaCode上的实现(下面复制了),我觉得很难理解。

import Data.Array

countingSort :: (Ix n) => [n] -> n -> n -> [n]
countingSort l lo hi = concatMap (uncurry $ flip replicate) count
  where count = assocs . accumArray (+) 0 (lo, hi) . map (\i -> (i, 1)) $ l

我喜欢haskell的一个方面是算法如何非常清晰(例如Haskell quicksort示例),至少作为未经优化的规范。这似乎很不清楚,我想知道它是否必然如此或过度。

有人可以吗

  1. 澄清这里发生了什么,
  2. 或许提供更具指导性和清晰度的实施,
  3. 解决这是否实际上正在实施计数排序,或者非严格性(懒惰)和不变性是否意味着这实际上是一些伪装成计数排序的其他类型?

2 个答案:

答案 0 :(得分:7)

它确实在进行计数。这是一个稍微改写的版本,我觉得更容易理解:

import Data.Array

countingSort :: (Ix n) => [n] -> n -> n -> [n]
countingSort l lo hi = concat [replicate times n | (n, times) <- counts]
  where counts = assocs (accumArray (+) 0 (lo, hi) [(i, 1) | i <- l])

让我们一步一步地分解它。我们将使用列表[5, 3, 1, 2, 3, 4, 5]

*Main> [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]
[(5,1),(3,1),(1,1),(2,1),(3,1),(4,1),(5,1)]

我们只是采用列表中的每个元素并将其转换为1的元组。这是我们计数的基础。现在我们需要一种方法来总结每个元素的计数。这是accumArray发挥作用的地方。

*Main> accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]
array (1,5) [(1,1),(2,1),(3,2),(4,1),(5,2)]

accumArray的第一个参数是在累积期间应用的操作(对我们来说只是简单的添加)。第二个参数是起始值,第三个参数是边界。因此,我们最终得到一个数组映射数字到它们在输入中的计数。

接下来,我们使用assocs从地图中获取键/值元组:

*Main> assocs $ accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]
[(1,1),(2,1),(3,2),(4,1),(5,2)]

然后replicate根据其数量重复每个数字:

*Main> [replicate times n | (n, times) <- assocs $ accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]]
[[1],[2],[3,3],[4],[5,5]]

最后,我们使用concat将这个列表列表转换为单个列表:

*Main> concat [replicate times n | (n, times) <- assocs $ accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]]
[1,2,3,3,4,5,5]

在我上面写的实际功能中,我使用where打破了这个单行。

我发现列表理解比mapconcatMap更容易处理,所以这些是我在函数版本中所做的主要更改。

(uncurry $ flip replicate)是一个不错的技巧...... flip replicate为您提供了replicate的版本,它以相反的顺序获取其参数。 uncurry接受那个curried函数并将其转换为一个以元组作为参数的函数。这些产生与我的列表理解相同的结果,它解构了元组,然后以相反的顺序传递其参数。我对Haskell不太熟悉,不知道这是一个常见的习语,但对我来说,列表理解更容易理解。

答案 1 :(得分:3)

countaccumArray用作直方图构建器。也就是说,对于从lohi的每个数字,它返回参数列表中出现的次数。

count ::  (Ix i, Num e) => [i] -> i -> i -> [(i, e)]
count l lo hi = assocs . accumArray (+) 0 (lo, hi) . map (\i -> (i, 1)) $ l

count [6,2,1,6] 0 10 ==  
   [(0,0),(1,1),(2,1),(3,0),(4,0),(5,0),(6,2),(7,0),(8,0),(9,0),(10,0)]

count的结果用于从此规范生成原始元素。这是通过replicate元组fst的每个snd元素来完成的。这会生成连接在一起的列表列表。

 f l lo hi = map (uncurry $ flip replicate ) $ count l lo hi
 f [6,2,1,6] 0 10 == [[],[1],[2],[],[],[],[6,6],[],[],[],[]]

完整解决方案相当于

countingSort l lo hi = concat $ f l lo hi