3.7 Calculating Program Running Time

This modules discusses the analysis for several simple code fragments. We will make use of the algorithm analysis simplifying rules:

  1. If f(n)f(n) is in O(g(n))O(g(n)) and g(n)g(n) is in O(h(n))O(h(n)), then f(n)f(n) is in O(h(n))O(h(n)).
  2. If f(n)f(n) is in O(kg(n))O(k g(n)) for any constant k>0k > 0, then f(n)f(n) is in O(g(n))O(g(n)).
  3. If f1(n)f_1(n) is in O(g1(n))O(g_1(n)) and f2(n)f_2(n) is in O(g2(n))O(g_2(n)), then f1(n)+f2(n)f_1(n) + f_2(n) is in O(max(g1(n),g2(n)))O(\max(g_1(n), g_2(n))).
  4. If f1(n)f_1(n) is in O(g1(n))O(g_1(n)) and f2(n)f_2(n) is in O(g2(n))O(g_2(n)), then f1(n)f2(n)f_1(n) f_2(n) is in O(g1(n)g2(n))O(g_1(n) g_2(n)).

Example: Simple assignment

We begin with an analysis of a simple assignment to an integer variable.

a = b

Because the assignment statement takes constant time, it is Θ(1)\Theta(1).

Example: Simple for-loop

Consider a simple for loop.

sum = 0
for i = 0 to n-1:
    sum = sum + n

The first line is Θ(1)\Theta(1). The for loop is repeated nn times. The third line takes constant time so, by simplifying rule (4), the total cost for executing the two lines making up the for loop is Θ(n)\Theta(n). By rule (3), the cost of the entire code fragment is also Θ(n)\Theta(n).

Example: Many for-loops

We now analyze a code fragment with several for loops, some of which are nested.

sum = 0
for j = 0 to n-1:     // First for loop
    for i = 0 to j-1: // is a double loop.
        sum = sum + 1
for k = 0 to n-1:     // Second for loop.
    A[k] = k

This code fragment has three separate statements: the first assignment statement and the two for loops. Again the assignment statement takes constant time; call it c1c_1. The second for loop is just like the one in Example #FLAnal and takes c2n=Θ(n)c_2 n = \Theta(n) time.

The first for loop is a double loop and requires a special technique. We work from the inside of the loop outward. The expression sum++ requires constant time; call it c3c_3. Because the inner for loop is executed jj times, by simplifying rule (4) it has cost c3jc_3j. The outer for loop is executed nn times, but each time the cost of the inner loop is different because it costs c3jc_3j with jj changing each time. You should see that for the first execution of the outer loop, jj is 1. For the second execution of the outer loop, jj is 2. Each time through the outer loop, jj becomes one greater, until the last time through the loop when j=nj = n. Thus, the total cost of the loop is c3c_3 times the sum of the integers 1 through nn. We know that

i=1ni=n(n+1)2 \sum_{i = 1}^{n} i = \frac{n (n+1)}{2}

which is Θ(n2)\Theta(n^2). By simplifying rule (3), Θ(c1+c2n+c3n2)\Theta(c_1 + c_2 n + c_3 n^2) is simply Θ(n2)\Theta(n^2).

Example: Comparing nested loops

Compare the asymptotic analysis for the following two code fragments.

sum1 = 0
for i = 0 to n-1:      // First double loop.
    for j = 0 to n-1:  // Do n times.
        sum1 = sum1 + 1

sum2 = 0
for i = 0 to n-1       // Second double loop.
    for j = 0 to i-1:  // Do i times.
        sum2 = sum2 + 1

In the first double loop, the inner for loop always executes nn times. Because the outer loop executes nn times, it should be obvious that the statement sum1=sum1+1 is executed precisely n2n^2 times. The second loop is similar to the one analyzed in the previous example, with cost j=1nj\sum_{j = 1}^{n} j. This is approximately 12n2\frac{1}{2} n^2. Thus, both double loops cost Θ(n2)\Theta(n^2), though the second requires about half the time of the first.

Example: Non-quadratic nested loops

Not all doubly nested for loops are Θ(n2)\Theta(n^2). The following pair of nested loops illustrates this fact.

sum1 = 0
k = 1
while k <= n:           // Do log n times.
    for j = 0 to n-1:   // Do n times.
        sum1 = sum1 + 1
    k = k * 2

sum2 = 0
k = 1
while k <= n:           // Do log n times.
    for j = 0 to k-1:   // Do k times.
        sum2 = sum2 + 1
    k = k * 2

When analyzing these two code fragments, we will assume that nn is a power of two. The first code fragment has its outer for loop executed logn+1\log n+1 times because on each iteration kk is multiplied by two until it reaches nn. Because the inner loop always executes nn times, the total cost for the first code fragment can be expressed as

i=0lognn=nlogn \sum_{i=0}^{\log n} n = n \log n

So the cost of this first double loop is Θ(nlogn)\Theta(n \log n). Note that a variable substitution takes place here to create the summation, with k=2ik = 2^i.

In the second code fragment, the outer loop is also executed logn+1\log n+1 times. The inner loop has cost kk, which doubles each time. The summation can be expressed as

i=0logn2i=Θ(n) \sum_{i=0}^{\log n} 2^i = \Theta(n)

where nn is assumed to be a power of two and again k=2ik = 2^i.

What about other control statements? While loops are analyzed in a manner similar to for loops. The cost of an if statement in the worst case is the greater of the costs for the then and else clauses. This is also true for the average case, assuming that the size of nn does not affect the probability of executing one of the clauses (which is usually, but not necessarily, true). For switch statements, the worst-case cost is that of the most expensive branch. For subroutine calls, simply add the cost of executing the subroutine.

There are rare situations in which the probability for executing the various branches of an if or switch statement are functions of the input size. For example, for input of size nn, the then clause of an if statement might be executed with probability 1/n1/n. An example would be an if statement that executes the then clause only for the smallest of nn values. To perform an average-case analysis for such programs, we cannot simply count the cost of the if statement as being the cost of the more expensive branch. In such situations, the technique of amortized analysis can come to the rescue.

Determining the execution time of a recursive subroutine can be difficult. The running time for a recursive subroutine is typically best expressed by a recurrence relation. For example, the recursive factorial function calls itself with a value one less than its input value. The result of this recursive call is then multiplied by the input value, which takes constant time. Thus, the cost of the factorial function, if we wish to measure cost in terms of the number of multiplication operations, is one more than the number of multiplications made by the recursive call on the smaller input. Because the base case does no multiplications, its cost is zero. Thus, the running time for this function can be expressed as

T(n)=T(n1)+1, for n>1T(1)=0 \begin{eqnarray} T(n) &=& T(n-1) + 1, \textrm{ for } n>1 \\ T(1) &=& 0 \end{eqnarray}

The closed-form solution for this recurrence relation is Θ(n)\Theta(n).

3.7.1 Case Study: Two Search Algorithms

The final example of algorithm analysis for this section will compare two algorithms for performing search in an array. Earlier, we determined that the running time for sequential search on an array where the search value KK is equally likely to appear in any location is Θ(n)\Theta(n) in both the average and worst cases. We would like to compare this running time to that required to perform a binary search on an array whose values are stored in order from lowest to highest. Here is a visualization of the binary search method.

3.7.2 Binary Search Practice Exercise

3.7.4 Practice questions: Running time

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

a = b + c
d = a + e
  • Does any line of code get executed more than once?

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

sum = 0
for i = 0 to 2:
    for j = 0 to n-1:
        sum = sum + 1
  • How much work is done by the body of the inner for loop?
  • How many times is the inner for loop executed? Multiply this by the amount of work done by the body.
  • How many times is the outer for loop executed? Multiply this by the amount of work done by the inner for loop.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

sum = 0
for i = 0 to n*n-1:
    sum = sum + 1
  • How much work is done by the body of the inner for loop?
  • How many times is the for loop executed? Multiply this by the amount of work done by the body.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

for i = 0 to n-2:
    for j = i+1 to n-1:
        tmp = AA[i][j]
        AA[i][j] = AA[j][i]
        AA[j][i] = tmp
  • How much work is done by the body of the inner for loop?
  • How many times is the inner for loop executed? Multiply this by the amount of work done by the body.
  • How many times is the outer for loop executed? Multiply this by the amount of work done by the inner for loop.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

sum = 0
for i = 1 to n:
    j = 1
    while j <= n:
        sum = sum + 1
        j = j * 2
  • How much work is done by the body of the inner for loop?
  • How many times is the inner for loop executed? Multiply this by the amount of work done by the body.
  • How many times is the outer for loop executed? Multiply this by the amount of work done by the inner for loop.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

sum = 0
i = 1
while i <= n:
    for j = 1 to n:
        sum = sum + 1
    i = i * 2
  • How much work is done by the body of the inner for loop?
  • How many times is the inner for loop executed? Multiply this by the amount of work done by the body.
  • How many times is the outer for loop executed? Multiply this by the amount of work done by the inner for loop.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

Assume that array A contains nn values, “random” takes constant time, and “sort” takes nlognn \log n time.

sum = 0
for i = 0 to n-1:
    for j = 0 to n-1:
        A[j] = random(n)
    sort(A)
  • How much work is done by the body of the inner for loop?
  • How many times is the inner for loop executed? Multiply this by the amount of work done by the body.
  • How many times is the outer for loop executed? Multiply this by the amount of work done by the inner for loop.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

Assume array A contains a random permutation of the values from 0 to n1n-1.

sum = 0
for i = 0 to n-1:
    j = 0
    while A[j] != i:
        sum = sum + 1
        j = j + 1
  • How much work is done by the body of the inner for loop?
  • How many times is the inner for loop executed? Multiply this by the amount of work done by the body.
  • How many times is the outer for loop executed? Multiply this by the amount of work done by the inner for loop.

Determine Θ\Theta for the following code fragment in the average case. Assume that all variables are integers.

sum = 0
if EVEN(n):
    for i = 0 to n-1:
        sum = sum + 1
else:
    sum = sum + n
  • How much work is done by each branch of the if-then-else statement? Use the more expensive one.