3.5 Asymptotic Analysis and Upper Bounds

Two views of a graph illustrating the growth rates for six equations. The bottom view shows in detail the lower-left portion of the top view. The horizontal axis represents input size. The vertical axis can represent time, space, or any other measure of cost.

Despite the larger constant for the curve labeled 10n10 n in the figure above, 2n22 n^2 crosses it at the relatively small value of n=5n = 5. What if we double the value of the constant in front of the linear equation? As shown in the graph, 20n20 n is surpassed by 2n22 n^2 once n=10n = 10. The additional factor of two for the linear growth rate does not much matter. It only doubles the xx-coordinate for the intersection point. In general, changes to a constant factor in either equation only shift where the two curves cross, not whether the two curves cross.

When you buy a faster computer or a faster compiler, the new problem size that can be run in a given amount of time for a given growth rate is larger by the same factor, regardless of the constant on the running-time equation. The time curves for two algorithms with different growth rates still cross, regardless of their running-time equation constants. For these reasons, we usually ignore the constants when we want an estimate of the growth rate for the running time or other resource requirements of an algorithm. This simplifies the analysis and keeps us thinking about the most important aspect: the growth rate. This is called asymptotic algorithm analysis. To be precise, asymptotic analysis refers to the study of an algorithm as the input size “gets big” or reaches a limit (in the calculus sense). However, it has proved to be so useful to ignore all constant factors that asymptotic analysis is used for most algorithm comparisons.

In rare situations, it is not reasonable to ignore the constants. When comparing algorithms meant to run on small values of nn, the constant can have a large effect. For example, if the problem requires you to sort many collections of exactly five records, then a sorting algorithm designed for sorting thousands of records is probably not appropriate, even if its asymptotic analysis indicates good performance. There are rare cases where the constants for two algorithms under comparison can differ by a factor of 1000 or more, making the one with lower growth rate impractical for typical problem sizes due to its large constant. Asymptotic analysis is a form of “back of the envelope” estimation for algorithm resource consumption. It provides a simplified model of the running time or other resource needs of an algorithm. This simplification usually helps you understand the behavior of your algorithms. Just be aware of the limitations to asymptotic analysis in the rare situation where the constant is important.

3.5.1 Upper Bounds

Several terms are used to describe the running-time equation for an algorithm. These terms – and their associated symbols – indicate precisely what aspect of the algorithm’s behavior is being described. One is the upper bound for the growth of the algorithm’s running time. It indicates the upper or highest growth rate that the algorithm can have.

Because the phrase “has an upper bound to its growth rate of f(n)f(n)” is long and often used when discussing algorithms, we adopt a special notation, called big-Oh notation. If the upper bound for an algorithm’s growth rate (for, say, the worst case) is (f(n)), then we would write that this algorithm is “in the set O(f(n))O(f(n)) in the worst case” (or just “in O(f(n))O(f(n)) in the worst case”). For example, if n2n^2 grows as fast as 𝐓(n)\mathbf{T}(n) (the running time of our algorithm) for the worst-case input, we would say the algorithm is “in O(n2)O(n^2) in the worst case”.

The following is a precise definition for an upper bound. 𝐓(n)\mathbf{T}(n) represents the true running time of the algorithm. f(n)f(n) is some expression for the upper bound.

For 𝐓(n)\mathbf{T}(n) a non-negatively valued function, 𝐓(n)\mathbf{T}(n) is in set O(f(n))O(f(n)) if there exist two positive constants cc and n0n_0 such that 𝐓(n)cf(n)\mathbf{T}(n) \leq cf(n) for all n>n0n > n_0.

Constant n0n_0 is the smallest value of nn for which the claim of an upper bound holds true. Usually n0n_0 is small, such as 1, but does not need to be. You must also be able to pick some constant cc, but it is irrelevant what the value for cc actually is. In other words, the definition says that for all inputs of the type in question (such as the worst case for all inputs of size nn) that are large enough (i.e., n>n0n > n_0), the algorithm always executes in less than or equal to cf(n)cf(n) steps for some constant cc.

Example: Quadratic algorithm

For a particular algorithm, 𝐓(n)=c1n2+c2n\mathbf{T}(n) = c_1 n^2 + c_2 n in the average case where c1c_1 and c2c_2 are positive numbers. Then,

c1n2+c2nc1n2+c2n2(c1+c2)n2 c_1 n^2 + c_2 n \leq c_1 n^2 + c_2 n^2 \leq (c_1 + c_2)n^2

for all n>1n > 1. So, 𝐓(n)cn2\mathbf{T}(n) \leq c n^2 for c=c1+c2c = c_1 + c_2, and n0=1n_0 = 1. Therefore, 𝐓(n)\mathbf{T}(n) is in O(n2)O(n^2) by the second definition.

Example: Accessing an array cell

Assigning the value from a given position of an array to a variable takes constant time regardless of the size of the array. Thus, 𝐓(n)=c\mathbf{T}(n) = c (for the best, worst, and average cases). We could say in this case that 𝐓(n)\mathbf{T}(n) is in O(c)O(c). However, it is traditional to say that an algorithm whose running time has a constant upper bound is in O(1)O(1).

If someone asked you out of the blue “Who is the best?” your natural reaction should be to reply “Best at what?” In the same way, if you are asked “What is the growth rate of this algorithm”, you would need to ask “When? Best case? Average case? Or worst case?” Some algorithms have the same behavior no matter which input instance of a given size that they receive. An example is finding the maximum in an array of integers. But for many algorithms, it makes a big difference which particular input of a given size is involved, such as when searching an unsorted array for a particular value. So any statement about the upper bound of an algorithm must be in the context of some specific class of inputs of size nn. We measure this upper bound nearly always on the best-case, average-case, or worst-case inputs. Thus, we cannot say, “this algorithm has an upper bound to its growth rate of n2n^2” because that is an incomplete statement. We must say something like, “this algorithm has an upper bound to its growth rate of n2n^2 in the average case”.

Knowing that something is in O(f(n))O(f(n)) says only how bad things can be. Perhaps things are not nearly so bad. Because sequential search is in O(n)O(n) in the worst case, it is also true to say that sequential search is in O(n2)O(n^2). But sequential search is practical for large nn in a way that is not true for some other algorithms in O(n2)O(n^2). We always seek to define the running time of an algorithm with the tightest (lowest) possible upper bound. Thus, we prefer to say that sequential search is in O(n)O(n). This also explains why the phrase “is in O(f(n))O(f(n))” or the notation “O(f(n))\in O(f(n))” is used instead of “is O(f(n))O(f(n))” or “=O(f(n))= O(f(n))”. There is no strict equality to the use of big-Oh notation. O(n)O(n) is in O(n2)O(n^2), but O(n2)O(n^2) is not in O(n)O(n).

3.5.2 Simplifying Rules

Once you determine the running-time equation for an algorithm, it really is a simple matter to derive the big-Oh expressions from the equation. You do not need to resort to the formal definitions of asymptotic analysis. Instead, you can use the following rules to determine the simplest form.

  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)).

The first rule says that if some function g(n)g(n) is an upper bound for your cost function, then any upper bound for g(n)g(n) is also an upper bound for your cost function.

The significance of rule (2) is that you can ignore any multiplicative constants in your equations when using big-Oh notation.

Rule (3) says that given two parts of a program run in sequence (whether two statements or two sections of code), you need consider only the more expensive part.

Rule (4) is used to analyze simple loops in programs. If some action is repeated some number of times, and each repetition has the same cost, then the total cost is the cost of the action multiplied by the number of times that the action takes place.

Taking the first three rules collectively, you can ignore all constants and all lower-order terms to determine the asymptotic growth rate for any cost function. The advantages and dangers of ignoring constants were discussed near the beginning of this section. Ignoring lower-order terms is reasonable when performing an asymptotic analysis. The higher-order terms soon swamp the lower-order terms in their contribution to the total cost as (n) becomes larger. Thus, if 𝐓(n)=3n4+5n2\mathbf{T}(n) = 3 n^4 + 5 n^2, then 𝐓(n)\mathbf{T}(n) is in O(n4)O(n^4). The n2n^2 term contributes relatively little to the total cost for large nn.

From now on, we will use these simplifying rules when discussing the cost for a program or algorithm.

3.5.3 Tight Bounds

The definition for big-Oh allows us to greatly overestimate the cost for an algorithm. But sometimes we know a tight bound – that is, a bound that truly reflects the cost of the algorithm or program with a constant factor. In that case, we can express this more accurate state of our knowledge using the Θ\Theta symbol instead of using big-Oh.

While some textbooks and programmers will casually say that an algorithm is “order of” or “big-Oh” of some cost function, it is generally better to use Θ\Theta notation rather than big-Oh notation whenever we have sufficient knowledge about an algorithm to be sure that we know the cost to within a constant factor. OpenDSA modules use Θ\Theta notation in preference to big-Oh notation whenever our state of knowledge makes that possible.

3.5.4 Summary

3.5.5 Practice questions: Asymptotic analysis

Answer TRUE or FALSE.

The Sequential Search algorithm is in O(n2)O(n^2).

  • Recall that O(n2)O(n^2) means that the program has no inputs that cost more than this.
  • A proposed upper bound can be much higher than the actual cost of the program, and still be correct.

For sequential search, the best case occurs when:

  • It is a serious misunderstanding of algorithm analysis to think that best case is related to input size. The array being small has nothing to do with what is the best case input (of a given size).
  • Implementation of an algorithm (as a program) has nothing to do with the algorithm’s best case.
  • Since sequential search can be used on an unsorted array, the value for the search key (large or small) might have nothing to do with where the record with that key will be in the array.

What is the smallest integer kk such that n\sqrt n is in O(nk)O(n^k)?

  • n=n0.5\sqrt n = n^{0.5}
  • What is the smallest integer greater than 0.5?

What is the smallest integer kk such that nlognn \log n is in O(nk)O(n^k)?

  • nlognn \log n is bigger than n1n^{1}.
  • So, that means 1 is too small.
  • But, nlognn \log n is less than n2n^2.
  • Actually nlognn \log n is less than n1+an^{1+a} even for tiny positive values of aa.

Which of these is the best upper bound for a growth rate of 5n+35n + 3?

  • The simplifying rules tell us that we can drop constants and lower order terms from a polynomial that defines the growth rate.

Which of these is the best upper bound for a growth rate of 5n+35n + 3?

  • The simplifying rules tell us that we can drop constants and lower order terms from a polynomial that defines the growth rate.
  • If you don’t see a tight upper bound, pick the smallest bound that is larger.