Haskell 纯函数式语言中的有效堆

Haskell 纯函数式语言中的有效堆,haskell,functional-programming,binary-heap,heapsort,purely-functional,Haskell,Functional Programming,Binary Heap,Heapsort,Purely Functional,作为Haskell中的练习,我尝试实现heapsort。堆通常在命令式语言中实现为数组,但在纯函数式语言中效率极低。所以我研究了二进制堆,但到目前为止,我发现的所有东西都是从命令的角度描述它们的,并且给出的算法很难转化为函数设置。如何用纯函数式语言(如Haskell)高效地实现堆 编辑:我所说的高效是指它应该仍然是O(n*logn),但它不必超过C程序。另外,我想使用纯函数式编程。在Haskell中执行此操作还有什么意义?这是一个包含HeapSort的ML版本的页面。它非常详细,应该提供一个很好

作为Haskell中的练习,我尝试实现heapsort。堆通常在命令式语言中实现为数组,但在纯函数式语言中效率极低。所以我研究了二进制堆,但到目前为止,我发现的所有东西都是从命令的角度描述它们的,并且给出的算法很难转化为函数设置。如何用纯函数式语言(如Haskell)高效地实现堆


编辑:我所说的高效是指它应该仍然是O(n*logn),但它不必超过C程序。另外,我想使用纯函数式编程。在Haskell中执行此操作还有什么意义?

这是一个包含HeapSort的ML版本的页面。它非常详细,应该提供一个很好的起点


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

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

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(logn)时间内找到右下角的元素。您还可以查看通常不使用数组实现的其他类型的堆,如二项式堆和斐波那契堆


关于数组性能的最后一点注意事项:在Haskell中,使用静态数组和使用可变数组之间存在权衡。对于静态数组,在更改元素时必须创建数组的新副本。对于可变数组,垃圾收集器很难将不同代的对象分开。尝试使用STArray实现heapsort,看看您喜欢它。

冈崎的附录中有许多Haskell堆实现。(源代码可以在链接上下载。这本书本身很值得一读。)它们本身都不是二进制堆,但它们的结构非常相似。它有O(logn)插入、删除和合并操作。还有更复杂的数据结构,如,和,它们具有更好的性能。

作为Haskell中的练习,我用ST Monad实现了命令式heapsort

{-# 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
{-#语言范围的TypeVariables}
导入控制.Monad(表单,表单)
import Control.Monad.ST(ST,runST)
导入Data.Array.MArray(newListArray、readArray、writeArray)
导入Data.Array.ST(STArray)
导入Data.STRef(newSTRef、readSTRef、writeSTRef)
heapSort::对于所有a。作战需求文件a=>[a]->[a]
heapSort list=runST$do
设n=长度列表

heapJon Fairbairn早在1997年就在Haskell Cafe的邮件列表中发布了一个功能性heapsort:

我在下面复制了它,重新格式化以适应这个空间。我还稍微简化了merge_heap的代码

我很惊讶treefold没有出现在标准前奏曲中,因为它非常有用。翻译自1992年10月我在《思考》杂志上写的版本——乔恩·费尔拜恩

模块树的前面,其中
--三倍(*)z[a,b,c,d,e,f]=(((a*b)*(c*d))*(e*f))
三倍f 0[]=0
三倍f零[x]=x
三倍f零(a:b:l)=三倍f零(f a b:pairfold l)
哪里
pairfold(x:y:rest)=f x y:pairfold rest
pairfold l=l——这里l的元素少于2个
模块Heapsort在哪里
导入三倍
数据堆a=Nil |节点a[堆a]
heapify x=节点x[]
heapsort::Ord a=>[a]->[a]
heapsort=展平堆。合并堆。海皮菲地图
哪里
合并堆::Ord a=>[堆a]->堆a
merge_heaps=treefold merge_heap Nil
展平堆Nil=[]
展平堆(节点x堆)=x:展平堆(合并堆)
merge_heap Nil=堆
合并堆节点a@(节点a堆a)节点b@(节点b堆b)
|a
这里是哈斯克尔的斐波那契堆:

以下是基于冈崎工作的其他一些k-ary堆的pdf文件


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

是的,ST monad为您提供了必要的“可变状态”。它提供了单独的可变变量(有点像F#中的
ref
)以及可变数组,它们对于就地排序特别有意义。-1:没有人能够在Haskell中编写有效的快速排序。这一个非常有效@JonHarrop@JonHarrop对它可以在ST(纯严格状态线程,不牺牲引用完整性)或I/o(可以使用并发性,因此像其他任何东西一样受竞争条件的约束)中运行。如果我添加了不安全的读/写操作,我本可以让它更快。@TheInternet:Ok。并行性应该会让它快一点,但Haskell的安全并行方法会让它慢得多。随着线性类型被添加到GHC中,情况可能不再如此。它将允许类似于Rust的分裂。当然,这是Clean在20年前所做的。请注意,每次数组写入的O(n)开销是GHC 6.12中修复的一个错误。我不知道这是如何成为常规堆,尽管它使用堆。不过还是不错,链接好像断了
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)