In this section,
is the number of elements of the array, and
is the number of updates.
The most straightforward implementation of a fully persistent array uses an arbitrary persistent map, whose keys are the numbers from 0 to n − 1. A persistent map may be implemented using a persistent balanced tree, in which case both updates and lookups would take
time. This implementation is optimal for the pointer machine model.[1]: 88–89
A fully persistent array may be implemented using an array and the
so-called Baker's trick.[2] This implementation is used in the OCaml module parray.ml[3] by Jean-Christophe Filliâtre.
In order to define this implementation, a few other definitions must
be given. An initial array is an array that is not generated by
an update on another array. A child of an array ar is an
array of the form ar.update(i,v), and ar is the parent
of ar.update(i,v). A descendant of an array ar is either
ar or the descendant of a child of ar. The initial array
of an array ar is either ar if ar is initial, or it is the
initial array of the parent of ar. That is, the initial array of
ar is the unique array init such that
, with init initial
and
an arbitrary sequence of indexes and
an arbitrary sequence of value. A
family of arrays is thus a set of arrays containing an initial
array and all of its descendants. Finally, the tree of a family of
arrays is the tree whose nodes are the
arrays, and with an edge e from ar to each of its children
ar.update(i,v).
A persistent array using Baker's trick consists of a pair with
an actual array called array and the tree of arrays. This tree
admits an arbitrary root - not necessarily the initial array. The
root may be moved to an arbitrary node of the tree. Changing the root
from root to an arbitrary node ar takes time proportional to
the depth of ar. That is, in the distance between root and
ar. Similarly, looking up a value takes time proportional to the
distance between the array and the root of its family. Thus, if the
same array ar may be lookup multiple times, it is more efficient
to move the root to ar before doing the lookup. Finally updating
an array only takes constant time.
Technically, given two adjacent arrays ar1 and ar2, with
ar1 closer to the root than ar2, the edge from ar1 to
ar2 is labelled by (i,ar2[i]), where i the only position
whose value differ between ar1 and ar2.
Accessing an element i of an array ar is done as follows. If
ar is the root, then ar[i] equals root[i]. Otherwise, let
e the edge leaving ar toward the root. If the label of e
is (i,v) then ar[i] equals v. Otherwise, let ar2 be
the other node of the edge e. Then ar[i] equals
ar2[i]. The computation of ar2[i] is done recursively using
the same definition.
The creation of ar.update(i,v) consists in adding a new node
ar2 to the tree, and an edge e from ar to ar2 labelled
by (i,v).
Finally, moving the root to a node ar is done as follows. If
ar is already the root, there is nothing to do. Otherwise, let
e the edge leaving ar toward the current root, (i,v) its
label and ar2 the other end of e. Moving the root to ar is
done by first moving the root to ar2, changing the label of e
to (i, ar2[i]), and changing array[i] to v.
Updates take
time. Lookups take
time if the root is the array being looked up, but
time in the worst case.
In 1989, Dietz[4]
gave an implementation of fully persistent arrays using
space such that lookups can be done in
worst-case time, and updates can be done in
expected amortized time. By the lower bound from the previous section, this time complexity for lookup is optimal when
for
. This implementation is related to the order-maintenance problem and involves vEB trees, one for the entire array and one for each index.
Straka showed that the times for both operations can be (slightly) improved to
.[1]: 88–89
Straka showed how to achieve
worst-case time and linear (
) space, or
worst-case time and super-linear space. It remains open whether it is possible to achieve worst-case time
subject to linear space.[1]: 88