Queues are data structures that contains elements in a certain order. A queue can be empty (i.e., not contain any element), one can test whether a queue is empty or not, an element can be enqueued into a queue, and an element can be dequeued from a non-empty queue. The two canonical orders are
Our goal is to define the polymorphic type:
type 'a queue
together with the following operators:
Food for thought:
The type of enqueue is in curried form. In uncurried form, this type would read ('a * 'a queue) -> 'a queue.
For symmetry with dequeue, one could be tempted to uncurry enqueue:
enqueue : ('a * 'a queue) -> 'a queue
dequeue : 'a queue -> ('a * 'a queue) option
In practice, though, both forms are equivalent and OCaml encourages a curried style.
The last element that was enqueued is the first to be dequeued. (A lifo queue is also known as a stack.)
The following unit-test function captures what it means for a queue to be last in, first out:
let test_lifo_queue_int empty is_empty enqueue dequeue =
let b0 = (is_empty empty = true)
and b1 = (is_empty (enqueue 3 empty) = false)
and b2 = (match dequeue empty with
| Some _ ->
false
| None ->
true)
and b3 = (match dequeue (enqueue 3 empty) with
| Some (x, q) ->
x = 3 && is_empty q
| None ->
false)
and b4 = (match dequeue (enqueue 4 (enqueue 3 empty)) with
| Some (x, q) ->
x = 4 && (match dequeue q with
| Some (y, q') ->
y = 3 && is_empty q'
| None ->
false)
| None ->
false)
(* etc. *)
in b0 && b1 && b2 && b3 && b4;;
In words:
The first element that was enqueued is the first to be dequeued.
The following unit-test function captures what it means for a queue to be first in, first out:
let test_fifo_queue_int empty is_empty enqueue dequeue =
let b0 = (is_empty empty = true)
and b1 = (is_empty (enqueue 3 empty) = false)
and b2 = (match dequeue empty with
| Some _ ->
false
| None ->
true)
and b3 = (match dequeue (enqueue 3 empty) with
| Some (x, q) ->
x = 3 && is_empty q
| None ->
false)
and b4 = (match dequeue (enqueue 4 (enqueue 3 empty)) with
| Some (x, q) ->
x = 3 && (match dequeue q with
| Some (y, q') ->
y = 4 && is_empty q'
| None ->
false)
| None ->
false)
(* etc. *)
in b0 && b1 && b2 && b3 && b4;;
In words:
It is straightforward to represent a last-in, first-out queue with a list because the enqueueing and dequeueing activities happen at the same end of this list. So the empty queue can be represented with the empty list, enqueueing can be implemented with List.cons, and dequeueing non-empty queues can be implemented with List.hd and List.tl, which operate in constant time:
type 'a lifo_queue_v1 = 'a list;;
let lifo_empty_v1 =
[];;
let lifo_is_empty_v1 q =
match q with
| [] ->
true
| _ :: _ ->
false;;
let lifo_enqueue_v1 x q =
x :: q;;
let lifo_dequeue_v1 q =
match q with
| [] ->
None
| x :: q' ->
Some (x, q');;
These OCaml functions pass the unit test:
# test_lifo_queue_int lifo_empty_v1 lifo_is_empty_v1 lifo_enqueue_v1 lifo_dequeue_v1;;
- : bool = true
#
Food for thought:
So for example, starting with the empty queue [],
It is far less straightforward to represent a first-in, first-out queue with a list because the enqueueing and dequeueing activities happen at both ends of this list. Let us study the two obvious implementations: enqueueing by copying the current list (Version 1), and dequeueing by copying the current list (Version 2).
In this representation, the queue is a list of elements where the last element in this list shall be the last to be dequeued:
type 'a fifo_queue_v1 = 'a list;;
let fifo_empty_v1 =
[];;
let fifo_is_empty_v1 q =
q = [];;
let fifo_enqueue_v1 x q =
let rec prepend q =
match q with
| [] ->
[x]
| x' :: q' ->
x' :: prepend q'
in prepend q;;
let fifo_dequeue_v1 q =
match q with
| [] ->
None
| x :: q' ->
Some (x, q');;
These OCaml functions pass the unit test:
# test_fifo_queue_int fifo_empty_v1 fifo_is_empty_v1 fifo_enqueue_v1 fifo_dequeue_v1
- : bool = true
#
Food for thought:
each of fifo_empty_v1, fifo_is_empty_v1, and fifo_dequeue_v1 operates in constant time;
fifo_enqueue_v1 operates in time and space proportional to the given queue, i.e., in linear time;
fifo_enqueue_v1 could be defined more concisely as follows:
let fifo_enqueue_v1 x q =
List.append q [x];;
using the infix operator @ that stands for list concatenation, fifo_enqueue_v1 could be defined even more briefly as follows:
let fifo_enqueue_v1 x q =
q @ [x];;
So for example, starting with the empty queue [],
In this representation, the queue is a list of elements where the first element in this list shall be the last to be dequeued:
type 'a fifo_queue_v2 = 'a list;;
let fifo_empty_v2 =
[];;
let fifo_is_empty_v2 q =
q = [];;
let fifo_enqueue_v2 x q =
x :: q;;
let fifo_dequeue_v2 q =
match q with
| [] ->
None
| x :: q' ->
let rec traverse x q =
match q with
| [] ->
(x, [])
| x' :: q' ->
let (last_element, prefix) = traverse x' q'
in (last_element, x :: prefix)
in Some (traverse x q');;
These OCaml functions pass the unit test:
# test_fifo_queue_int fifo_empty_v2 fifo_is_empty_v2 fifo_enqueue_v2 fifo_dequeue_v2;;
- : bool = true
#
Food for thought:
So for example, starting with the empty queue [],
The lists representing the queues are reverse of each other in Versions 1 and 2, and this representation incurs a linear cost, either to enqueue (in Version 1), or to dequeue (in Version 2).
Positing that the common case is that consecutive queue operations are the same, i.e., several enqueueing operations in a row, followed by several dequeueing operations in a row, let us adopt a flipped representation of the queue as a list indexed by a Boolean:
type 'a fifo_queue_v3 = bool * 'a list;;
The key idea is threefold:
This way, the linear cost only occurs when the queue shifts from being enqueued to being dequeued, and from being dequeued to being enqueued, instead of each time the queue is enqueued (in Version 1) or each time the queue is dequeued (in Version 2):
type 'a fifo_queue_v3 = bool * 'a list;;
let fifo_empty_v3 =
(true, []);;
let fifo_is_empty_v3 q =
match q with
| (_, []) ->
true
| (_, _ :: _) ->
false;;
let fifo_enqueue_v3 x (flag, xs) =
let xs = if flag
then xs
else List.rev xs
in (true, x :: xs);;
let fifo_dequeue_v3 (flag, xs) =
let xs = if flag
then List.rev xs
else xs
in match xs with
| [] ->
None
| x :: xs' ->
Some (x, (false, xs'));;
These OCaml functions pass the unit test:
# test_fifo_queue_int fifo_empty_v3 fifo_is_empty_v3 fifo_enqueue_v3 fifo_dequeue_v3;;
- : bool = true
#
Food for thought:
So for example, starting with the empty queue (true, []),
Each time the Boolean changes, there is a linear cost.
During his PhD studies, Chris Okasaki proposed an even more radical idea: to represent the queue as a pair of lists:
The key point was to reverse the second list and make it the first when the first list runs out of elements to dequeue:
type 'a fifo_queue_v4 = 'a list * 'a list;;
let fifo_empty_v4 =
([], []);;
let fifo_is_empty_v4 q =
q = ([], []);;
let fifo_enqueue_v4 x (pool, reversed_prefix) =
(pool, x :: reversed_prefix);;
let fifo_dequeue_v4 (pool, reversed_prefix) =
match pool with
| [] ->
(match List.rev reversed_prefix with
| [] ->
None
| x :: new_pool ->
Some (x, (new_pool, [])))
| x :: pool' ->
Some (x, (pool', reversed_prefix));;
These OCaml functions pass the unit test:
# test_fifo_queue_int fifo_empty_v4 fifo_is_empty_v4 fifo_enqueue_v4 fifo_dequeue_v4;;
- : bool = true
#
Food for thought:
So for example, starting with the empty queue ([], []),
Version 5 is a mild variation of Version 4 where the second list is represented as a function:
type 'a fifo_queue_v5 = 'a list * ('a list -> 'a list);;
let cons x xs =
x :: xs;;
let compose f g x =
f (g x);;
let fifo_empty_v5 =
([], fun es -> es);;
let fifo_is_empty_v5 (es, h) =
es = [] && h [] = [];;
let fifo_enqueue_v5 x (pool, h) =
(pool, compose h (cons x));;
let fifo_dequeue_v5 (pool, h) =
match pool with
| [] ->
(match h [] with
| [] ->
None
| x :: new_pool ->
Some (x, (new_pool, fun es -> es)))
| x :: pool' ->
Some (x, (pool', h));;
These OCaml functions pass the unit test:
# test_fifo_queue_int fifo_empty_v5 fifo_is_empty_v5 fifo_enqueue_v5 fifo_dequeue_v5;;
- : bool = true
#
Food for thought:
So for example, starting with the empty queue ([], h0), where h0 denotes a function that maps [] to [],
Clone Version 5 of the code above into a Version 6 where in the definition of the enqueue operation, (cons v) is composed on the left of h instead of on the right:
let fifo_enqueue_v6 x (pool, h) =
(pool, compose (cons x) h);;
Which kind of queue does Version 6 implement? A known one? A new one? Why?
Fix Version 5 so that fifo_is_empty_v5 operates in constant time.
In the resource file for the present lecture note, a type of binary tree is defined with a payload in the leaves:
type 'a binary_tree =
| Leaf of 'a
| Node of 'a binary_tree * 'a binary_tree;;
Four tree-traversing functions are defined that collect the payloads into a list:
Look at the output of these tree-traversing functions, characterize the traversal that each of these functions implements, and test your characterization with 4 new telling examples.
You should return
Friendly hints:
draw some binary trees based on the convention that in Node (t1, t2), t1 denotes the left subtree and t2 denotes the right subtree. For example, the tree Node (Node (Leaf 3, Leaf 9), Leaf 5) is depicted as follows:
use the traced versions of traverse_foo_lifo, traverse_foo_fifo, traverse_bar_lifo, and traverse_bar_fifo provided in the resource file to visualize what is going on.
Subsidiary (and optional) questions:
Fixed a typo in the narrative, thanks to Sviatlana Yasiukova’s eagle eye [07 Apr 2019]
Polished the narrative [02 Apr 2019]
Created [31 Mar 2019]