CMPU 102, Fall 2005 | Lecture 10 |
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:
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 4051 63 72 75 88 8940 is greater than 19 Step 3:15 1922 4051 63 72 75 88 8940 is greater than 22 Step 4:15 19 224051 63 72 75 88 89Found 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:
2^{0}, 2^{1}, 2^{2}, ..., 2^{log2 n - 1}, 2^{log2 n}
So the number of steps is at most log_{2} 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 = 2^{log2 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.
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.
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
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; } }