haskell 纯函数式语言中的高效堆

xzv2uavs  于 2023-06-06  发布在  其他
关注(0)|答案(9)|浏览(171)

作为Haskell的一个练习,我尝试实现堆排序。堆通常在命令式语言中实现为数组,但这在纯函数式语言中效率非常低。所以我已经研究了二进制堆,但到目前为止我发现的所有内容都是从命令式的Angular 描述它们的,并且所提出的算法很难转换为函数式设置。如何在纯函数式语言(如Haskell)中有效地实现堆?

  • 编辑:* 高效的意思是它应该仍然是O(n*log n),但它不必击败C程序。另外,我想使用纯函数式编程。在Haskell中这样做还有什么意义呢?
s6fujrry

s6fujrry1#

在Okasaki的Purely Functional Data Structures (pdf)的附录中有许多Haskell堆实现。(源代码可以在这里下载。这本书本身很值得阅读。)它们本身都不是二进制堆,但是"leftist" heap非常相似。它具有O(log n)的插入、删除和合并操作。还有更复杂的数据结构,如skew heapsbinomial heapssplay heaps,它们具有更好的性能。

noj0wjuj

noj0wjuj2#

Jon Fairbairn早在1997年就在Haskell Cafe邮件列表中发布了一个函数式堆排序:
http://www.mail-archive.com/haskell@haskell.org/msg01788.html
我在下面复制它,重新格式化以适应这个空间。我还稍微简化了merge_heap的代码。
我很惊讶treefold没有出现在标准的prelude中,因为它是如此的有用。翻译自我1992年10月在《思考》中写的版本--乔恩·费尔贝恩

module Treefold where

-- treefold (*) z [a,b,c,d,e,f] = (((a*b)*(c*d))*(e*f))
treefold f zero [] = zero
treefold f zero [x] = x
treefold f zero (a:b:l) = treefold f zero (f a b : pairfold l)
    where 
        pairfold (x:y:rest) = f x y : pairfold rest
        pairfold l = l -- here l will have fewer than 2 elements

module Heapsort where
import Treefold

data Heap a = Nil | Node a [Heap a]
heapify x = Node x []

heapsort :: Ord a => [a] -> [a]    
heapsort = flatten_heap . merge_heaps . map heapify    
    where 
        merge_heaps :: Ord a => [Heap a] -> Heap a
        merge_heaps = treefold merge_heap Nil

        flatten_heap Nil = []
        flatten_heap (Node x heaps) = x:flatten_heap (merge_heaps heaps)

        merge_heap heap Nil = heap
        merge_heap node_a@(Node a heaps_a) node_b@(Node b heaps_b)
            | a < b = Node a (node_b: heaps_a)
            | otherwise = Node b (node_a: heaps_b)
laik7k3q

laik7k3q3#

您还可以使用ST monad,它允许您编写命令式代码,但安全地公开纯函数式接口。

qmelpv7a

qmelpv7a4#

作为Haskell的一个练习,我用ST Monad实现了一个命令式堆排序。

{-# LANGUAGE ScopedTypeVariables #-}

import Control.Monad (forM, forM_)
import Control.Monad.ST (ST, runST)
import Data.Array.MArray (newListArray, readArray, writeArray)
import Data.Array.ST (STArray)
import Data.STRef (newSTRef, readSTRef, writeSTRef)

heapSort :: forall a. Ord a => [a] -> [a]
heapSort list = runST $ do
  let n = length list
  heap <- newListArray (1, n) list :: ST s (STArray s Int a)
  heapSizeRef <- newSTRef n
  let
    heapifyDown pos = do
      val <- readArray heap pos
      heapSize <- readSTRef heapSizeRef
      let children = filter (<= heapSize) [pos*2, pos*2+1]      
      childrenVals <- forM children $ \i -> do
        childVal <- readArray heap i
        return (childVal, i)
      let (minChildVal, minChildIdx) = minimum childrenVals
      if null children || val < minChildVal
        then return ()
        else do
          writeArray heap pos minChildVal
          writeArray heap minChildIdx val
          heapifyDown minChildIdx
    lastParent = n `div` 2
  forM_ [lastParent,lastParent-1..1] heapifyDown
  forM [n,n-1..1] $ \i -> do
    top <- readArray heap 1
    val <- readArray heap i
    writeArray heap 1 val
    writeSTRef heapSizeRef (i-1)
    heapifyDown 1
    return top

顺便说一句,我认为如果它不是纯粹的功能性的,那么在Haskell中这样做就没有意义。我认为我的玩具实现比C++中使用模板实现的要好得多,将东西传递给内部函数。

q3aa0525

q3aa05256#

就像在Haskell中编写的高效快速排序算法一样,您需要使用monad(状态转换器)来就地执行操作。

7fhtutme

7fhtutme7#

Haskell中的数组并不像你想象的那么低效,但是Haskell中的典型做法可能是使用普通的数据类型来实现它,就像这样:

data Heap a = Empty | Heap a (Heap a) (Heap a)
fromList :: Ord a => [a] -> Heap a
toSortedList :: Ord a => Heap a -> [a]
heapSort = toSortedList . fromList

如果要解决这个问题,我可能会先将列表元素填充到一个数组中,这样更容易为堆创建索引。

import Data.Array
fromList xs = heapify 0 where
  size = length xs
  elems = listArray (0, size - 1) xs :: Array Int a
  heapify n = ...

如果你使用的是二进制最大堆,你可能希望在删除元素时跟踪堆的大小,这样你就可以在O(log N)时间内找到右下角的元素。您还可以查看通常不使用数组实现的其他类型的堆,如二项式堆和斐波那契堆。
关于阵列性能的最后一点注意事项:在Haskell中,在使用静态数组和使用可变数组之间有一个折衷。对于静态数组,在更改元素时必须创建数组的新副本。对于可变数组,垃圾收集器很难将不同代的对象分开。尝试使用STArray实现堆排序,看看你喜欢它。

b4qexyjb

b4qexyjb8#

我尝试将标准的二进制堆移植到功能设置中。有一篇文章描述了这样的想法:本文中的所有源代码清单都是用Scala编写的。但它可以很容易地移植到任何其他函数式语言中。

q5iwbnjs

q5iwbnjs9#

这是一个包含堆排序的ML版本的页面。它非常详细,应该提供一个很好的起点。
http://flint.cs.yale.edu/cs428/coq/doc/Reference-Manual021.html

相关问题