February 2021
Reference:
“Purely functional data structures” Chris Okasaki, Cambridge University Press, 1998.
(Extended version of the author’s PhD thesis.)
type Queue -- type of queues
type Elem -- type of elements
enqueue :: Elem -> Queue -> Queue
-- add an element to the back
dequeue :: Queue -> (Elem, Queue)
-- get the element from the front
-- and the smaller queue
empty :: Queue -- make an empty queue
isEmpty :: Queue -> Bool -- test emptyness
type Queue = [Elem]
enqueue :: Elem -> Queue -> Queue
enqueue x list = list ++ [x]
dequeue :: Queue -> (Elem, Queue)
dequeue (first:rest) = (first, rest)
dequeue [] = error "dequeue: empty queue"
empty :: Queue
empty = []
isEmpty :: Queue -> Bool
isEmpty = null
dequeue
, empty
and isEmpty
take \(O(1)\) timeenqueue
takes \(O(n)\):Represent the queue by a pair of lists:
Idea:
\[ \text{dequeue} \leftarrow \underbrace{\begin{array}{|c|c|c|c|} \hline 3 & 1 & 7 & 2 \\ \hline \end{array}}_{\text{front}}\! \underbrace{\begin{array}{|c|c|c|} \hline 4 & 5 & 9 \\ \hline \end{array}}_{\text{back}} \leftarrow \text{enqueue} \]
Can be represented as:
(Note that the back list is in reverse order.)
\[ \leftarrow \begin{array}{|c|c|c|c|c|c|c|} \hline 3 & 1 & 7 & 2 & 4 & 5 & 9 \\ \hline \end{array} \leftarrow \]
The representation is not unique, e.g.:
([3,1,7,2], [9,5,4])
([3,1,7], [9,5,4,2])
Also, the front or back may be empty:
([], [9,5,4,2,7,1,3])
([3,1,7,2,4,5,9], [])
Recall: dequeue from the start of front
and enqueue into the start of back
.
Special case: to dequeue
when the front is empty we must first reverse the back and move it to the front.
([], [9,5,4,2,7,1,3])
\(\downarrow\) reverse
([3,1,7,2,4,5,9], [])
\(\downarrow\) extract head
(3, ([1,7,2,4,5,9], []))
enqueue :: Elem -> Queue -> Queue
enqueue elem (front, back) = (front, elem:back)
dequeue :: Queue -> (Elem, Queue)
dequeue ([],back) = extract (reverse back) []
dequeue (front, back) = extract front back
-- helper function
extract (x:front) back = (x, (front,back))
extract [] back = error "empty queue"
enqueue
is always \(O(1)\)dequeue
is variableif the front is not empty, we just extract the head: \(O(1)\)
if the front is empty, we need to reverse the back: \(O(n)\)
We will use amortization to show that \(n\) operations take only \(O(n)\) time (because expensive dequeues are infrequent).
Consider \(n\) operations over a data structure:
\[ s_0 \stackrel{t_1}{\mapsto} s_1 \stackrel{t_2}{\mapsto} s_2 \mapsto \cdots \stackrel{t_n}{\mapsto} s_n \]
\(s_{i-1}\) is the state before operation \(i\)
\(s_i\) is the state after operation \(i\) (and before \(i+1\))
\(t_i\) is the actual cost for operation \(i\)
We want an upper bound to the accumulated cost: \[ \sum_{i=1}^n t_i \]
Assume we find a function (called potential):
\[ \phi : S \rightarrow \mathbb{R}^+_0 \]
from the states \(s_0\), \(s_1\), etc. to non-negative numbers.
Define amortized costs \(a_i\) of operation \(i\):
\[ a_i := \underbrace{t_i}_{\text{actual cost}} + \underbrace{\phi(s_i)}_{\text{potential after}} - \underbrace{\phi(s_{i-1})}_{\text{potential before}} \]
Theorem: If
then: \[ \sum_{i=1}^n a_i \geq \sum_{i=1}^n t_i\ . \]
(The accumulated amortized costs are an upper-bound on the accumulated actual costs.)
\[ \begin{align} \sum_{i=1}^n a_i & = \sum_{i=1}^n \left(t_i + \phi(s_i) - \phi(s_{i-1})\right) \\ & = \sum_{i=1}^n t_i + \underbrace{\sum_{i=1}^n \left(\phi(s_i) - \phi(s_{i-1})\right)}_{\text{telescoping sum}} \\ & = \sum_{i=1}^n t_i + \phi(s_n) - \phi(s_0) \\ & \geq \sum_{i=1}^n t_i \quad (\text{because}~\phi(s_0)=0 ~\text{and}~ \phi(s_n)\geq0) \end{align} \]
When doing amortized analysis we can choose the potential function such that:
\[ \begin{array}{ccc} s_{i-1} &\stackrel{t_i}{\longmapsto} & s_i \\ \vdots & & \vdots \\ \phi(s_{i-1}) & \underbrace{\color{blue}{\phi(s_i) - \phi(s_{i-1})}}_{\text{change}} & \phi(s_i) \end{array} \]
type Queue = ([Elem], [Elem]) -- front, back
enqueue :: Elem -> Queue -> Queue
enqueue elem (front, back) = (front, elem:back)
dequeue :: Queue -> (Elem, Queue)
dequeue ([],back) = extract (reverse back) []
dequeue (front, back) = extract front back
-- helper function
extract (x:front) back = (x, (front,back))
extract [] back = error "empty queue"
We will use potential method to show that:
Recall that we first need to define a potential function:
\[ \phi : \text{Queue} \to \mathbb{R}^+_0 \]
Let
\[ \phi(f,b) := \text{length}~b \]
Observe:
\[ \begin{array}{rl} \phi(f,b) & \geq 0\\ \phi(\texttt{[]},\texttt{[]}) &= 0 \end{array} \]
The amortized cost \(a\) is:
\[ \begin{array}{rccc} a & =& \underbrace{1}_{\text{actual cost}} &+\quad \underbrace{\phi(f,x:b)}_{\text{potential after}} &-\quad \underbrace{\phi(f, b)}_{\text{potential before}} \\ & =& 1 & +\quad (1+\text{length}~b) &-\quad \text{length}~b\\ & =& 2 \end{array} \]
If the front is non-empty:
The amortized cost is:
\[ \begin{array}{rccc} a & =& \underbrace{1}_{\text{actual cost}} &+\quad \underbrace{\phi(f,b)}_{\text{potential after}} &-\quad \underbrace{\phi(x:f, b)}_{\text{potential before}} \\ & =& 1 &+\quad \text{length}~b &-\quad \text{length}~ b \\ & =& 1 \end{array} \]
If the front non-empty:
The actual cost is \(\text{length}~b + 1\). The amortized cost is:
\[ \begin{array}{rccc} a & =& \underbrace{1+\text{length}~b}_{\text{actual cost}} &+\quad \underbrace{\phi(xs,\texttt{[]})}_{\text{potential after}} &-\quad \underbrace{\phi(\texttt{[]}, b)}_{\text{potential before}} \\ & =& 1 + \text{length}~b & + \quad 0 & - \quad \text{length}~b\\ & = &1 \end{array} \]
Conclusion: all operations have \(O(1)\) amortized cost.
Implement a Haskell module for an abstract type for queues that are polymorphic on items, e.g.
Export all needed operations but don’t export the constructor.
Implement a variant that allows inserting and extracting from both the front and the back of the queue.
type Deque
type Elem
empty :: Deque
isEmpty :: Deque -> Bool
pushFront :: Elem -> Deque -> Deque
pushBack :: Elem -> Deque -> Deque
popFront :: Deque -> (Elem, Deque)
popBack :: Deque -> (Elem, Deque)