Amortization
The notion of amortization is important in situations where we care
only about the total running time of a group of operations and don’t
care about the running time of each individual operation. For
instance, given a sequence of n operations, we may wish to bound the
total running time of the sequence by O(n) without insisting that each
individual operation run in O(1) time. We might be satisfied if a few
operations run in O(log n) or even O(n) time, provided the total cost
of the sequence is only O(n). The following implementation of aQueue
exploits the idea of amortization and provides O(1) snoc (insert)
and remove operations. This is achieved by using two internal lists,
one to represent the front of the queue and the other to represent the
tail. The implementation contains no mutable operation, so it can be safely shared by
concurrent processes without explicit synchronization:
(define (mk) (cons null null))
(define front car)
(define rear cdr)
(define (apply-invariant q)
(if (and (null? (front q))
(not (null? (rear q))))
;; reverse eats up the credits accumulated
;; by the elements in the list.
(cons (reverse (rear q)) null)
q))
;; Every snoc adds a credit of one to the new element.
(define (snoc q e)
(apply-invariant (cons (front q) (cons e (rear q)))))
(define (head q)
(if (not (null? (front q)))
(caar q)
null))
(define (tail q)
(if (not (null? (front q)))
(apply-invariant (cons (cdr (front q)) (rear q)))
(apply-invariant (cons null (rear q)))))
(define (remove q)
(if (not (null? (front q)))
(apply-invariant (cons (cdr (front q)) (rear q)))))Sample usage:
> (define q (snoc (snoc (snoc (mk) 1) 2) 3)) > q => ((1) 3 2) > (front q) => (1) > (tail q) => ((2 3)) > (snoc q 4) => ((1) 4 3 2) > (remove q) => ((2 3)) > (remove (remove q)) => ((3))
Every function except tail takes O(1) worst-case time, but tail
takes O(n) worst-case time. How is this cost amortized over time? This
is done by assigning each element of the rear list a credit of 1.
Every snoc into a non-empty queue takes one actual step and
allocates a credit to the new element of the rear list, for an
amortized cost of two.
Every tail that does not reverse the rear list takes one actual step
and neither allocates nor spends any credits, for an amortized cost of
one. Finally, every tail that does reverse the rear list takes m + 1
actual steps, where m is the length of the rear list, and spends the
m credits contained by that list, for an amortized cost of m + 1 -
m = 1.
Reference: Purely Functional Data Structures by Chris Okasaki