Lists provide a convenient representation for stacks, but what about queues in a purely functional setting?
We can't add a new element to the end of a list or remove the last element without copying the rest of the list in the process, but if we use two lists we can get a fast and simple queue implementation.
The first list, f
, contains the front
elements of the queue in order, and the second list,
r
, contains the rear elements in reverse
order, e.g., the queue with integers 1…6 might be
represented as one of these
([1,2,3], [6,5,4])
([1,2,3,4,5,6], [])
([1], [6,5,4,3,2])
The head of the queue is the first element of
f
, so head
returns this element
and tail
removes it. To add an element to the end
of the queue, we just add it to the beginning of
r
using the function snoc
- fun head (x::f, r) = x fun tail (x::f, r) = (f, r) fun snoc ((f, r), x) = (f, x :: r); > val ('a, 'b) head = fn : 'a list * 'b -> 'a val ('a, 'b) tail = fn : 'a list * 'b -> 'a list * 'b val ('a, 'b) snoc = fn : ('a * 'b list) * 'b -> 'a * 'b list
To ensure that head
always succeeds on a
non-empty queue, we must maintain the invariant that
f
is only ever empty if
r
is also empty. When f
is exhausted, we reverse r
to replenish it.
This needs to happen in tail
and snoc
.
- fun head (x::f, r) = x fun checkf ([], r) = (rev r, []) | checkf q = q fun snoc ((f, r), x) = checkf (f, x::r) fun tail (x::f, r) = checkf (f, r); > val ('a, 'b) head = fn : 'a list * 'b -> 'a val 'a checkf = fn : 'a list * 'a list -> 'a list * 'a list val 'a snoc = fn : ('a list * 'a list) * 'a -> 'a list * 'a list val 'a tail = fn : 'a list * 'a list -> 'a list * 'a list
head
obviously runs in O(1)
time, but in the worst case, tail
and
snoc
could take O(n)
time.
The worst case doesn't give a realistic picture. If we look
at the amortized time bound, we see that the
n
operations a snoc
takes in
its worst case can only happen after n
times
with the O(1)
case. Similarly, the
n
operations a tail
takes in
its worst case ensures that the next n
tail
operations will be O(1)
.
All operations on functional queues run in
O(1)
amortized time.
Recall the insertinorder
function, called
insert
here in this version for reals. It inserts a
new element into the proper place in a sorted list
- fun insert (elt : real, []) = [elt] | insert (elt, lst as x::xs) = if elt < x then elt :: lst else x :: insert (elt, xs); > val insert = fn : real * real list -> real list
We can use insert
to implement an insertion
sort
- val insort = foldl insert []; > val insort = fn : real list -> real list
Insertion sort takes
O(n2)
comparisons in the
average case and in the worst case.
Quick sort works by picking a pivot value and partitioning the list into two lists: values less than the pivot and values greater or equal to the pivot. The sort is applied recursively to the two partitions and the resulting (sorted) lists are concatenated together.
With help from the List.partition
functional,
which partitions a list according to a predicate function, quick
sort is very elegant and clear
- fun qsort [] = [] : real list | qsort (x::xs) = case (List.partition (fn y => y < x) xs) of (left, right) => qsort left @ x :: qsort right; > val qsort = fn : real list -> real list
Quick sort takes
O(n log n)
comparisons on average and
O(n2)
in the worst
case, depending on how well the pivot values are chosen. Quick
sort is excellent for imperative languages because it can sort
an array in-place, but in functional languages the merge sort
tends to do better.
Merge sort is another divide-and-conquer sorting algorithm.
It works by dividing the input into two random groups, sorting
them, then merging them in order. Merging two lists is an
O(n)
operation on the length of the
resulting list
We use the basis functions List.take
and
List.drop
which take and drop the first
n
elements of a list, respectively
- List.take; > val 'a it = fn : 'a list * int -> 'a list - List.drop; > val 'a it = fn : 'a list * int -> 'a list
Note that this implementation is top-down: it completely sorts one half of the list before starting the other half.
- fun merge xs [] = xs : real list | merge [] ys = ys | merge (xlst as x::xs) (ylst as y::ys) = if x < y then x :: merge xs ylst else y :: merge xlst ys fun msort [] = [] | msort [x] = [x] | msort lst = let val k = length lst div 2 in merge (msort (List.take (lst, k))) (msort (List.drop (lst, k))) end; > val merge = fn : real list -> real list -> real list val msort = fn : real list -> real list
Merge sort takes
O(n log n)
comparisons on average and in the worst case.
A bottom-up approach is also possible: starting with individual elements and merging them into larger and larger lists until the sort is complete. The top-down approach is often faster on large lists because of cache effects.