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 nonempty 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 nonempty 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 [],
Concretely, implementing each step and naming each successive queue:
# let q0 = lifo_empty_v1;;
val q0 : 'a list = []
# let q1 = lifo_enqueue_v1 1 q0;;
val q1 : int list = [1]
# let q2 = lifo_enqueue_v1 2 q1;;
val q2 : int list = [2; 1]
# let q3 = lifo_enqueue_v1 3 q2;;
val q3 : int list = [3; 2; 1]
# let q4 = match lifo_dequeue_v1 q3 with Some (3, q) -> q | _ -> assert false;;
val q4 : int list = [2; 1]
# let q5 = match lifo_dequeue_v1 q4 with Some (2, q) -> q | _ -> assert false;;
val q5 : int list = [1]
# let q6 = lifo_enqueue_v1 4 q5;;
val q6 : int list = [4; 1]
# let q7 = lifo_enqueue_v1 5 q6;;
val q7 : int list = [5; 4; 1]
# let q8 = match lifo_dequeue_v1 q7 with Some (5, q) -> q | _ -> assert false;;
val q8 : int list = [4; 1]
# let q9 = match lifo_dequeue_v1 q8 with Some (4, q) -> q | _ -> assert false;;
val q9 : int list = [1]
# let q10 = match lifo_dequeue_v1 q9 with Some (1, q) -> q | _ -> assert false;;
val q10 : int list = []
#
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 [],
Concretely, implementing each step and naming each successive queue:
# let q0 = fifo_empty_v1;;
val q0 : 'a list = []
# let q1 = fifo_enqueue_v1 1 q0;;
val q1 : int list = [1]
# let q2 = fifo_enqueue_v1 2 q1;;
val q2 : int list = [1; 2]
# let q3 = fifo_enqueue_v1 3 q2;;
val q3 : int list = [1; 2; 3]
# let q4 = match fifo_dequeue_v1 q3 with Some (1, q) -> q | _ -> assert false;;
val q4 : int list = [2; 3]
# let q5 = match fifo_dequeue_v1 q4 with Some (2, q) -> q | _ -> assert false;;
val q5 : int list = [3]
# let q6 = fifo_enqueue_v1 4 q5;;
val q6 : int list = [3; 4]
# let q7 = fifo_enqueue_v1 5 q6;;
val q7 : int list = [3; 4; 5]
# let q8 = match fifo_dequeue_v1 q7 with Some (3, q) -> q | _ -> assert false;;
val q8 : int list = [4; 5]
# let q9 = match fifo_dequeue_v1 q8 with Some (4, q) -> q | _ -> assert false;;
val q9 : int list = [5]
# let q10 = match fifo_dequeue_v1 q9 with Some (5, q) -> q | _ -> assert false;;
val q10 : int list = []
#
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 [],
Concretely, implementing each step and naming each successive queue:
# let q0 = fifo_empty_v2;;
val q0 : 'a list = []
# let q1 = fifo_enqueue_v2 1 q0;;
val q1 : int list = [1]
# let q2 = fifo_enqueue_v2 2 q1;;
val q2 : int list = [2; 1]
# let q3 = fifo_enqueue_v2 3 q2;;
val q3 : int list = [3; 2; 1]
# let q4 = match fifo_dequeue_v2 q3 with Some (1, q) -> q | _ -> assert false;;
val q4 : int list = [3; 2]
# let q5 = match fifo_dequeue_v2 q4 with Some (2, q) -> q | _ -> assert false;;
val q5 : int list = [3]
# let q6 = fifo_enqueue_v2 4 q5;;
val q6 : int list = [4; 3]
# let q7 = fifo_enqueue_v2 5 q6;;
val q7 : int list = [5; 4; 3]
# let q8 = match fifo_dequeue_v2 q7 with Some (3, q) -> q | _ -> assert false;;
val q8 : int list = [5; 4]
# let q9 = match fifo_dequeue_v2 q8 with Some (4, q) -> q | _ -> assert false;;
val q9 : int list = [5]
# let q10 = match fifo_dequeue_v2 q9 with Some (5, q) -> q | _ -> assert false;;
val q10 : int list = []
#
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.
Concretely, implementing each step and naming each successive queue:
# let q0 = fifo_empty_v3;;
val q0 : bool * 'a list = (true, [])
# let q1 = fifo_enqueue_v3 1 q0;;
val q1 : bool * int list = (true, [1])
# let q2 = fifo_enqueue_v3 2 q1;;
val q2 : bool * int list = (true, [2; 1])
# let q3 = fifo_enqueue_v3 3 q2;;
val q3 : bool * int list = (true, [3; 2; 1])
# let q4 = match fifo_dequeue_v3 q3 with Some (1, q) -> q | _ -> assert false;;
val q4 : bool * int list = (false, [2; 3])
# let q5 = match fifo_dequeue_v3 q4 with Some (2, q) -> q | _ -> assert false;;
val q5 : bool * int list = (false, [3])
# let q6 = fifo_enqueue_v3 3 q5;;
val q6 : bool * int list = (true, [3; 3])
# let q6 = fifo_enqueue_v3 4 q5;;
val q6 : bool * int list = (true, [4; 3])
# let q7 = fifo_enqueue_v3 5 q6;;
val q7 : bool * int list = (true, [5; 4; 3])
# let q8 = match fifo_dequeue_v3 q7 with Some (3, q) -> q | _ -> assert false;;
val q8 : bool * int list = (false, [4; 5])
# let q9 = match fifo_dequeue_v3 q8 with Some (4, q) -> q | _ -> assert false;;
val q9 : bool * int list = (false, [5])
# let q10 = match fifo_dequeue_v3 q9 with Some (5, q) -> q | _ -> assert false;;
val q10 : bool * int list = (false, [])
#
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 ([], []),
Concretely, implementing each step and naming each successive queue:
# let q0 = fifo_empty_v4;;
val q0 : 'a list * 'b list = ([], [])
# let q1 = fifo_enqueue_v4 1 q0;;
val q1 : 'a list * int list = ([], [1])
# let q2 = fifo_enqueue_v4 2 q1;;
val q2 : 'a list * int list = ([], [2; 1])
# let q3 = fifo_enqueue_v4 3 q2;;
val q3 : 'a list * int list = ([], [3; 2; 1])
# let q4 = match fifo_dequeue_v4 q3 with Some (1, q) -> q | _ -> assert false;;
val q4 : int list * int list = ([2; 3], [])
# let q5 = match fifo_dequeue_v4 q4 with Some (2, q) -> q | _ -> assert false;;
val q5 : int list * int list = ([3], [])
# let q6 = fifo_enqueue_v4 4 q5;;
val q6 : int list * int list = ([3], [4])
# let q7 = fifo_enqueue_v4 5 q6;;
val q7 : int list * int list = ([3], [5; 4])
# let q8 = match fifo_dequeue_v4 q7 with Some (3, q) -> q | _ -> assert false;;
val q8 : int list * int list = ([], [5; 4])
# let q9 = match fifo_dequeue_v4 q8 with Some (4, q) -> q | _ -> assert false;;
val q9 : int list * int list = ([5], [])
# let q10 = match fifo_dequeue_v4 q9 with Some (5, q) -> q | _ -> assert false;;
val q10 : int list * int list = ([], [])
#
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 [],
Concretely, implementing each step and naming each successive queue:
# let q0 = fifo_empty_v5;;
val q0 : 'a list * ('b -> 'b) = ([], <fun>)
# let (_, h0) = q0 in h0 [] = [];;
- : bool = true
# let q1 = fifo_enqueue_v5 1 q0;;
val q1 : 'a list * (int list -> int list) = ([], <fun>)
# let (_, h1) = q1 in h1 [] = [1];;
- : bool = true
# let q2 = fifo_enqueue_v5 2 q1;;
val q2 : 'a list * (int list -> int list) = ([], <fun>)
# let (_, h2) = q2 in h2 [] = [1; 2];;
- : bool = true
# let q3 = fifo_enqueue_v5 3 q2;;
val q3 : 'a list * (int list -> int list) = ([], <fun>)
# let (_, h3) = q3 in h3 [] = [1; 2; 3];;
- : bool = true
# let q4 = match fifo_dequeue_v5 q3 with Some (1, q) -> q | _ -> assert false;;
val q4 : int list * (int list -> int list) = ([2; 3], <fun>)
# let (_, h0) = q4 in h0 [] = [];;
- : bool = true
# let q5 = match fifo_dequeue_v5 q4 with Some (2, q) -> q | _ -> assert false;;
val q5 : int list * (int list -> int list) = ([3], <fun>)
# let (_, h0) = q5 in h0 [] = [];;
- : bool = true
# let q6 = fifo_enqueue_v5 4 q5;;
val q6 : int list * (int list -> int list) = ([3], <fun>)
# let (_, h4) = q6 in h4 [] = [4];;
- : bool = true
# let q7 = fifo_enqueue_v5 5 q6;;
val q7 : int list * (int list -> int list) = ([3], <fun>)
# let (_, h5) = q7 in h5 [] = [4; 5];;
- : bool = true
# let q8 = match fifo_dequeue_v5 q7 with Some (3, q) -> q | _ -> assert false;;
val q8 : int list * (int list -> int list) = ([], <fun>)
# let (_, h5) = q8 in h5 [] = [4; 5];;
- : bool = true
# let q9 = match fifo_dequeue_v5 q8 with Some (4, q) -> q | _ -> assert false;;
val q9 : int list * (int list -> int list) = ([5], <fun>)
# let (_, h0) = q9 in h0 [] = [];;
- : bool = true
# let q10 = match fifo_dequeue_v5 q9 with Some (5, q) -> q | _ -> assert false;;
val q10 : int list * (int list -> int list) = ([], <fun>)
# let (_, h0) = q10 in h0 [] = [];;
- : bool = true
#
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:
Created [10 Jan 2023]