The Queue
abstraction employs a First-In-First-Out (FIFO) policy for removing (“dequeing”) elements.
enqueue : a -> Queue a -> Queue a
dequeue : Queue a -> Maybe (Queue a)
peek : Queue a -> Maybe a
Without having O(1) time access to the last element in a List
, special care must be taken to provide efficient Queue
operations in a purely functional language.
SlowQueue.elm
type Queue a = Q (List a)
empty : Queue a
empty = Q []
isEmpty : Queue a -> Bool
isEmpty q = q == empty
enqueue : a -> Queue a -> Queue a
enqueue x (Q xs) = Q (xs ++ [x])
dequeue : Queue a -> Maybe (Queue a)
dequeue (Q xs) = case xs of
_::xs_ -> Just (Q xs_)
[] -> Nothing
peek : Queue a -> Maybe a
peek (Q xs) = case xs of
x::_ -> Just x
[] -> Nothing
Running times:
enqueue
: Θ(n)dequeue
: O(1)peek
: O(1)AnotherSlowQueue.elm
type Queue a = Front (List a) | Back (List a)
empty : Queue a
empty = Front []
isEmpty : Queue a -> Bool
isEmpty q = case q of
Front [] -> True
Back [] -> True
_ -> False
enqueue : a -> Queue a -> Queue a
enqueue x q = case q of
Front xs -> Back (x :: List.reverse xs)
Back xs -> Back (x :: xs)
dequeue : Queue a -> Maybe (Queue a)
dequeue q = case q of
Back [] -> Nothing
Back (x::xs) -> Just (Back xs)
Front xs -> dequeue (Back (List.reverse xs))
peek : Queue a -> Maybe a
peek q = case q of
Back [] -> Nothing
Back (x::_) -> Just x
Front xs -> peek (Back (List.reverse xs))
Running times:
enqueue
: O(n)dequeue
: O(n)peek
: O(n)MediumQueue.elm
type Queue a = Q { front: List a, back: List a }
mkQ f b = Q {front = f, back = b}
empty : Queue a
empty = mkQ [] []
isEmpty : Queue a -> Bool
isEmpty q = q == empty
enqueue : a -> Queue a -> Queue a
enqueue x (Q {front, back}) = mkQ front (x::back)
dequeue : Queue a -> Maybe (Queue a)
dequeue (Q {front, back}) = case (front, back) of
(_::f_, _) -> Just (mkQ f_ back)
([], []) -> Nothing
([], _) -> dequeue (mkQ (List.reverse back) [])
peek : Queue a -> Maybe a
peek (Q {front, back}) = case (front, back) of
(x::_, _) -> Just x
([], []) -> Nothing
([], _) -> peek (mkQ (List.reverse back) [])
Running times:
enqueue
: O(1)dequeue
: O(n)peek
: O(n)FastQueue.elm
Same representation Q {front, back}
as previous version, but with the invariant front = []
implies back = []
.
enqueue : a -> Queue a -> Queue a
enqueue x (Q {front, back}) =
case front of
[] -> mkQ [x] []
_ -> mkQ front (x::back)
dequeue : Queue a -> Maybe (Queue a)
dequeue (Q {front, back}) =
case front of
[] -> Nothing
_::[] -> Just (mkQ (List.reverse back) [])
_::f_ -> Just (mkQ f_ back)
peek : Queue a -> Maybe a
peek (Q {front, back}) = List.head front
We can factor out a common pattern from enqueue
and dequeue
. Notice that arguments f
and b
to checkFront
do not necessarily satisfy the invariant, but the output Queue
does.
checkFront : List a -> List a -> Queue a
checkFront f b =
case f of
[] -> mkQ (List.reverse b) []
_ -> mkQ f b
enqueue_ : a -> Queue a -> Queue a
enqueue_ x (Q {front, back}) = checkFront front (x::back)
dequeue_ : Queue a -> Maybe (Queue a)
dequeue_ (Q {front, back}) =
case front of
[] -> Nothing
_::f_ -> Just (checkFront f_ back)
Running times:
enqueue
: O(1)dequeue
: O(n)peek
: O(1)The worst-case bound for dequeue
does not tell the whole story, however, since it often runs in O(1) time and only occasionally runs in O(n) time. Using amortized asymptotic analysis, we will be able to argue that dequeue
runs in O(1) on average.