CMPSCI 187: Programming With Data Structures
============================================

Today's topics
--------------

-   administrivia
-   queues: use and implementation

Administrivia
=============

Reminders
---------

A06 due Thursday. A06 note: Factorial comment has a typo. It states that
you should throw an IllegalArgumentException if n\<1; it should be n\<0;
n == 0 is the base case. We won't test this case due to ambiguity.

READ ASSIGNMENT DESCRIPTIONS. In particular, forbidden classes, classes
you're expected to create, etc.

Queues
======

Queues
------

A *queue* is like a stack -- it is an ADT that holds a collection of
elements. Like a stack, you can insert and remove elements. Unlike a
stack, you can only remove the *oldest* (earliest-inserted) element in
the queue.

We put elements into the "back" of the queue by "enqueueing" them, and
remove them from the front of the queue by "dequeueing" them. (This is
DJW's terminology, we'll see later that the Java API uses slightly
different terms.)

DJW's queues do not have the equivalent of their stack's `peek()` --
this time they don't seem to care the `dequeue()` is both an observer
and a transformer. Awesome.

Clicker question: Simple queue operations
-----------------------------------------

Another use: searching graphs
-----------------------------

*Graphs* are an abstraction we'll explore in more detail (and that
you'll see again and again in later CS classes). Not a plot of a
function, but a relationship between vertices and edges.

For example, you can make a graph of the NE states, where the vertices
are the states, and an edge exists between them iff they are adjacent
(on board).

The DJW `blob` example from the last two classes can be trivially
transformed into a graph -- but so can many other problems. We'll focus
on blob for now, but keep in mind everything we do is general to
anything that can be described as a graph.

Our `markBlobs()` and `visit()` methods used recursion to find all the
filled squares in a connected cluster. Once we searched all neighbors of
a square, we were done with it and returned to its predecessor. This
resulted in a *backtracking* search.

We could do this with an explicit stack instead of recursion, and the
behavior would be the same. We'd start by visiting the first node. To
visit a node, we'd mark it, then push each of its neighbors onto the
stack. Then we'd loop until the stack was empty.

Pseudocode:

    stack.push(first node)
    while (stack not empty)
      node = stack.pop()
      mark node as visited
      for each neighbor:
        if unvisited
          stack.push(neighbor)

At any given time, the stack holds the nodes we are waiting to search
next.

This search is *greedy* in the sense that it pursues a given path
completely before backtracking -- we go deeper rather than broader
whenever possible. Hence the name *depth-first search*. (show on board)

What if we use the same algorithm, but with a stack instead of a queue?
That is, what if it looked like:

    queue.enqueue(first node)
    while (queue not empty)
      node = queue.dequeue()
      mark node as visited
      for each neighbor:
        if unvisited
          queue.enqueue(neighbor)

We'd visit the first square, then all of its neighbors, then all of
their neighbors, and so on. (show on board)

In other words, we'd visit all the squares at *distance* 1 from the
starting square before any square at distance 2, then all at distance 2
before any at distance 3, etc.

This is called a *breadth-first* search, because we search broadly
through the graph instead of going deeply in one direction.

Distance in a graph
-------------------

We define the distance between one square (i, j) and another (i', j') in
a Grid as the length of shortest path consisting of filled squares
between the squares, using only orthogonal moves, or infinity if there
is no such path.

Clicker question: distance
--------------------------

We can find the distance between two squares on a grid by starting a
queue-based search from one to the other, and remembering the distance
to each square we visit. If we empty the queue without finding the
target, it's unreachable from the starting square.

(draw example on board)

BFS is guaranteed to find the shortest path. But it searches every node
at distance k before searching any node at distance k+1. This may or may
not be a good idea; DFS searches deeply, but might ignore a nearby
solution if it goes down the "wrong" path first. You'll learn more about
these (and other search strategies that can leverage information you
know about the graph) in CMPSCI 250 and later courses.

Clicker question: Breadth-first search
--------------------------------------

Clicker question: Depth-first search
------------------------------------

Implementing queues using linked lists
--------------------------------------

Recall to implement a stack with linked lists, we used just a head
pointer. This made sense, because we always manipulated the same end of
the list.

Queues have to manipulate both ends of the list, so we'll need a head
and tail pointer. Should we enqueue at the head or the tail? And where
should we dequeue?

We know that both adding and removing at the head are pretty
straightforward, since that's what we did in stacks. What about at the
tail?

(on board)

Adding at the tail is trivial, but removing the tail in a singly-linked
list is not (either O(n), or involved an `penultimate` pointer and
special case checking). So let's enqueue onto the tail, and dequeue from
the head.

What does the code look like? (Abbreviated from DJW)

``` {.java}
public class Queue<T> {
  LLNode<T> head;
  LLNode<T> tail;
  
  public Queue() {
    head = null;
    tail = null;
  }
...
```

When we enqueue, we make a new node and add it to the rear. Special
case: if the queue is empty, our new node will be both the head and the
tail. (on board)

``` {.java}
public void enqueue(T value) {
  LLNode<T> node = new LLNode<T>(value);
  if (head == null) {
    head = newNode);
  } else {
    tail.setNext(newNode);
  }
  tail = newNode;
}
```

Clicker question: Changing `enqueue`
------------------------------------

Dequeuing is similar. We remove the head node from the list and return
its value. The corresponding special case is if the node is the last
node; if so, we need to remember to also set tail to null. (on board)

``` {.java}
public T dequeue() {
  T value = head.getInfo();
  head = head.getNext();
  if (head == null) {
    tail = null);
  }
  return value;
}
```

Implementing queues using arrays
--------------------------------

Suppose we want to use arrays to back our queue, like we did with
stacks. With stacks, we fixed the element at index 0 as the "bottom" of
the stack, and maintained a top pointer. What if we do this with queues?

(on board)

We can easily enqueue elements by keeping a pointer to the last element
enqueued. But what about dequeuing? We'd have to remove the element at
position zero, then move the element from position 1 into 0, 2 in to 1,
and so on. That's O(n) and terrible.

The right thing to do parallels the linked list implementation; instead
of a head and tail pointer, we'll maintain two indices into the array,
and allow both the front and rear of the queue to move through the
array.

There is a trick, though: we need to now thing of the array as a circle,
not as a linear array, that "wraps around". This is kind of like a
clock: (on board)

Now, instead of incrementing our front/rear indices, we use
`front = (front + 1) % capacity)` -- this is modular arithmetic, and
"wraps around" exactly as we want.

We'll define `rear` as the last occupied index (and initialize to what?
virtual position "-1" == capacity - 1). Thus the next place to enqueue
is `rear + 1`. We'll define `front` as the next occupied index to
`dequeue` and initialize it to 0.

Clicker question: Queue size
----------------------------

We have to keep track of the `size`, since both an empty queue and a
full queue have rear just before front.

``` {.java}
  public class Queue<T> {
  public final int DEFAULT_CAP = 100;
  T[] queue;
  int size = 0;
  int front = 0;
  int rear;
  
  public Queue(int capacity) {
    queue = (T[])new Object[capacity];
    rear = capacity - 1;
  }
  public Queue() {
    this(DEFAULT_CAP);
  }
  
  public boolean isFull() {
    return size == queue.length;
  }
  
  public boolean isEmpty() {
    return size == 0;
  }
  
  public void enqueue(T value) throws QueueOverflowException {
    if (isFull()) {     
      throw new QueueOverflowException();
    }
    rear = (rear + 1) % queue.length;
    queue[rear] = element;
    size++;
  }
  
  public T dequeue() throws QueueUnderflowException {
    if (isEmpty()) {
      throw new QueueUnderflowException();
    }
    T value = queue[front];
    front = (front + 1) % queue.length;
    size--;
    return value;
  }
}
```

Do we really have to track `size` in an instance variable? No. It turns
out we can compute it:

    public int size() {
      if (head == null) {
        return 0;
      }
      else {
        return (queue.length + rear - front + 1) % queue.length;
      }
    }

Usually we prefer to O(1) compute properties like `size()` rather than
track them when modified, but in this case it's a question of
taste/style as to which is more straightforward.

Is the above clearer than the use of an explicit `size` counter?
Debatable. Is it less error-prone to implement? Debatable. Use your
judgment, and develop it by reading others' code.

Unbounded queues with arrays, why we double
-------------------------------------------

In Assignment 04, you implemented an unbounded stack by copying into a
new array of double the capacity of the original array. The `ArrayList`
class in `java.util` also uses doubling.

You could do the same here. DJW implement an unbounded array-based queue
using *incrementing* -- where they increase the capacity array by a
fixed amount each time. Why do we generally double? For O(1) behavior,
averaged across all n elements. Here's the idea:

If we increment k each time the capacity is full, we're essentially
doing:

k + 2k + 3k + ... mk work, where n = mk

This is m \* (m + 1) / 2 work, or n/k \* (n/k + 1) / 2, which is
O(n\^2).

But this work is averaged over the n elements. O(n\^2) / n \~= O(n) work
per element.

If we double the array each time the capacity is full, we're doing:

2\^0 + 2\^1 + 2\^2 + ... + 2\^k

work in total. where n = 2\^k. This sum equals:

2\^(k+1) - 1

which is

2 \* 2\^k - 1

And, the biggest array we see is of length n, so

n = 2\^k

or equivalently

log\_2 n = k

so:

2 \* 2\^(log\_2 n) - 1 =

2n - 1

which is O(n). Again, this work is averaged over the n elements. O(n) /
n \~= O(1) work per element. This is why we double rather than used a
fixed-size increment.

(There is more to the story, having to do with how memory allocators
work, but the above is a good first approximation.)

Clicker question: Big-O doubling
--------------------------------

Big-O(ther) things to think about
---------------------------------

What are the big-O runtimes of each operation in the two implementations
we've talked about? What are the big-O size requirements?

Administrivia
=============

A06 due Thursday.

Read sections 5.7, 5.8.
