Breadth-first Numbering: Functional Queues
I spent some time this weekend (okay fine, most of Saturday and Sunday afternoon) on an exercise Michael Baker shared on our “geeks” mailing list. The problem is a functional pearl from Chris Okasaki: given a binary tree, reproduce a structurally identical tree with its nodes numbered in breadth-first order.
For example, numbering this tree:
1 2 3 4 5 6 7 8 |
|
Should yield this tree:
1 2 3 4 5 6 7 8 |
|
If you’ve ever solved a search problem, this might sound stupid easy. But getting the details of a functional solution right can be a challenge. As Okasaki puts it in the paper:
…I presented the problem to many other functional programmers and was continually amazed at the baroque solutions I received in reply. With only a single exception, everyone who came near a workable answer went in a very different direction from my solution right from the very beginning of the design process. I gradually realized that I was witnessing some sort of mass mental block, a communal blind spot, that was steering programmers away from what seemed to be a very natural solution.
Before you read my baroque solution, you might want to try for yourself. I’ll wait.
Although I love Clojure, using built-in queues and lazy seqs felt like cheating. So I chose to use Racket with Rackunit, and tried to use as many primitives as possible.
Breadth-first traversal is easy with a queue, but an efficient functional queue can be tricky. Consing an element to the front of a Scheme list is cheap, but appending is expensive—it requires “cdring down” over all the elements. One solution (cribbed from Okasaki himself) is to represent a queue as a pair of lists. The list on the left is the head of the queue, so elements can be popped of in O(1) time. The right side represents the rest of the elements in reverse, so elements can be pushed on to the end in constant time. Here are the first steps towards an implementation: an empty queue with left and right selectors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Inserting an item conses it on to the right-side list:
1 2 3 4 5 6 7 |
|
To dequeue an item, “remove” it from the left side with car
, and
return a new queue, with the cdr
of the left side list:
1 2 3 4 5 6 7 8 |
|
When the left side is out of elements, reverse the right side list,
and swap it with the left. Here’s the buildup to swap-and-reverse-car
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Now we can write a dequeue function that really works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
That’s all it takes to build a simple functional queue. Unfortunately, it’s not very efficient. Reversing a list is the kind of O(n) operation we built our queue to avoid in the first place, but if many more items are inserted than removed, we’ll end up reversing and swapping a lot. We can do better—and I’ll explain how in my next post.