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]