2013-04-21
Suppose we have a list implementation in which every element (or
node) in the list contains a head, which is the item at
this node and a tail, which is rest of the list. I’ve used
Scala here, but most of the code looks almost like pseudocode.
trait List {
def isEmpty: Boolean
def head: Int
def tail: List
}
class Cons(val head: Int, val tail: List) extends List {
def isEmpty = false
}
object Nil extends List {
def isEmpty = true
def head = throw new NoSuchElementException("Nil.head")
def tail = throw new NoSuchElementException("Nil.tail")
}One way to reverse this without any mutation is:
def reversed(xs: List): List = {
def aux(xs: List, acc: List) =
if (xs.isEmpty) acc
else reversed(xs.tail, new Cons(xs.head, acc))
aux(xs, Nil)
}Here is how this works. Suppose we have a list [1, 2, 3, 4, 5]. This is represented in our scheme as:
[]
/ \
1 []
/ \
2 []
/ \
3 []
/ \
4 []
/ \
5 Nil
Here, each node is a Cons and the left child of a node
is the value at that node and the right child is the tail, which can
either be another Cons or Nil.
The Scala code to create this list is:
val l = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Cons(5, Nil)))))Now, in the reversed function, all the work is done by
the auxilliary inner function aux. If the argument
xs to aux is empty(meaning a
Nil), return the accumulator argument acc.
Otherwise, add the head of xs to the accumulator and
recursively call reversed with just the tail of
xs. It is clear that the recursion always terminates, as
xs.tail will always have one element less than
xs and eventually, a call to
reversed(Nil, ...) will be made, which just returns the
second argument, breaking the recursion. Let us trace the execution of
reversed:
val l = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Cons(5, Nil)))))
reversed(l)
\_ aux([1, 2, 3, 4, 5], Nil)
\_ aux([2, 3, 4, 5], [1])
\_ aux([3, 4, 5], [2, 1])
\_ aux([4, 5], [3, 2, 1])
\_ aux([5], [4, 3, 2, 1])
\_ aux(Nil, [5, 4, 3, 2, 1])
|
<------------------[5, 4, 3, 2, 1]