Эффективные кучи в чисто функциональных языках

в качестве упражнения в Haskell я пытаюсь реализовать heapsort. Куча обычно реализуется как массив на императивных языках, но это было бы крайне неэффективно в чисто функциональных языках. Поэтому я посмотрел на бинарные кучи, но все, что я нашел до сих пор, описывает их с императивной точки зрения, и представленные алгоритмы трудно перевести в функциональную настройку. Как эффективно реализовать кучу на чисто функциональном языке, таком как Хаскелл?

Edit: под эффективным я имею в виду, что он все еще должен быть в O(N*log n), но он не должен бить программу C. Кроме того, я хотел бы использовать чисто функциональное программирование. Какой еще смысл делать это в Хаскелле?

9 ответов


существует ряд реализаций кучи Haskell в приложении к Okasaki Чисто Функциональные Структуры Данных. (Исходный код можно загрузить по ссылке. Саму книгу стоит прочитать.) Ни один из них не является бинарными кучами, как таковыми, но "левак" кучи очень похож. Он имеет операции вставки, удаления и слияния O(log n). Существуют также более сложные структуры данных, такие как косое кучи, бином кучи и splay кучи, которые имеют более высокую производительность.


Джон Фэрбэрн опубликовал функциональный heapsort в список рассылки Haskell Cafe еще в 1997 году:

http://www.mail-archive.com/haskell@haskell.org/msg01788.html

я воспроизвожу его ниже, переформатированный в соответствии с этим пространством. Я также немного упростил код merge_heap.

Я удивлен, что treefold не находится в стандартной прелюдии, так как это так полезно. В переводе с версию я писал в вдумайтесь в октябре 1992 -- Джон Fairbairn

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)

вы также можете использовать ST монада, которая позволяет писать императивный код, но безопасно предоставлять чисто функциональный интерфейс.


в качестве упражнения в Haskell я реализовал императивный heapsort с ST монадой.

{-# 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++ с шаблонами, передавая материал внутренним функциям.


и вот куча Фибоначчи в Haskell:

https://github.com/liuxinyu95/AlgoXY/blob/algoxy/datastruct/heap/other-heaps/src/FibonacciHeap.hs

вот pdf-файл для некоторых других куч k-ary, основанных на работе Окасаки.

https://github.com/downloads/liuxinyu95/AlgoXY/kheap-en.pdf


Как и в эффективных алгоритмах Quicksort, написанных в Haskell, вам нужно использовать монады (трансформаторы состояния), чтобы делать вещи на месте.


Я попытался перенести стандартную двоичную кучу в функциональные настройки. Есть статья с описанной идеей:функциональный подход к стандартным двоичным кучам. Все исходные коды в статье находятся в Scala. Но он может быть легко перенесен на любой другой функциональный язык.


массивы в 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 есть компромисс между использованием статических массивов и использованием изменяемых массивов. В статических массивах при изменении элементов необходимо создавать новые копии массивов. С изменяемыми массивами, сборщик мусора имеет трудное время держать различные поколения объектов разделены. Попробуйте реализовать heapsort с помощью STArray и посмотреть, как вам это нравится.


вот страница, содержащая ML-версию HeapSort. Это довольно подробно и должно обеспечить хорошую отправную точку.

http://flint.cs.yale.edu/cs428/coq/doc/Reference-Manual021.html