Эффективные кучи в чисто функциональных языках
-
06-09-2019 - |
Вопрос
В качестве упражнения на Haskell я пытаюсь реализовать пирамидальную сортировку.В императивных языках куча обычно реализуется как массив, но в чисто функциональных языках это было бы крайне неэффективно.Итак, я рассмотрел двоичные кучи, но все, что я нашел до сих пор, описывает их с императивной точки зрения, а представленные алгоритмы трудно перевести в функциональную среду.Как эффективно реализовать кучу на чисто функциональном языке, таком как Haskell?
Редактировать: Под эффективностью я подразумеваю, что оно все равно должно быть в O(n*log n), но оно не обязательно должно превосходить программу на C.Кроме того, я хотел бы использовать чисто функциональное программирование.Какой еще смысл делать это на Haskell?
Решение
В приложении к книге Окасаки есть несколько реализаций кучи на Haskell. Чисто функциональные структуры данных.(Исходный код можно скачать по ссылке.Эту книгу стоит прочитать.) Ни одна из них не является двоичной кучой сама по себе, но "левая" куча очень похожа.Он имеет операции вставки, удаления и слияния O(log n).Существуют также более сложные структуры данных, такие как перекос кучи, биномиальные кучи, и разбрасывать кучи которые имеют лучшую производительность.
Другие советы
Джон Фэйрберн опубликовал функциональную пирамидальную сортировку в списке рассылки Haskell Cafe еще в 1997 году:
http://www.mail-archive.com/haskell@haskell.org/msg01788.html
Я воспроизвожу его ниже, отформатировав так, чтобы оно соответствовало этому месту.Я также немного упростил код merge_heap.
Я удивлен, что древовидная складка не входит в стандартную прелюдию, ведь она настолько полезна.Перевод версии, которую я написал в Ponder в октябре 1992 года — Джон Фэйрберн
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 я реализовал императивную пирамидальную сортировку с помощью 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++ с помощью шаблонов, передавая данные внутренним функциям.
А вот куча Фибоначчи в Haskell:
https://github.com/liuxinyu95/AlgoXY/blob/algoxy/datastruct/heap/other-heaps/src/FibonacciHeap.hs
Вот pdf-файл некоторых других k-арных куч, основанных на работе Окасаки.
Как и в эффективных алгоритмах быстрой сортировки, написанных на Haskell, вам нужно использовать монады (преобразователи состояний), чтобы делать что-то на месте.
Массивы в 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 и посмотрите, понравится ли она вам.
Я попробовал портировать стандартную бинарную кучу в функциональные настройки.Есть статья с описанной идеей: Функциональный подход к стандартным двоичным кучам.Все листинги исходного кода в статье написаны на Scala.Но его можно очень легко перенести на любой другой функциональный язык.
Вот страница, содержащая версию HeapSort для машинного обучения.Он довольно подробный и должен послужить хорошей отправной точкой.
http://flint.cs.yale.edu/cs428/coq/doc/Reference-Manual021.html