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:
- If is in and is in , then is in .
- If is in for any constant , then is in .
- If is in and is in , then is in .
- If is in and is in , then is in .
Example: Simple assignment
We begin with an analysis of a simple assignment to an integer variable.
= b a
Because the assignment statement takes constant time, it is .
Example: Simple for-loop
Consider a simple for
loop.
sum = 0
for i = 0 to n-1:
sum = sum + n
The first line is
.
The for
loop is repeated
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
.
By rule (3), the cost of the entire code fragment is also
.
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.
= k A[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
.
The second for
loop is just like the one in Example #FLAnal and
takes
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
.
Because the inner for
loop is executed
times, by simplifying rule (4) it has cost
.
The outer for
loop is executed
times, but each time the cost of the inner loop is different because it
costs
with
changing each time. You should see that for the first execution of the
outer loop,
is 1. For the second execution of the outer loop,
is 2. Each time through the outer loop,
becomes one greater, until the last time through the loop when
.
Thus, the total cost of the loop is
times the sum of the integers 1 through
.
We know that
which is . By simplifying rule (3), is simply .
Example: Comparing nested loops
Compare the asymptotic analysis for the following two code fragments.
= 0
sum1 for i = 0 to n-1: // First double loop.
for j = 0 to n-1: // Do n times.
= sum1 + 1
sum1
= 0
sum2 for i = 0 to n-1 // Second double loop.
for j = 0 to i-1: // Do i times.
= sum2 + 1 sum2
In the first double loop, the inner for
loop always
executes
times. Because the outer loop executes
times, it should be obvious that the statement sum1=sum1+1
is executed precisely
times. The second loop is similar to the one analyzed in the previous
example, with cost
.
This is approximately
.
Thus, both double loops cost
,
though the second requires about half the time of the first.
Example: Non-quadratic nested loops
Not all doubly nested for
loops are
.
The following pair of nested loops illustrates this fact.
= 0
sum1 = 1
k while k <= n: // Do log n times.
for j = 0 to n-1: // Do n times.
= sum1 + 1
sum1 = k * 2
k
= 0
sum2 = 1
k while k <= n: // Do log n times.
for j = 0 to k-1: // Do k times.
= sum2 + 1
sum2 = k * 2 k
When analyzing these two code fragments, we will assume that
is a power of two. The first code fragment has its outer
for
loop executed
times because on each iteration
is multiplied by two until it reaches
.
Because the inner loop always executes
times, the total cost for the first code fragment can be expressed
as
So the cost of this first double loop is . Note that a variable substitution takes place here to create the summation, with .
In the second code fragment, the outer loop is also executed times. The inner loop has cost , which doubles each time. The summation can be expressed as
where is assumed to be a power of two and again .
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
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
,
the then
clause of an if
statement might be
executed with probability
.
An example would be an if
statement that executes the
then
clause only for the smallest of
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
The closed-form solution for this recurrence relation is .
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 is equally likely to appear in any location is 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.3 Analyzing Binary Search
Function binarySearch
is designed to find the (single)
occurrence of
and return its position. A special value is returned if
does not appear in the array. This algorithm can be modified to
implement variations such as returning the position of the first
occurrence of
in the array if multiple occurrences are allowed, and returning the
position of the greatest value less than
when
is not in the array.
Comparing sequential search to binary search, we see that as grows, the running time for sequential search in the average and worst cases quickly becomes much greater than the running time for binary search. Taken in isolation, binary search appears to be much more efficient than sequential search. This is despite the fact that the constant factor for binary search is greater than that for sequential search, because the calculation for the next search position in binary search is more expensive than just incrementing the current position, as sequential search does.
Note however that the running time for sequential search will be roughly the same regardless of whether or not the array values are stored in order. In contrast, binary search requires that the array values be ordered from lowest to highest. Depending on the context in which binary search is to be used, this requirement for a sorted array could be detrimental to the running time of a complete program, because maintaining the values in sorted order requires a greater cost when inserting new elements into the array. This is an example of a tradeoff between the advantage of binary search during search and the disadvantage related to maintaining a sorted array. Only in the context of the complete problem to be solved can we know whether the advantage outweighs the disadvantage.
3.7.4 Practice questions: Running time
Determine for the following code fragment in the average case. Assume that all variables are integers.
= b + c
a = a + e d
- Does any line of code get executed more than once?
Determine 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 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 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:
= AA[i][j]
tmp = AA[j][i]
AA[i][j] = tmp AA[j][i]
- 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 for the following code fragment in the average case. Assume that all variables are integers.
sum = 0
for i = 1 to n:
= 1
j while j <= n:
sum = sum + 1
= j * 2 j
- 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 for the following code fragment in the average case. Assume that all variables are integers.
sum = 0
= 1
i while i <= n:
for j = 1 to n:
sum = sum + 1
= i * 2 i
- 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 for the following code fragment in the average case. Assume that all variables are integers.
Assume that array A contains values, “random” takes constant time, and “sort” takes time.
sum = 0
for i = 0 to n-1:
for j = 0 to n-1:
= random(n)
A[j] 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 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 .
sum = 0
for i = 0 to n-1:
= 0
j while A[j] != i:
sum = sum + 1
= j + 1 j
- 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 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.