18.3. Example Problems¶
Example 1
Here is an algorithm that solves the problem of printing all elements of an array.
void printA(int[] a) { for(int i = 0; i < a.length; i++) { System.out.println(a[i]); } // for } // printA
Questions:
What is the problem size?
What is
T(n)
if the set of key processing steps only includes print-like statements? Note: We choose print statements as our key processing step because they take significantly longer to execute when compared to the other operations in this method (addition, assignment, array access, etc.).
Think about the answers to the previous two questions before reading ahead
Towards a Sample Solution:
What is the problem size? In this example, the problem size is the array length. We know this because the number of operations that will execute is determined by the length of the array. If we increase the size of
a
, that directly impacts the number of print statements that will execute. Therefore, letn = a.length
.What is
T(n)
if the set of key processing steps only includes print-like statements?You could try to find a pattern by performing multiple executions of the program with different array lengths and recording the number of print-like statements:
n = a.length
# print-like statements
0
0
1
1
2
2
...
...
4
4
...
...
8
8
...
...
16
16
...
...
After multiple executions, we think we see a pattern:
T(n) = n
. Indeed, if we check this formula for each row in the table we made, then we’ll see that this formula works! However, this strategy of finding a pattern is often tedious and error-prone as the algorithms get more complicated. Instead, we prefer to use a more systematic approach that leads to fewer errors and gives us insight into the structure of the algorithm with respect to the key processing steps.NOTE: That being said, creating the table is a good way to verify your formula for
T(n)
after deriving it systematically.Now, to derive
T(n)
for this algorithm in a more systematic way, let’s first identify the most nested operation – here, operation refers to any instruction in the set of key processing steps:void printA(int[] a) { for(int i = 0; i < a.length; i++) { System.out.println(a[i]); // -------> 1 } // for } // printA
Next, we identify the enclosing loop:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { // -----\ System.out.println(a[i]); // -------> 1 | n } // for // --------------------------------/ } // printA
Finally, we identify how many iterations the loop will have with respect to
n
:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // -----\ System.out.println(a[i]); // -------> 1 | n } // for // --------------------------------/ } // printA
If you label the operations and iterations as we did in the diagram above, then you can simply multiply across in a simple scenario like this:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { // -----\ System.out.println(a[i]); // -------> 1 | n ⇒ T(n) = 1 * n } // for // --------------------------------/ } // printA
Therefore,
T(n) = n
for thisprintA
method. You might read this as “The number of print statements this method performs is equal to the length of the array”.
Example 2:
Here is an algorithm that solves the problem of printing all elements of an array in a square fashion:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { for(int j = 0; j < a.length; j++) { System.out.print(a[i] + " "); } // for System.out.println(); } // for } // printA
Questions:
What is the problem size?
What is
T(n)
if the set of key processing steps only includes print-like statements?
Think about the answers to the previous two questions before reading ahead
Towards a Sample Solution:
What is the problem size? In this example, the problem size is the array length. Therefore, let
n = a.length
.What is
T(n)
if the set of key processing steps only includes print-like statements? To answer this question, let’s first identify the most nested operation:void printA(int[] a) { for(int i = 0; i < a.length; i++) { for(int j = 0; j < a.length; j++) { System.out.print(a[i] + " "); // -------> 1 } // for System.out.println(); } // for } // printA
Next, we identify the enclosing loop and its number of iterations:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { for(int j = 0; j < a.length; j++) { // ----------\ System.out.print(a[i] + " "); // -------> 1 | n } // for // ------------------------------------/ System.out.println(); } // for } // printA
It appears that there is another print-like statement at the same level as the loop we just identified:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { for(int j = 0; j < a.length; j++) { // ----------\ System.out.print(a[i] + " "); // -------> 1 | n } // for // ------------------------------------/ System.out.println(); // ------------------------> 1 } // for } // printA
Now, we identify the next enclosing loop and its number of iterations:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { // ------------------\ for(int j = 0; j < a.length; j++) { // ----------\ | System.out.print(a[i] + " "); // -------> 1 | n | n } // for // ------------------------------------/ | System.out.println(); // ------------------------> 1 | } // for ------------------------------------------------/ } // printA
If you label the operations and iterations as we did in the diagram above, then you can simply multiply across and add up the results in a scenario like this:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { // ------------------\ for(int j = 0; j < a.length; j++) { // ----------\ | System.out.print(a[i] + " "); // -------> 1 | n | n ⇒ T(n) = 1 * n * n } // for // ------------------------------------/ | System.out.println(); // ------------------------> 1 | + 1 * n } // for ------------------------------------------------/ } // printA
Therefore,
T(n) = n^2 + n
for this particularprintA
method.
Example 3 [Tricky]:
Here is a modified version of the previous example:
void printA(int[] a) { for(int i = 0; i < a.length; i++) { for(int j = 0; j < 10; j++) { System.out.print(a[i] + " "); } // for System.out.println(); } // for } // printA
Questions:
What is the problem size?
What is
T(n)
if the set of key processing steps only includes print-like statements?
Think about the answers to the previous two questions before reading ahead
Towards a Sample Solution:
What is the problem size? In this example, the problem size is the array length. Therefore, let
n = a.length
.What is
T(n)
if the set of key processing steps only includes print-like statements? To answer this question, let’s diagram the code the way we did in the previous examples:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // -------------------\ for(int j = 0; j < 10; j++) { // ----------------\ | System.out.print(a[i] + " "); // -------> 1 | 10 | n } // for // ------------------------------------/ | System.out.println(); // ------------------------> 1 | } // for -------------------------------------------------/ } // printA
This was the tricky part. The inner for-loop only iterates
10
times. This changes the overall timing function. Just as before, you can simply multiply across and add up the results:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // -------------------\ for(int j = 0; j < 10; j++) { // ----------------\ | System.out.print(a[i] + " "); // -------> 1 | 10 | n ⇒ T(n) = 1 * 10 * n } // for // ------------------------------------/ | System.out.println(); // ------------------------> 1 | + 1 * n } // for -------------------------------------------------/ } // printA
Therefore,
T(n) = 11n
for this particularprintA
method!Note: One brutally common mistake is to assume that the number of the resulting polynomial always increases with the number of loop nestings. This is not the case, as is illustrated in the last example. Even though there are two loops that are nested, the overall degree of the polynomial is
1
and not2
.
Example 4 [Trickier]:
Here is a slightly modified version of the previous example (pay close attention to spot the subtle differences):
void printA(int[] a) { for(int i = 0; i < a.length; i++) { for(int j = 0; j < i; j++) { System.out.print(a[i] + " "); } // for System.out.println(); } // for } // printA
Questions:
What is the problem size?
What is
T(n)
if the set of key processing steps only includes print-like statements?
Think about the answers to the previous two questions before reading ahead
Towards a Sample Solution:
What is the problem size? In this example, the problem size is the array length. Therefore, let
n = a.length
.What is
T(n)
if the set of key processing steps only includes print-like statements? To answer this question, let’s diagram the code the way we did in the previous examples:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // --------------------\ for(int j = 0; j < i; j++) { // -----------------\ | System.out.print(a[i] + " "); // -------> 1 | < n | n } // for // ------------------------------------/ | System.out.println(); // ------------------------> 1 | } // for --------------------------------------------------/ } // printA
This was the tricky part. The inner for-loop may have as many as
n - 1
iterations, however, this number changes based on what iteration of the outermost loop the code is in. We don’t want to mark the loop as having the minimum number since that would undercount, however, it’s okay in this scenario to mark it with the maximum number (technically an overcount) so long as we indicate it’s<
that number. Just as before, you can simply multiply across and add up the results, this time accounting for the presence of<
instead of an=
:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // --------------------\ for(int j = 0; j < i; j++) { // -----------------\ | System.out.print(a[i] + " "); // -------> 1 | < n | n ⇒ T(n) < 1 * n * n } // for // ------------------------------------/ | System.out.println(); // ------------------------> 1 | + 1 * n } // for --------------------------------------------------/ } // printA
Therefore,
T(n) < n^2 + n
for this particularprintA
method!
Example 5 [Even Trickier]:
Below is a slightly modified version of the previous example, now decomposed into two separate methods.
void printUntil(int[] a, int i) { for(int j = 0; j < i; j++) { System.out.print(a[i] + " "); } // for } // printUntil
void printA(int[] a) { for(int i = 0; i < a.length; i++) { printUntil(a, i); System.out.println(); } // for } // printA
Here, we will focus our analysis on the algorithm described by
printA
. Since this example has the same code as the previous example, albeit split between two methods, our analysis should result in the same timing function.Questions:
What is the problem size?
What is
T(n)
if the set of key processing steps only includes print-like statements?
Think about the answers to the previous two questions before reading ahead
Towards a Sample Solution:
What is the problem size? In this example, we actually have two problem sizes!
The problem size for
printA
is the array length because increasing or decreasing that length impacts how long the method takes to execute. Therefore, letn = a.length
when discussing this method.The problem size for
printUntil
is its parameteri
. Although we are passing the array into the method, the array’s length does not impact how long this method takes to execute; however, it’s parameteri
does. Therefore, letn = i
when discussing this method.
Before we continue, please remember that
n
(the problem size) means something different depending on the method that we’re analyzing.First, let’s analyze
printUntil
to derive its timing function. So that we don’t confuse the function we derive for this method with the one we’ll derive forprintA
, letU(n)
denote for this method wheren = i
. What isU(n)
if the set of key processing steps only includes print-like statements? To answer this question, let’s diagram the code the way we did in the previous examples:void printUntil(int[] a, int i) { for(int j = 0; j < i; j++) { // -------------------\ System.out.print(a[i] + " "); // ----------> 1 | = i ⇒ U(n) = 1 * i } // for // ---------------------------------------/ = 1 * n [since n = i] } // printUntil = n
As we can see,
U(n) = n
print-like statements forprintUntil
wheren = i
. Remember that the problem sizen
forprintUntil
is different than the problem size forprintA
.Now that we have derived the timing function for our helper method, let’s analyze the
printA
method. What isT(n)
if the set of key processing steps only includes print-like statements? Remember that the problem sizen
forprintA
is different than the problem size forprintUntil
. To answer this question, let’s diagram the code the way we did in the previous examples:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // ----------------\ printUntil(a, i); // -------------------> U(i) | n [where n = a.length] System.out.println(); // -------------------> 1 | } // for // -------------------------------------------/ } // printA
Before we continue, we note thatU(i)
here is theU(n)
function that we previously derived forprintUntil
called to explicitly supplyi
as itsn
value. This is called function composition.SinceprintA
utilizesprintUntil
, it stands to reason thatT(n)
needs to utilizeU(n)
:To complete this tricky derivation, we now use the result of the function composition to replace
U(n)
with its result:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // ---------------\ printUntil(a, i); // -------------------> i | n [since U(i) = i] System.out.println(); // -------------------> 1 | } // for // ------------------------------------------/ } // printA
We want out final derivation to be in terms of
n
. Within the observed loop, we see thatprintUntil(a, i)
will execute1 * i
print-like statements. We further observe that the largest value thati
can be inside its enclosed loop isn
. Therefore, we simplify the expression in order to provide a reasonable upper bound with respect ton
, then complete the derivation:void printA(int[] a) { for(int i = 0; i < a.length; i++) { // ---------------\ printUntil(a, i); // -------------------> < n | n ⇒ T(n) < n * n System.out.println(); // -------------------> 1 | + 1 * n } // for // ------------------------------------------/ } // printA
Therefore,
T(n) < n^2 + n
for this particularprintA
method! This should not be surprising as this is the same algorithm from Example 4 decomposed into two separate methods.
Example 6 [Even Trickier 2]:
Here is an algorithm that solves the problem of printing characters of a string, in reverse, cutting the index value by
3
each time.void printS(String s) { for(int i = s.length() - 1; i > 0; i = i / 3) { System.out.println(s.charAt(i)); } // for } // printS
Questions:
What is the problem size?
What is
T(n)
if the set of key processing steps only includes print-like statements?
Think about the answers to the previous two questions before reading ahead
Towards a Sample Solution:
What is the problem size? In this example, the problem size is the string length. Therefore, let
n = s.length()
.What is
T(n)
if the set of key processing steps only includes print-like statements? If we follow our established strategy for a derivation, we might end up with the following diagram:void printS(String s) { for(int i = s.length() - 1; i > 0; i = i / 3) { // --\ System.out.println(s.charAt(i)); // ---------> 1 | < n ⇒ T(n) < 1 * n } // for // -----------------------------------------/ } // printS
While this is correct, it is not the best estimate for an upper bound that we can derive for the number of iterations of the observed loop. To get a better estimate, we first observe that this loop behaves a little differently than the loops we’ve observed in the previous examples – instead of incrementing or decrementing by some fixed value, the loop variable instead decreases by multiplying or dividing by some fixed value. In this case, the loop variable
i
is divided by3
after each iteration. We can use this to our advantage to derive a better estimate.Without loss of generality, assume that
n = s.length()
is a power of3
. To determine how many iterations of the loop occur, we might ask the following question:If you start with
n - 1
, how many times can you integer divide by3
before you get to0
?This can be slightly reworded to:
If you start with
n
, how many times can you integer divide by3
until the value is1
?If we let
k
denote the number of iterations of the loop, then we have the following:void printS(String s) { for(int i = s.length() - 1; i > 0; i = i / 3) { // --\ System.out.println(s.charAt(i)); // ---------> 1 | k } // for // -----------------------------------------/ } // printS
This can be modeled as a math problem:
n / 3^k = 1
To answer the question “how many times can you integer divide”, we need only solve for
k
using some precalculus magic (i.e., logarithms):Original model:
n / 3^k = 1
Multiply both sides by
3^k
:3^k = n
Take the logarithm of both sides:
log(3^k) = log(n)
Move the exponent out of the logarithm:
k * log(3) = log(n)
Divide both sides by
log(3)
:k = log(n) / log(3)
Assuming both logarithms are the same base (they are), dividing the first logarithm by
log(3)
changes the base of the logarithm to3
:k = log3(n)
where
log3(n)
denotes the base-3 logarithm ofn
.
We did make a pretty big assumption that
n
is a power of3
. Ifn
is not a power of3
, then the base-3 logarithm will not be in an integer – this poses a problem as the count for the number of iterations must be an integer. We could get fancy and use theceil
(ceiling function) to get an exact answer:k = ceil(log3(n))
However, this will make formally establishing a complexity class (covered later) a little trickier. Instead, we’ll let
k
denote an upper bound instead of an equality – that is, we’ll express it as some value (assumed to be an integer) less than or equal to the logarithm plus one:k ≤ log3(n) + 1
This is okay since
ceil(log3(n)) ≤ log3(n) + 1
for all reasonablen
values. In a Discrete Mathematics course, you would need to be more rigorous about this. For now, we’ll take it as a reasonable estimate. Now that we have a value fork
, we can substitute it into our derivation:void printS(String s) { for(int i = s.length() - 1; i > 0; i = i / 3) { // --\ System.out.println(s.charAt(i)); // ---------> 1 | ≤ log3(n) + 1 ⇒ T(n) ≤ 1 * log3(n) + 1 } // for // -----------------------------------------/ } // printS
Therefore,
T(n) ≤ log3(n) + 1
for this particularprintS
method! Taking advantage of the way the loop iterates differently (i.e., multiply/dividing vs. adding/subtracting) leads us to a much better estimate of the number of key processing steps with respect to the problem size.