CIS 121 April 19, 2000


In order to understand our discussion of the analysis of the complexity of algorithms, we need to review a few basic mathematical concepts.

If n is any positive integer, then

1 + 2 + 3 + ... + n = n(n+1)/2
It is also important to understand logarithms. The concept of a logarithm is a very ingenious one. Its major feature is that it turns multiplication into addition. The reason for this is the following very simple law.

an*am=am+n
A logarithm is actually an exponent. Associated with a logarithm is a number called its base. The logarithm of a number to a certain base is the exponent to which we must raise the base to achieve the number. For example,

102=100 and 103=1000
So the logarithm of 100 base 10 is 2, and the logarithm of 1000 base 10 is 3.

Another important base is 2. This base is important because much of what we do in computers involves the binary system in which there are two digits, 0 and 1.

Another important fact about logarithms is the change of base formula.

logan = logbn/logba
Notice that logba is a constant with respect to n.

Recall that the natural logarithm is the logarithm whose base is the number e. We normally omit the base when we refer to logarithms base e.

When we analyze an algorithm, we try to measure how much time it will take the algorithm to complete as a function of the number of items in the construct in question, for example the number of elements in an array. To establish a mechanism for measuring algorithms, we make the following definition.



What this definition says is that a function f(x) is bounded above by a contant multiple of g(x) as the value of x tends to infinity. That is, in the long run, f(x) is bounded above by a constant multiple of g(x).

This is read as "f is big O of g". Keep in mind that this only measures the behavior of the functions in the long run and does not tell us anything about the behavior of the function for small values of x. For example, in the long run n2 is larger than 1000000n, but in the short run it is not.

Before we try to analyze the efficiency of algorithms we need to establish some basic facts about the big O notation.



What this theorem tells us is that big O is similar to an ordering. It doesn't necessarily give us the best bound. For example, if a function is O(N), it is also O(N2).



What this theorem tells us is that in the long run the largest function dominates and we can ignore smaller terms.

O(1)

The graph of constant complexity is the following.

It is a horizontal line.

O(log N)

The Binary Search Algorithm
public class BinarySearch {
   int[] array;

   public BinarySearch(int[] array) {
      this.array = array;
   }

   public int binary(int key,int first,int last) {
      if (first > last) 
         return(array.length);
      else {
         int index = (first+last)/2;
         if (array[index]==key)
            return(index);
         else if (array[index]>key) 
            return(binary(key,first,index-1));
         else
            return(binary(key,index+1,last));
      }
   }

   public int search(int key) {
      return(binary(key,0,array.length-1));
   }

   public static void main(String[] args) {
      int[] array = {5,7,10,13,18,20,21};

      BinarySearch myBinarySearch = new BinarySearch(array);

      System.out.println(myBinarySearch.search(5));
      System.out.println(myBinarySearch.search(21));
   }
}   
  • BinarySearch.java

    Analysis of the Algorithm

    We assume the array is sorted. The algorithm works by halving the array in which we are searching. We examine the element in the middle of the array to see if it is the element we are looking for. If it is not then we compare the values, and if the element we are looking for is "smaller" than the one in the middle, then we search in the left subarray. If the element we are looking for is "larger" than the one in the middle, then we search in the right subarray.

    In order to obtain a big O estimate for how long this algorithm will take to complete, let us consider the number of times we must halve an array and search on one side or the other. That is we want to determine how many different arrays we will examine before we get to a one-element array. Notice that we are starting out with N elements in the array. The next we examine will have N/2 elements. As we proceed with the algorithm this pattern continues until we get to an array with one element in it.

    That is the lengths of the arrays which we examine as we proceed through the algorithm are
    N  N/2  N/4  ...  1
    
    It is not easy to determine the number of arrays we must examine. However if we look at the base 2 logarithm of each of these numbers, then it is easy to determine the number of steps needed on average.
    log2N  log2N-1  log2-2  ...  0
    
    So, on average, we must take log2N + 1 steps to find the element we are looking for.

    The graph of the logarithm is the following.

    It is a gentle curve from left to right.

    When the Input Size Doubles

    When the input size doubles, then the complexity factor increases by 1 since log2(2N) = log22 + log2N = log2N + 1. So the growth in the number of steps we must take in order to find the element we are looking for is up by 1.

    O(N)

    The Linear Search

    public class LinearSearch {
       int[] array;
    
       public LinearSearch(int[] array) {
          this.array = array;
       }
    
       public int search(int key) {
          int index = array.length;
          
          for (int i=0;i<=array.length-1;i++)
             if (array[i]==key)
                index=i;
    
          return(index);
       }
    
       public static void main(String[] args) {
          int[] array = {5,7,10,13,18,20,21};
    
          LinearSearch myLinearSearch = new LinearSearch(array);
    
          System.out.println(myLinearSearch.search(5));
          System.out.println(myLinearSearch.search(21));
       }
    }   
    
  • LinearSearch.java

    Analysis of the Algorithm

    This is a very simple algorithm that begins at the first element in the array and searches until it finds the element we are looking for. So on average we would look through N/2 of the elements, and since we can ignore constant factors in the long run, the algorithm is O(N).

    The graph of O(N) is the following.

    It is a non-horizontal line.

    O(N log N)

    The QuickSort
    public class QuickSort {
       int[] array;
    
       public QuickSort(int[] array) {
          this.array = array;
       }
    
       public void swap(int i,int j) {
          int temporary = array[i];
    
          array[i] = array[j];
          array[j] = temporary;
       }
    
       public int partition(int[] array,int first,int last) {
          int pivot = array[first];
          int lo = first;
          int hi = last;
    
          while (lo <= hi) {
             while ((lo <= last) && (array[lo] <= pivot)) lo++;
    
             while (array[hi] > pivot) hi--;
    
             if (lo < hi) {
                swap(lo,hi);
                lo++;
                hi--;
             }
          }
       
          swap(first,hi);
          return hi;
       }
    
       public void quicksort(int array[],int first,int last) {
          int pivotIndex;
    
          if (first < last) {
             pivotIndex = partition(array,first,last);
    
             quicksort(array,first,pivotIndex-1);
             quicksort(array,pivotIndex+1,last);
          }
       }
    
       public void sort() {
          quicksort(array,0,array.length-1);
       }
    
       public String toString() {
          String output = "";
    
          for (int i=0;i<=array.length-1;i++) {
             output += String.valueOf(array[i]);
             output += "\t";
          }
    
          return(output);
       }
    
       public static void main(String[] args) {
          int[] array = {18,7,21,5,13,10,20};
    
          QuickSort myQuickSort = new QuickSort(array);
    
          System.out.println(myQuickSort);
    
          myQuickSort.sort();
    
          System.out.println(myQuickSort);
       }
    }
    
  • QuickSort.java

    Analysis of the Algorithm

    Let us examine how this algorithm works. We pick an element of the array which we call the pivot. We locate the correct position of this pivot in the array. We do this by searching through the array, moving all elements "smaller" than the pivot to the left and all elements "larger" than the pivot to the right.

    After we have the pivot element in its correct position, we then send the left subarray and right subarray to the quicksort method.

    The graph of n log n is the following.

    It is like a very flat parabola.

    O(N2)

    The Bubble Sort
    public class BubbleSort {
       int[] array;
    
       public BubbleSort(int[] array) {
          this.array = array;
       }
    
       public void swap(int i,int j) {
          int temporary = array[i];
    
          array[i] = array[j];
          array[j] = temporary;
       }
    
       public void sort() {
          int i;
          int j;
    
          for (i=0;i<=array.length-1;i++)
             for (j=i+1;j<=array.length-1;j++)
                if (array[j]<array[i])
                   swap(i,j);
    
       }
    
       public String toString() {
          String output = "";
    
          for (int i=0;i<=array.length-1;i++) {
             output += String.valueOf(array[i]);
             output += "\t";
          }
    
          return(output);
       }
    
       public static void main(String[] args) {
          int[] array = {18,7,21,5,13,10,20};
    
          BubbleSort myBubbleSort = new BubbleSort(array);
    
          System.out.println(myBubbleSort);
    
          myBubbleSort.sort();
       
          System.out.println(myBubbleSort);
       }
    }
    
  • BubbleSort.java

    The graph of n2 is the following.

    It is a parabola.