• No results found

As an example of this technique, we convert the amortized banker’s queues of Section 3.4.2 to worst-case queues. Queues such as these that support all operations in

O(1)

worst-case time are called real-time queues [HM81].

In the original data structure, queues are rotated using ++ and

reverse

. Since

reverse

is monolithic, our first task is finding a way to perform rotations incrementally. This can be done by executing one step of the reverse for every step of the ++. We define a function

rotate

such that

rotate (

f

,

r

,

a

) =

f

++ reverse

r

++

a

Then

The extra argument

a

is called an accumulating parameter and is used to accumulate the partial results of reversing

r

. It is initially empty.

Rotations occur when j

R

j

=

j

F

j

+ 1

, so initially j

r

j

=

j

f

j

+ 1

. This relationship is

preserved throughout the rotation, so when

f

is empty,

r

contains a single element. The base case is therefore

rotate ($Nil, $Cons (

y

, $Nil),

a

)

=

($Nil) ++ reverse ($Cons (

y

, $Nil)) ++

a

=

$Cons (

y

,

a

)

In the recursive case,

rotate ($Cons (

x

,

f

), $Cons (

y

,

r

),

a

)

=

($Cons (

x

,

f

)) ++ reverse ($Cons (

y

,

r

)) ++

a

=

$Cons (

x

,

f

++ reverse ($Cons (

y

,

r

)) ++

a

)

=

$Cons (

x

,

f

++ reverse

r

++ $Cons (

y

,

a

))

=

$Cons (

x

, rotate (

f

,

r

, $Cons (

y

,

a

))) The complete code for

rotate

is

fun rotate (

f

,

r

,

a

) = $case (

f

,

r

) of

($Nil, $Cons (

y

, )))Cons (

y

,

a

) j($Cons (

x

,

f

0 ), $Cons (

y

,

r

0 )))Cons (

x

, rotate (

f

0 ,

r

0 , $Cons (

y

,

a

))) Note that the intrinsic cost of every suspension created by

rotate

is

O(1)

. Just rewriting the pseudo-constructor

queue

to call

rotate

(

f

,

r

, $

Nil

) instead

f

++

reverse r

, and making no other changes, already drastically improves the worst-case behavior of the queue operations from

O(n)

to

O(log n)

(see [Oka95c]), but we can further improve the worst-case behavior to

O(1)

using scheduling.

We begin by adding a schedule to the datatype. The original datatype is

datatype

Queue = QueuefF :

Stream, LenF : int, R :

Stream, LenR : intg

We add a new field

S

of type

Stream

that represents a schedule for forcing the nodes of

F

.

S

is some suffix of

F

such that all the nodes before

S

in

F

have already been forced and memoized. To force the next suspension in

F

, we simply inspect the first node of

S

.

Besides adding

S

, we make two further changes to the datatype. First, to emphasize the fact that the nodes of

R

need not be scheduled, we change

R

from a stream to a list. This involves minor changes to

rotate

. Second, we eliminate the length fields. As we will see shortly, we no longer need the length fields to determine when

R

becomes longer than

F

— instead, we will obtain this information from the schedule. The new datatype is thus

datatype

Queue = Queue offF :

stream, R :

list, S :

streamg

structure RealTimeQueue : QUEUE=

struct

datatypeQueue = Queue offF :stream, R :list, S :streamg (Invariant:jSj=jFj;jRj)

exception EMPTY

val empty = QueuefF = $Nil, R = [ ], S = $Nilg

fun isEmpty (QueuefF =

f

, . . .g) = null

f

fun rotate (

f

,

r

,

a

) = $case (

f

,

r

) of

($Nil, $Cons (

y

, )))Cons (

y

,

a

) j($Cons (

x

,

f

0 ), $Cons (

y

,

r

0 )))Cons (

x

, rotate (

f

0 ,

r

0 , $Cons (

y

,

a

)))

fun queuefF =

f

, R =

r

, S = $Cons (

x

,

s

)g= QueuefF =

f

, R =

r

, S =

s

g jqueuefF =

f

, R =

r

, S = $Nilg= let val

f

0 = rotate (

f

,

r

, $Nil) in QueuefF =

f

0 , R = [ ], S =

f

0 gend

fun snoc (QueuefF =

f

, R =

r

, S =

s

g,

x

) = queuefF =

f

, R =

x

::

r

, S =

s

g

fun head (QueuefF = $Nil, . . .g) = raise EMPTY jhead (QueuefF = $Cons (

x

,

f

), . . .g) =

x

fun tail (QueuefF = $Nil, . . .g) = raise EMPTY

jtail (QueuefF = $Cons (

x

,

f

), R =

r

, S =

s

g) = queuefF =

f

, R =

r

, S =

s

g

end

Figure 4.1: Real-time queues based on scheduling [Oka95c].

fun snoc (QueuefF =

f

, R =

r

, S =

s

g,

x

) = queuefF =

f

, R =

x

::

r

, S =

s

g

fun head (QueuefF = $Cons (

x

,

f

), . . .g) =

x

fun tail (QueuefF = $Cons (

x

,

f

), R =

r

, S =

s

g) = queuefF =

f

, R =

r

, S =

s

g

The pseudo-constructor

queue

maintains the invariant thatj

S

j

=

j

F

j;j

R

j(which incidentally

guarantees that j

F

j j

R

j since j

S

j cannot be negative).

snoc

increases j

R

j by one and

tail

decreasesj

F

jby one, so when

queue

is called,j

S

j

=

j

F

j;j

R

j

+ 1

. If

S

is non-empty, then

we restore the invariant by simply taking the tail of

S

. If

S

is empty, then

R

is one longer than

F

, so we rotate the queue. In either case, inspecting

S

to determine whether or not it is empty forces and memoizes the next suspension in the schedule.

fun queuefF =

f

, R =

r

, S = $Cons (

x

,

s

)g= QueuefF =

f

, R =

r

, S =

s

g jqueuefF =

f

, R =

r

, S = $Nilg= let val

f

0 = rotate (

f

,

r

, $Nil) in QueuefF =

f

0 , R = [ ], S =

f

0 gend

In the amortized analysis, the unshared cost of every queue operation is

O(1)

. Therefore, every queue operation does only

O(1)

work outside of forcing suspensions. Hence, to show that all queue operations run in

O(1)

worst-case time, we must prove that no suspension takes more than

O(1)

time to execute.

Only three forms of suspensions are created by the various queue functions.

$

Nil

is created by

empty

and

queue

(in the initial call to

rotate

). This suspension is

trivial and therefore executes in

O(1)

time regardless of whether it has been forced and memoized previously.

$

Cons

(

y

,

a

) is created in the second line of

rotate

and is also trivial. Every call to

rotate

immediately creates a suspension of the form

$case (

f

,

r

,

a

) of ($Nil, [

y

],

a

))Cons (

y

,

a

) j($Cons (

x

,

f

0 ),

y

::

r

0 ,

a

))Cons (

x

, rotate (

f

0 ,

r

0 , $Cons (

y

,

a

)))

The intrinsic cost of this suspension is

O(1)

. However, it also forces the first node of

f

, creating the potential for a cascade of forces. But note that

f

is a suffix of the front stream that existed just before the previous rotation. The treatment of the schedule

S

guarantees that every node in that stream was forced and memoized prior to the rotation. Forcing the first node of

f

simply looks up that memoized value in

O(1)

time. The above suspension therefore takes only

O(1)

time altogether.

Since every suspension executes in

O(1)

time, every queue operation takes only

O(1)

worst- case time.

Hint to Practitioners: These queues are not particularly fast when used ephemerally, because of overheads associated with memoizing values that are never looked at again, but are the fastest known real-time implementation when used persistently.