On the way to a breadth-first
numbering
solution, I’ve built a simple functional queue and created lazy
lists
from Scheme primitives (and a couple handy macros). In this post,
I’ll bring the pieces together to create an improved queue. (And yes,
I promise I’ll actually start numbering trees sometime soon).
Our simple queue consisted of two lists: one for the head of the
queue, and a reversed one for the tail:
1
'((123)(54))
Our improved queue makes two changes: lazy lists and incremental
reversal. It will look like this:
This looks a little complicated, but just like the simple queue, it’s also a list of a head and
reversed tail, with each side storing the length of the associated
list. This equivalent is a little simpler:
1
'((llist'(123)).3)(llist'(54)).2))
To start implementing the improvements, we need to update the
selectors to get the lists and lengths from both sides:
1234567891011121314151617181920212223242526272829
(define empty-q(list (cons '()0)(cons '()0)))(define five-items(list (cons (llist'(12))2)(cons (llist'(345))3)))(define lhs-len(lambda (queue)(cdr (left-sidequeue))))(check-equal?(lhs-lenfive-items)2"Our lazy queue stores the length of the lists on each side.")(define rhs-len(lambda (queue)(cdr (right-sidequeue))))(check-equal?(rhs-lenfive-items)3"Our lazy queue stores the length of the lists on each side.")(define lhs-list(lambda (queue)(car (left-sidequeue))))(check-equal?(lcar(lhs-listfive-items))(lcar(llist'(12))))(define rhs-list(lambda (queue)(car (right-sidequeue))))(check-equal?(lcar(rhs-listfive-items))(lcar(llist'(345))))
Now we can write an updated insert function:
1234567891011
(define ins(lambda (itemqueue)(list (left-sidequeue)(cons (lconsitem(rhs-listqueue))(+ 1(rhs-lenqueue))))))(let ((three-items(ins3(ins2(ins1empty-q))))(six-items(ins6(ins5(ins4(ins3(ins2(ins1empty-q))))))))(check-equal?(take-n3(rhs-listthree-items))'(321))(check-equal?(take-n3(rhs-listsix-items))'(654)"Ins adds elements to the right side."))
Remove is a little more complicated. In the simple queue, we simply
swapped and reversed the right side. We want our improved queue to
avoid reversing long right side lists. The solution is incremental
reversal: rebalance the queue every time an element is removed.
In Okasaki’s implementation,
this is done with functions called make-queue and rotate. Below
are my Scheme translations.
Rotate reverses the right side list and concatenates it to the left.
It’s similar to the simple queue implementation, but it uses lazy
list operators and it’s designed to work incrementally:
1234567891011121314
(define rotate(lambda (leftright)(define rotate-recur(lambda (leftrightaccumulator)(if (null? left)(lcons(lcarright)accumulator)(lcons(lcarleft)(rotate-recur(lcdrleft)(lcdrright)(lcons(lcarright)accumulator))))))(rotate-recurleftright'())))(let ((rotated(rotate(lhs-listfive-items)(rhs-listfive-items))))(check-equal?(take-n5rotated)'(12543)"Rotate reverses the right side list and concatenates it to the left."))
Make-queue implements the incremental reversal logic. Now, we no
longer wait until the head list is empty to swap and reverse. Instead,
we rotate the queue as soon as the tail list contains one more element
than the head. This keeps the queue balanced, and ensures that we
won’t run into an expensive reversal:
1234567891011121314
(define make-queue(lambda (leftright)(if (<= (cdr right)(cdr left))(list leftright)(list (cons (rotate(car left)(car right))(+ (cdr left)(cdr right)))(cons '()0)))))(let ((rebalanced(make-queue(left-sidefive-items)(right-sidefive-items))))(check-equal?(take-n5(lhs-listrebalanced))'(12543))(check-equal?(rhs-listrebalanced)'()"Make-queue rebalances the queue when the right side is longer than the left."))
To maintain a balanced queue, we’ll want to call make-queue on
insertion and removal. Here’s an improved insert, and a new remove:
1234567891011121314151617181920212223242526272829
(define ins(lambda (itemqueue)(make-queue(left-sidequeue)(cons (lconsitem(rhs-listqueue))(+ 1(rhs-lenqueue))))))(let ((three-items(ins3(ins2(ins1empty-q))))(six-items(ins6(ins5(ins4(ins3(ins2(ins1empty-q))))))))(check-equal?(take-n3(lhs-listthree-items))'(123))(check-equal?(take-n3(lhs-listsix-items))'(123))(check-equal?(take-n3(rhs-listsix-items))'(654)"Ins adds elements to the right side and rebalances if it's longer than the left."))(define rem(lambda (queue)(if (and (null? (lhs-listqueue))(null? (rhs-listqueue)))'()(list (lcar(lhs-listqueue))(make-queue(cons (lcdr(car (left-sidequeue)))(- (lhs-lenqueue)1))(right-sidequeue))))))(let ((removed(rem(ins4(ins3(ins2(ins1empty-q)))))))(check-equal?(car removed)1)(check-equal?(take-n2(lhs-list(cadr removed)))'(23))(check-equal?(take-n1(rhs-list(cadr removed)))'(4)"Rem returns a pair: the element removed from the queue and the new queue."))
Finally, let’s add a couple convenience functions to insert and remove
multiple items:
1234567891011121314151617181920212223242526
(define ins-items(lambda (itemsqueue)(if (null? items)queue(ins-items(cdr items)(ins(car items)queue)))))(let ((seven-items(ins-items'(1234567)empty-q)))(check-equal?(take-n7(lhs-listseven-items))'(1234567)"Ins-items adds multiple items to the queue."))(define rem-n(lambda (nqueue)(define rem-n-iter(lambda (nqueueitems)(if (= 0n)(cons (reverse items)queue)(rem-n-iter(- n1)(car (cdr (remqueue)))(cons (car (remqueue))items)))))(rem-n-iternqueue'())))(let ((remove-four(rem-n4(ins-items'(1234567)empty-q))))(check-equal?(car remove-four)'(1234))(check-equal?(+ (lhs-len(cdr remove-four))(rhs-len(cdr remove-four)))3"Rem-n returns a list of removed items and the new queue."))
Next time, we’ll finally bring everything together to solve the
breadth-first numbering problem.