CMPU 102, Fall 2005 Lecture 10

Analysis of Algorithms cont'd, Queues

Binary Search

Binary search is a technique to quickly find an element in an array if the array is sorted.  The idea is analogous to how you would look up an entry in a phone book.  If you're looking for a name starting with "M", you don't start by looking at the beginning; instead, you start in the middle, and then depending on whether the page you're looking at is before or after the name you want, you flip some number of pages forward or backward.  Binary search works the same way.  You start in the middle of the array.  At each step:

  1. Compare the element you're looking at to the element you're trying to find.  If it's an exact match, then you're done.

  2. If the current element is greater than the one you're looking for, then the element you're trying to find is earlier in the array.

  3. If the current element is less than the one you're trying to find, then the one you're looking for is later in the array.

The idea is that at each step, you eliminate at least half of the remaining possibilities.

Binary search can be implemented iteratively using variables min and max.  At each step, min is the index of the beginning of the search range, and max is the index of the end of the search range.  By picking an element in the middle of the range min..max, we will either find the element we want, or we will narrow the search range.  In the following example, the elements min and max will be shown in blue, and the middle element will be shown in red.  The elements that are crossed out are ones that have been eliminated by the search.

looking for element 40

Step 1: 15 19 22 40 51 63 72 75 88 89
40 is less than 51

Step 2: 15 19 22 40 51 63 72 75 88 89
40 is greater than 19

Step 3: 15 19 22 40 51 63 72 75 88 89
40 is greater than 22

Step 4: 15 19 22 40 51 63 72 75 88 89
Found it!

In Java:

/**
 * Find the index containing the given element in a sorted array.
 *
 * @param array      a sorted array
 * @param element    the element to look for
 * @param comparator a Comparator to use to compare array elements
 * @return index of the element in the array, or -1 if the element
 *               is not found
 */
public static<E>
int binarySearch(E[] array, E element, Comparator<E> comparator)
{
    int min = 0;
    int max = array.length - 1;

    while (min <= max) {
        // pick the element halfway between min and max
        int mid = min + ((max - min) / 2);

        int cmp = comparator.compare(element, array[mid]);

        if (cmp == 0) {
            // found it
            return mid;
        } else if (cmp < 0) {
            // element is less than middle element
            max = mid - 1;
        } else {
            // element is greater than the middle element
            min = mid + 1;
        }
    }

    // not found
    return -1;
}

Analysis: at each step the algorithm eliminates at least half of the remaining elements from consideration, and terminates when the element is found, or when there are no more elements to search.  The maximum number of search steps is thus the number of times we can divide the length of the array by 2 until a single element remains.  The number of elements still in consideration at each step looks something like

n, n/2, n/4, ..., 2, 1

Or, from the other direction:

1, 2, 4, ..., n/2, n

Rewritten as powers of 2:

20, 21, 22, ..., 2log2 n - 1, 2log2 n

So the number of steps is at most log2 n.  Recall that a logarithm yields the exponent needed to raise a base (in this case 2) to a particular value (in this case n).  The identity

n = 2log2 n

holds trivially.

Many algorithms are based on the idea of repeated halving.  Such algorithms will almost always have a log term in their worst case running time.  Log functions grow extremely slowly.  For example, the base 2 log of 1,000,000,000 is about 30.  Therefore, you can think of a log n term as being "almost constant".  So, binary search is a fast searching algorithm.

Queues

A Queue is a First-In, First-Out data structure.  Elements go in one end (the tail) and emerge from the other end (the head) in the same order in which they were added.  You can think of a queue as being a line at a bank.  You start by waiting at the end of the line (the tail of the queue).  As people in the line are served by the bank teller, you move towards the front of the line (the head of the queue).  Eventually it's your turn to go the window; this is when you are at the head of the queue.

The usual way to draw a queue is as follows:

This queue shows the results of adding "A", then "B", then "C" to the queue.

Here is a generic Java interface showing the usual queue operations:

public interface Queue<E> {
    public void enqueue(E element);
    public E dequeue();
    public boolean isEmpty();
}

Enqueue adds a new element at the tail of the queue.  Dequeue removes the element from the front of the queue and returns it.  IsEmpty returns true if the queue is empty and false if it is nonempty.

Preconditions and postconditions

Operation Precondition Postcondition
enqueue(e) --- e is new tail element
dequeue !isEmpty() removes and returns head element
isEmpty --- returns true if empty, false if not

Queue implementation using a linked list

Much like a stack, it is easy to use a singly-linked list to implement a queue.  The main difference is that in a queue, elements are being added and removed from different ends of the list.  For this reason, we need fields in the queue class to refer to both the tail and head elements:

Elements removed from the head of the list exactly as with a stack.  However, elements are added to the tail of the queue by attaching them to the next field of the final element of the list, and then updating tail to point to the new tail element.  (Enqueuing an element in a empty queue will be a special case.)

class Node<E> {
    public E payload;
    public Node<E> next;
}

public class SLLQueue<E> implements Queue<E> {
    private Node<E> head;
    private Node>E> tail;

    public void enqueue(E element) {
        Node<E> nodeToInsert = new Node<E>();
        nodeToInsert.payload = element;
        if (head == null) {
            // queue is empty
            head = tail = nodeToInsert;
        } else {
            // append to tail node
            tail.next = nodeToInsert;

            // the new node is now the tail
            tail = nodeToInsert;
        }
    }

    public E dequeue() {
        if (isEmpty()) throw new IllegalStateException();

        // Get the element from the head node and remove the head node
        E result = head.payload;
        head = head.next;

        // Queue may have become empty
        if (head == null) {
            tail = null;
        }

        return result;
    }

    public boolean isEmpty() {
        return head == null;
    }
}