patternMinor
A functional binary heap implementation
Viewed 0 times
functionalimplementationbinaryheap
Problem
I've implemented a binary heap in F#. It's pure and uses zippers for tree modification.
To test it out I have implemented heap sort using it but it takes 10 seconds to sort a list of 100 000. Regular List.sort is instant and since heap sort should have the same complexity I'm wondering what I can do to improve my implementation.
Profiling revealed most of the time (above 50%) is spent in the bubble down, which is to be expected since sort is basically just don't a bunch of removes but nothing in the method is really exceptionally slow (that I can see).
```
#nowarn "25"
namespace FSharpExt
module Heap =
type HeapNode =
| Full of 'a 'b HeapNode * HeapNode
| Half of 'a 'b HeapNode
| Leaf of 'a * 'b
| Empty
let (|KeyValue|) zipper =
match zipper with
| Full(k, v, _, _) | Half(k, v, _) | Leaf(k, v) -> (k, v)
| Empty -> failwith "List is empty"
let cut node =
match node with
| Leaf(k, v) -> Empty
| Half(k, v, _) -> Leaf(k, v)
| Full(k, v, left, _) -> Half(k, v, left)
type Direction = Left | Right
type Pointer = Direction list
let rec next pointer =
match pointer with
| [] -> [Left]
| x :: xs when x = Left -> Right :: xs
| x :: xs -> Left :: next xs
let rec previous pointer =
match pointer with
| [Left] -> []
| x :: xs when x = Right -> Left :: xs
| x :: xs -> Right :: previous xs
type Zipper = Zipper of HeapNode (HeapNode Direction) list
let moveLeftZipper (Zipper((Full(_, _, left, _) | Half(_, _, left)) as node, path)) = Zipper(left, (node, Left) :: path)
let moveRightZipper (Zipper(Full(_, _, _, right) as node, path)) = Zipper(right, (node, Right) :: path)
let moveDirectionZipper direction zipper =
match direction with
| Left -> moveLeftZipper zipper
To test it out I have implemented heap sort using it but it takes 10 seconds to sort a list of 100 000. Regular List.sort is instant and since heap sort should have the same complexity I'm wondering what I can do to improve my implementation.
Profiling revealed most of the time (above 50%) is spent in the bubble down, which is to be expected since sort is basically just don't a bunch of removes but nothing in the method is really exceptionally slow (that I can see).
```
#nowarn "25"
namespace FSharpExt
module Heap =
type HeapNode =
| Full of 'a 'b HeapNode * HeapNode
| Half of 'a 'b HeapNode
| Leaf of 'a * 'b
| Empty
let (|KeyValue|) zipper =
match zipper with
| Full(k, v, _, _) | Half(k, v, _) | Leaf(k, v) -> (k, v)
| Empty -> failwith "List is empty"
let cut node =
match node with
| Leaf(k, v) -> Empty
| Half(k, v, _) -> Leaf(k, v)
| Full(k, v, left, _) -> Half(k, v, left)
type Direction = Left | Right
type Pointer = Direction list
let rec next pointer =
match pointer with
| [] -> [Left]
| x :: xs when x = Left -> Right :: xs
| x :: xs -> Left :: next xs
let rec previous pointer =
match pointer with
| [Left] -> []
| x :: xs when x = Right -> Left :: xs
| x :: xs -> Right :: previous xs
type Zipper = Zipper of HeapNode (HeapNode Direction) list
let moveLeftZipper (Zipper((Full(_, _, left, _) | Half(_, _, left)) as node, path)) = Zipper(left, (node, Left) :: path)
let moveRightZipper (Zipper(Full(_, _, _, right) as node, path)) = Zipper(right, (node, Right) :: path)
let moveDirectionZipper direction zipper =
match direction with
| Left -> moveLeftZipper zipper
Solution
First of all I'm not surprised that your algorithm is significantly slower than
All these put together mean that functional heapsort is never going to be anywhere near
I don't see why you're using zippers here at all. After each operation, the zipper is positioned at the root of the tree, so you might as well use just the binary tree (plus a pointer to the next leaf position). All it does is to make your code more complicated (and probably less efficient).
I don't understand why do you have both
This is very confusing, since
I don't see any reason to use
Or even (if you're okay with not naming the parameter):
You don't need to name variables you're not going to use. Here, you could just write
You could start folding from
List.sort:List.sortis implemented usingArray.Sort, which uses introsort. Normal (array-based) heapsort is one of the slower sorting algorithms.
- When compared with normal heapsort, you're using a separate object for each item in the collection.
- When compared with mutable tree-based heapsort, you're also creating garbage when "mutating" the tree.
All these put together mean that functional heapsort is never going to be anywhere near
List.sort in terms of performance.I don't see why you're using zippers here at all. After each operation, the zipper is positioned at the root of the tree, so you might as well use just the binary tree (plus a pointer to the next leaf position). All it does is to make your code more complicated (and probably less efficient).
type HeapNode ='a and 'b are pretty bad names. All names you use should be meaningful.| Full of 'a * 'b * HeapNode * HeapNode
| Half of 'a * 'b * HeapNode
| Leaf of 'a * 'b
| EmptyI don't understand why do you have both
Empty and a special case for inner node with one child empty.let (|KeyValue|) zipper =This is very confusing, since
zipper is not a Zipper, it's a HeapNode.let rec next pointer =
match pointer with
| [] -> [Left]
| x :: xs when x = Left -> Right :: xs
| x :: xs -> Left :: next xsI don't see any reason to use
when here, you could write this as:let rec next pointer =
match pointer with
| [] -> [Left]
| Left :: xs -> Right :: xs
| Right :: xs -> Left :: next xsOr even (if you're okay with not naming the parameter):
let rec next =
function
| [] -> [Left]
| Left :: xs -> Right :: xs
| Right :: xs -> Left :: next xslet rec toRootZipper (Zipper(current, path) as zipper) =
match path with
| [] -> zipper
| x :: xs -> zipper |> moveUpZipper |> toRootZipperYou don't need to name variables you're not going to use. Here, you could just write
_ instead of x :: xs.let ofList list =
match list with
| [] -> empty
| first :: tail -> List.fold (fun h x -> insert x h) (singleton first) tailYou could start folding from
empty instead of singleton first. That way, you wouldn't even need a special case for the empty list:let ofList list = List.fold (fun h x -> insert x h) empty listCode Snippets
type HeapNode<'a, 'b when 'a : comparison> =| Full of 'a * 'b * HeapNode<'a, 'b> * HeapNode<'a, 'b>
| Half of 'a * 'b * HeapNode<'a, 'b>
| Leaf of 'a * 'b
| Emptylet (|KeyValue|) zipper =let rec next pointer =
match pointer with
| [] -> [Left]
| x :: xs when x = Left -> Right :: xs
| x :: xs -> Left :: next xslet rec next pointer =
match pointer with
| [] -> [Left]
| Left :: xs -> Right :: xs
| Right :: xs -> Left :: next xsContext
StackExchange Code Review Q#56255, answer score: 2
Revisions (0)
No revisions yet.