The overall goal of this chapter is to deal with a seemingly simple question: Given an algorithm, how good is it? I say "seemingly" simple because unless we define what we mean by "good", we cannot answer the question. Do we mean how elegant it is? How easy it is to understand? How easy it is to update if/when necessary? Whether or not it can be generalized?
Although all of these may be important questions, in algorithm analysis we are usually more interested in the following two questions: How long does the algorithm take to run, and how much space (memory) does the algorithm require. In fact, we follow the tradition of most books and focus most of our discussion on running time. This is usually reasonable since the amount of memory used by most algorithms is not large enough to matter. There are times, however, when analyzing the space required by an algorithm is important. For instance, when the data is really large (e.g. the graph that represents friendships on Facebook) or when you are implementing a space-time-tradeoff algorithm.
This page introduces the basic ideas used in algorithm analysis: why raw running times can be misleading, how input size affects running time, how best-case, worst-case, and average-case analysis differ, and how to recognize common time complexities.
Since this page focuses mostly on running time, our next task is to decide what "running time" should mean. At first, it might seem enough to time a program with a stopwatch, but that measurement depends on the computer, the programming language, the compiler, the operating system, and even what else the machine is doing. To compare algorithms rather than particular program runs, we need a more stable way to measure time.
So what should we count? There are several reasonable possibilities:
Question 1
Why aren't wall-clock time and CPU time the same?
Modern computers use multitasking to perform several tasks (seemingly) at the same time. Therefore, if an algorithm takes 1 minute of real time (wall-clock time), it might be that 58 seconds of that time was spent running the algorithm, but it could also be the case that only 30 seconds of that time were spent on that algorithm, and the other 30 seconds spent on other processes. In this case, the CPU time would be 30 seconds, but the wall-clock time 60 seconds.
Further complicating matters is increasing availability of machines with multiple processors. If an algorithm runs on 4 processors rather than one, it might take 1/4th the time in terms of wall-clock time, but it will probably take the same amount of CPU time (or close to it).
Because the running time of an algorithm is greatly affected by the characteristics of the computer system (e.g. processor speed, number of processors, amount of memory, file-system type, etc.), the running time does not necessarily provide a comparable measure, regardless of whether you use CPU time or wall-clock time. The next question asks you to think about why.
Question 2
Sue and Stu were competing to write the fastest algorithm to solve a problem. After a week, Sue informs Stu that her program took 1 hour to run. Stu declared himself victorious since his program took only 3 minutes. But the real question is this: Whose algorithm was more efficient? Can we be certain Stu's algorithm was better than Sue's? Explain. (Hint: Make sure you don't jump to any conclusion too quickly. Think about all of the possibilities.)
We cannot be certain whose algorithm is better with the given information. Maybe Sue used a TRS-80 Model IV from the 1980s to run her program and Stu used the fastest supercomputer in the words (as of June 2026, LineShine). In this case, it is possible that if Sue ran her program on LineShine it would have only taken 2 minutes, making her the real winner.
The answer to the previous question should make it clear that you cannot compare the running times of algorithms if they were run on different machines. Even if two algorithms are run on the same computer, the wall-clock times may not be comparable.
Question 3
Why isn't the wall-clock time of two algorithms that are run on the same computer always a reliable indicator of their relative performances?
As has already been mentioned, other processes on the machine can have a significant influence on the wall-clock time. For instance, if I run two CPU-intensive programs at once, the wall-clock time of each might be about twice what it would be if I ran them one at a time. If they are run on a machine with multiple cores the wall-clock time might be closer to the CPU-time. But other processes that are running can still throw off the numbers.
In fact, if you run the same algorithm on the same machine multiple times, it will not always take the same amount of time. Sometimes the differences between trial runs can be significant.
Question 4
If two algorithms are run on the same machine, can we reliably compare the CPU-times?
For the most part, yes. This is especially true if the running times of the algorithms are not too close to each other (in other words, if one of the algorithms is significantly faster than the other). However, the number of other processes running on the machine can have an influence on CPU-time. For instance, if there are more processes running, there are more context switches, and depending on how the CPU-time is counted, these context switches can influence the runtime. So although comparing the CPU-time of two algorithms that are run on the same computer gives a pretty good indication of which is better, it is still not perfect.
So the CPU-time turns out to be a pretty good measure of algorithm performance. Unfortunately, it does not really allow one to compare two algorithms. It only allows us to compare specific implementations of the algorithms. It also requires us to implement the algorithm in an actual programming language before we even know how good the algorithm is (that is, before we know if we should even spend the time to implement it).
But we can analyze and compare algorithms before they are implemented if we use the number of instructions as our measure of performance. There is still a problem with this measure. What is meant by an "instruction"? When you write a program in a language such as Java or C++, it is not executed exactly as you wrote it. It is compiled into some sort of machine language. The process of compiling does not generally involve a one-to-one mapping of instructions, so counting Java instructions versus C++ instructions wouldn't necessarily be fair. On the other hand, we certainly do not want to look at the machine code in order to count instructions—machine code is ugly. Further, when analyzing an algorithm, should we even take into account the exact implementation in a particular language, or should we analyze the algorithm apart from implementation?
O.K., that's enough of the complications. Let's get to the bottom line. When analyzing algorithms, we generally want to ignore what sort of machine it will run on and what language it will be implemented in. We also generally do not want to know exactly how many instructions it will take. Instead, we want to know the rate of growth of the number of instructions. This is sometimes called the asymptotic running time of an algorithm. In other words, as the size of the input increases, how does that affect the number of instructions executed? We will typically use Asymptotic Notation to specify the running time of an algorithm. We will call this the time complexity (or often just complexity) of the algorithm.
Algorithm analysis involves determining the size of the input, \(n\), and then finding a function based on \(n\) that tells us how long the algorithm will take if the input is of size \(n\). By "how long" we of course mean how many operations.
The examples in this section use algorithms that should be familiar from an earlier programming or data structures course. The goal is not to introduce new algorithms, but to practice analyzing simple ones carefully.
We will start by deciding what counts as the input size, then look at how to count operations, how best-case, worst-case, and average-case analysis differ, how to choose which operation to count, and how to analyze simple loops.
Given an algorithm, the size of the input is the amount of data the algorithm receives. In this course, we usually describe input size using the most natural counting measure for the problem. For an array, this is usually the number of elements. For a matrix, it is usually the number of entries. For a graph, it is usually the number of vertices, the number of edges, or both. When the input is a single number, the size can be measured in terms of the number of digits or bits needed to write it down, but most examples in this section use arrays, matrices, or graphs.
Example 1: Sequential Search
Given an array of \(n\) elements, often one needs to determine if a given number \(val\) is in the array. One way to do this is with the sequential search algorithm that simply looks through all of the elements in the array until it finds it or reaches the end. The most common version of this algorithm returns the index of the element, or \(-1\) if the element is not in the array. Here is one implementation.
int sequentialSearch(int a[], int n, int val) {
for(int i=0; i<n; i++) {
if(a[i]==val) {
return i;
}
}
return -1;
}
What is the size of the input to this algorithm?
Solution
There are a few possible answers to this question. The input technically consists of an array of \(n\) elements, the number \(n\), and the value we are searching for. So we could consider the size of the input to be \(n+2\). However, we usually ignore a constant number of extra numeric parameters when choosing the input-size variable. So we will say the size of the input is \(n\).
In general, if an algorithm takes as input an array of size \(n\) and some constant number of other numeric parameters, we will consider the size of the input to be \(n\).
Example 2: Graph Input Size
Suppose an algorithm takes as input a graph with \(V\) vertices and \(E\) edges. What is the size of the input?
Solution
We usually describe the input size using both \(V\) and \(E\). The number of vertices alone does not always tell us how large the graph is, because two graphs with the same number of vertices can have very different numbers of edges. For example, many graph algorithms are analyzed in terms of \(V+E\), \(E\log V\), or \(V^2\), depending on the graph representation and the algorithm.
Exercise 1
Consider an algorithm that takes as input an \(n\) by \(m\) matrix, an integer \(v\), and a real number \(r\). What is the size of the input?
The answer is usually \(n\cdot m\), since this is how many entries are in the matrix. Sometimes we use two variables, \(n\) and \(m\), rather than multiplying them into a single expression. As suggested previously, we usually ignore the constant number of extra numeric parameters.
Once we have identified the input size, the next step is to count how much work the algorithm performs. At first, we can try to count individual operations such as assignments, comparisons, increments, array accesses, and return statements. This kind of exact count is useful for learning, but later we will usually count only the operations that determine the rate of growth.
Example 3
How many operations does sequentialSearch take on an array of size \(n\)?
int sequentialSearch(int a[], int n, int val) {
for(int i = 0; i < n; i++) {
if(a[i] == val) {
return i;
}
}
return -1;
}
Solution
We could try to count every operation. The assignment i = 0 happens
once. Each time the loop condition is checked, the algorithm compares i
with n. Each time through the body of the loop, the algorithm accesses
a[i], compares a[i] with val, and later
increments i. If the value is found, the algorithm returns i.
If the value is not found, the loop condition is checked one extra time before the
algorithm returns \(-1\).
Already this is a little annoying. For example, should we count a[i] == val
as one operation, or as an array access plus a comparison? Should we count the
increment i++ as one operation, or as an addition plus an assignment?
Should returning a value count the same as an assignment? Different reasonable
counting rules can give different exact formulas.
To illustrate the issue, suppose \(val\) is found at index \(k\), where indexing
starts at \(0\). Then the loop body executes \(k+1\) times. The loop condition
i < n is also checked \(k+1\) times, the comparison
a[i] == val is made \(k+1\) times, and the increment i++
is executed \(k\) times before the successful return. Depending on the exact rules
we use, we might get a formula such as
\[
c_1 + c_2(k+1) + c_3(k+1) + c_4k + c_5,
\]
where the constants \(c_1,c_2,c_3,c_4,c_5\) represent the costs of different
low-level operations.
If \(val\) is not in the array, then the loop body executes \(n\) times, the loop
condition is checked \(n+1\) times, the comparison a[i] == val is made
\(n\) times, and the final return -1 executes once. Again, the exact
formula depends on what we decide to count, but it will have the form
\[
d_1n + d_2
\]
for some constants \(d_1\) and \(d_2\).
The important point is that the exact constants are not what we usually care about. If the value is found at index \(k\), the running time grows linearly with \(k+1\). If the value is not found, the running time grows linearly with \(n\). Thus, exact counting suggests that a successful search ending at index \(k\) takes \(\Theta(k+1)\) time, while an unsuccessful search takes \(\Theta(n)\) time.
In practice, we usually simplify the analysis by choosing one operation that captures
the main work. For sequential search, the natural choice is the comparison
a[i] == val. This comparison happens once for each array element that
is examined. If the target is at index \(k\), the comparison executes \(k+1\) times,
so a successful search ending at index \(k\) takes \(\Theta(k+1)\) time. If the target
is not present, the comparison executes \(n\) times, so an unsuccessful search takes
\(\Theta(n)\) time. Therefore, the best case is \(\Theta(1)\), while the worst case is
\(\Theta(n)\). We will use this same idea throughout the rest of the section: choose
an operation that represents the main work, then count how many times it executes.
The next example uses this simplified approach directly.
Example 4
Approximately how many operations does the following code perform? What is its asymptotic running time?
int total = 0;
for (int i = 0; i < n; i++) {
total = total + a[i];
}
Solution
The assignment total = 0 happens once. The loop executes \(n\)
times. During each iteration, the algorithm checks the loop condition, accesses
a[i], adds it to total, stores the result back in
total, and increments i. Each of these takes constant
time.
So the exact operation count depends on the details of what we count, but it has the form \[ c_1n + c_2 \] for constants \(c_1\) and \(c_2\). Therefore, the asymptotic running time is \(\Theta(n)\).
Equivalently, we could choose the assignment
total = total + a[i] as the representative operation. It executes
exactly \(n\) times, and the remaining work per iteration is also constant. This
again gives \(\Theta(n)\).
The previous examples bring up another important question: what if the same algorithm does different amounts of work on different inputs of the same size? For instance, sequential search might find the target immediately, near the end, or not at all. To describe these different possibilities, we often analyze three cases.
In each case, the input size \(n\) is fixed. We are not comparing small inputs to large inputs. We are comparing different inputs of the same size.
When performing a best-case analysis, we are trying to determine the smallest possible number of operations an algorithm will take on an input of size \(n\). This is often the least useful type of analysis. If someone tells you a repair will take "only an hour," but it actually takes three hours, they may have told you something closer to the best case than the case you actually experienced.
The phrase for an input of size \(n\) is important. Otherwise every algorithm would have a constant-time best case by choosing the smallest possible input, often of size \(0\) or \(1\). That sort of analysis is not very informative.
Remark
When you are asked to do a best-case analysis of an algorithm, remember that it is implied that what is being asked is the best-case analysis for an input of size \(n\). This applies to average-case and worst-case analysis as well, but it is easier to make this mistake when doing a best-case analysis.
Worst-case analysis considers the largest number of operations that can execute on an input of size \(n\). This is probably the most common and often the most useful type of analysis. When we prove a worst-case bound, we are saying that no input of size \(n\) can make the algorithm take longer than that bound.
The average case is more complicated, both to define and to compute. The first problem is deciding what "average" means for a particular problem. For instance, what does an "average" array look like? The second problem is that even with a good definition, computing the average-case complexity is usually harder than computing the best or worst case. Average-case analysis also has to be interpreted carefully: an average tells us what happens overall, not what must happen on every input.
I sometimes use the term expected running time instead of average-case running time. This is almost synonymous with average case, but when I use this term I am being less formal. Think of it as describing how long an algorithm will usually take. It will often coincide with either the average-case or worst-case complexity.
Example 5
Continuing the sequentialSearch example, what are the best-case,
worst-case, and average-case complexities?
Solution
The best case occurs when the target value is the first element in the array. In that case, the algorithm performs one comparison and returns immediately, so the best-case complexity is \(\Theta(1)\).
The worst case occurs when the target value is not in the array, or when it appears only at the last position. In either case, the algorithm must examine all \(n\) array elements, so the worst-case complexity is \(\Theta(n)\).
For a rough expected-case estimate, suppose the target is usually found somewhere near the middle of the array. Then the algorithm examines about \(n/2\) elements. Since \(n/2\) grows at the same rate as \(n\), this suggests expected running time \(\Theta(n)\). This is useful intuition, but it is not yet a full average-case analysis because we have not specified a probability model for the inputs.
Notice that the average-case and worst-case complexities in the previous example are the same. This makes sense: the average case is about half as much work as the worst case, but half of \(n\) is still \(\Theta(n)\). Also notice why the best case is \(\Theta(1)\). It is not because the best-case input has size \(1\). It is because, for any input size \(n\), the target might appear in the first position.
Example 6
Let's determine the average-case complexity of sequentialSearch.
We will count comparisons of the form a[i] == val.
Solution
If we assume that the element is equally likely to be anywhere in the array, then there is a \(1/n\) chance that it will be in any given spot. If it is in the first spot, the comparison executes once. If it is in the second spot, it executes twice. In general, it takes \(k\) comparisons if it is in the \(k\)th spot. Since each possibility has a \(1/n\) chance, the average expected search time is \[ \sum_{k=1}^{n}\frac{k}{n} = \frac{1}{n}\sum_{k=1}^{n}k = \frac{1}{n}\frac{n(n+1)}{2} = \frac{n+1}{2} = \Theta(n). \]
Our analysis simplified things a bit: it did not take into account the possibility that the element was not in the array. To do so, let's assume the element searched for is equally likely to be anywhere in the array or not in the array. That is, there is now a \(1/(n+1)\) chance that it will be in any of the \(n\) spots in the array and a \(1/(n+1)\) chance that it is not in the array. We divide by \(n+1\) because there are now \(n+1\) possibilities, each equally likely. If it is not in the array, the number of comparisons is \(n\). In this case the expected time is \[ \sum_{k=1}^{n}\left(\frac{k}{n+1}\right)+\frac{n}{n+1} = \frac{1}{n+1}\left(\sum_{k=1}^{n}k+n\right) = \frac{1}{n+1}\left(\frac{n(n+1)}{2}+n\right) = \frac{n^2+3n}{2(n+1)} = \Theta(n). \]
The previous calculation is more complete than simply saying the target is usually near the middle, but it still depends on an assumption we made. Is it really valid to assume that there is a \(1/(n+1)\) chance that the element searched for is not in the array? If we are searching for a lot of values in a small array, perhaps most of the values we search for are not in the array. Maybe it is more realistic to assume there is a \(50\%\) chance it is in the array and a \(50\%\) chance that it is not in the array. We could propose several other reasonable assumptions, too.
As stated before, it can be difficult to define "average." In this case, the exact assumption does not matter much asymptotically: under many reasonable assumptions, the average-case complexity still comes out as \(\Theta(n)\). For more complicated algorithms, however, the choice of probability model can matter much more.
This example demonstrates why average-case analysis is usually more difficult than best-case or worst-case analysis. Even for a simple algorithm, we had to decide which inputs were possible and how likely each one was. As algorithms become more complex, defining and computing the average case can become much harder, so we will sometimes skip it or discuss it informally.
Example 7
Consider an algorithm that adds all \(n\) values in an array.
int sumArray(int a[], int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum = sum + a[i];
}
return sum;
}
What are the best-case, worst-case, and average-case complexities?
Solution
The loop executes exactly \(n\) times no matter what values are stored in the array. The values affect the sum, but they do not affect how many iterations the loop performs. Therefore, the best-case, worst-case, and average-case complexities are all \(\Theta(n)\).
This is different from sequential search, where the number of iterations depends on where the target value appears or whether it appears at all.
The previous examples suggest a useful shortcut: instead of counting every low-level operation, we can choose one operation that represents the main work of the algorithm and count how many times it executes. This shortcut is valid only when the chosen operation really does capture the growth rate of the whole algorithm.
A good representative operation is usually one that occurs inside the most frequently executed part of the algorithm, such as the body of a loop. We must also check that the other operations being ignored take constant time or occur no more often than the operation we are counting.
Example 8
What is the complexity of max(x,y)? Justify your answer.
int max(int x, int y) {
if (x >= y) {
return x;
} else {
return y;
}
}
Solution
This method performs one comparison, then returns one of the two values. There is no loop or recursion, and the number of operations does not depend on the sizes or values of \(x\) and \(y\). Therefore, the best-case, worst-case, and average-case complexities are all \(\Theta(1)\).
In this case there is not much need to choose a representative operation. Any reasonable count gives a constant number of operations.
Example 9
Analyze the following algorithm that finds the maximum value in an array. Start by deciding which operation should be counted.
int maximum(int a[], int n) {
int currentMax = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
currentMax = max(currentMax, a[i]);
}
return currentMax;
}
Solution
The loop executes exactly \(n\) times. One reasonable representative operation
is the call to max(currentMax, a[i]), since it happens once per
loop iteration. Another reasonable choice is the assignment to
currentMax, which also happens once per iteration.
To make this analysis valid, we must know that max takes constant
time. The previous example showed that it does: max performs one
comparison and one return. Therefore each loop iteration takes constant time,
and the loop executes \(n\) times.
Thus, the best-case, worst-case, and average-case complexities are all \(\Theta(n)\). They are the same because the loop always examines every array element, no matter what values are stored in the array.
The previous example also shows why method calls need to be treated carefully. We were
allowed to treat max(currentMax, a[i]) as constant time only because we knew
what max did. In general, a method call might take constant time, linear
time, logarithmic time, or something else entirely.
Example 10
Suppose the following code is used to add up the values stored in a list-like data structure. What extra information do we need before we can analyze it correctly?
int total = 0;
for (int i = 0; i < n; i++) {
total = total + list.get(i);
}
Solution
We need to know how long list.get(i) takes. If list is
backed by an array, then get(i) might take \(\Theta(1)\) time, so
the whole loop would take \(\Theta(n)\) time.
If list is implemented as a linked list and get(i)
starts at the front each time, then get(i) might take \(\Theta(i)\)
time. In that case the loop would take
\[
\Theta(1+2+\cdots+n)=\Theta(n^2).
\]
The code looks like a simple loop, but the running time depends on the cost of the operation inside the loop. We cannot assume that every method call takes constant time.
Sometimes a single representative operation is enough. Other times, it is better to count several kinds of operations or analyze different parts of the algorithm separately. The key is to make sure the operations being counted determine the same rate of growth as the whole algorithm.
Let's see one more example.
Example 11
Consider the following algorithm, which checks whether an array is sorted in nondecreasing order.
boolean isSorted(int a[], int n) {
for (int i = 0; i < n - 1; i++) {
if (a[i] > a[i + 1]) {
return false;
}
}
return true;
}
What are the best-case, worst-case, and average-case complexities?
Solution
The natural operation to count is the comparison a[i] > a[i + 1].
The best case occurs when the first two elements are out of order. Then the
algorithm performs one comparison and returns false, so the best-case
complexity is \(\Theta(1)\).
The worst case occurs when the array is sorted, or when the first out-of-order pair occurs at the very end. Then the algorithm must check all \(n-1\) adjacent pairs, so the worst-case complexity is \(\Theta(n)\).
The average case depends on what kinds of arrays we expect. If out-of-order pairs tend to appear early, then the algorithm usually returns quickly. If arrays are often sorted or nearly sorted, then it may usually examine most of the array. Without a probability model for the inputs, we should not claim a precise average-case complexity. In many basic analyses, we would report the best case as \(\Theta(1)\) and the worst case as \(\Theta(n)\), and discuss the average case only if the input distribution is specified.
Remark
When you are asked for the complexity of an algorithm, you should do the following three things:
Give the best, average, and worst-case complexities unless otherwise specified. Sometimes the average case is quite complicated; in those cases, it may be skipped, approximated informally, or replaced with a clearly stated expected-case discussion.
Give answers in the form \(\Theta(f(n))\) for some function \(f(n)\), or \(O(f(n))\) if a tight bound is not possible. The function \(f(n)\) should be as simple as possible. For instance, instead of \(\Theta(3n^2+2n+89)\), use \(\Theta(n^2)\).
Clearly justify your answers by explaining what operation or operations you counted, how many times they execute as a function of the input size, and why that count represents the running time of the whole algorithm. Also mention any assumptions you are making, such as treating a method call or array access as constant time.
Many running-time analyses come down to understanding how often loops execute. In this section, we focus on simple loops whose iteration counts are easy to determine directly: consecutive loops, nested loops with fixed bounds, and nested loops whose bounds are still straightforward to multiply. More complicated loops, especially loops whose counts require summations, will be handled later.
Example 12
Give the complexity of the following code.
int q = 0;
for (int i = 1; i <= n; i++) {
q = q + i * i;
}
for (int j = 1; j <= n; j++) {
q = q * j;
}
Solution
This algorithm has two independent loops, each of which do slightly different things. Thus, we cannot
pick a single operation to count. Instead we will pick the assignment statements that involve \(q\).
That is, we will use both q=q+i*i and q=q*j.
The first assignment executes \(n\) times since the first loop executes for every value of \(i\) from \(1\) to \(n\). The second loop also executes its assignment \(n\) times for the same reason. Since the loops happen one after another, we add the number of operations, so the total is \(n+n=2n\) assignment statements. Since there are no conditional statements, this is the best, worst, and average-case number of assignment statements. Thus, the complexity for all three cases is \(\Theta(n)\).
Example 13
Give the complexity of the following code.
double v = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
v = v + A[i] * A[j];
}
}
Solution
Clearly the assignment (v=v + A[i] * A[j]) occurs the most often. The inner loop
always executes \(n\) times, each time doing one assignment. The outer loop
executes \(n\) times, and each time it executes, it executes the inner loop. Therefore the total
time is \(n\cdot n=\Theta(n^2)\). This is the best, worst, and average case complexity since nothing
about the input can change what the algorithm does.
Here is another way to think about it. The inner loop executes the assignment statement \(n\) times every time it executes. The first time through the outer loop, the whole inner loop executes and calls the assignment \(n\) times. The second time through the outer loop, the whole inner loop executes and calls the assignment \(n\) times. This happens all the way until the \(n\)th time through the outer loop during which the whole inner loop executes and calls the assignment \(n\) times. Thus, the total number of times the assignment is called is \(n+n+\cdots+n\) times (where there are \(n\) terms in the sum), which is just \(n\cdot n\). Thus the complexity is \(\Theta(n^2)\).
Remark
Always analyze loops from the inside out. The more practice you get, the more it will be obvious that this is the only way that will consistently work. Although you probably would have gotten the right answer in in the previous example by analyzing the outer loop first, it is not always that easy. We will see examples later where analyzing the outer loop first will lead to a wrong answer and/or become very confusing.
The previous two examples are easy to confuse. In Example 12, one loop happens after another, so the work is \(n+n\). In Example 13, one loop happens inside the other, so the work is \(n\cdot n\).
Here is an analogy. If you climb a flight of \(10\) stairs followed by another flight of \(10\) stairs, you climbed a total of \(10+10=20\) stairs. Now assume you go into a building that has \(10\) floors. There are \(10\) steps between floors (so it takes \(10\) steps to get from floor 1 to 2, etc.) If you climb to the top of the building, how many stairs did you climb? It is \(10+10+\cdots+10\) (where there are \(10\) terms in the sum), which is \(10^2=100\). How does this relate to the previous examples? Simple. In the first case, you executed:
for(stair 1 through 10)
climb stair
for(stair 1 through 10)
climb stair
and in the second case you executed:
for(floors 1 through 10)
for(stair 1 through 10)
climb stair
Hopefully you can see the similarity in the analysis in this situation and the previous examples.
Remark
Consecutive loops usually lead to adding running times. Nested loops often lead to multiplying running times. However, the exact result depends on the loop bounds, so do not assume every nested loop is quadratic.
Now it is your turn to try to analyze some algorithms:
Exercise 3
What is the worst-case complexity of the following algorithm?
int k = 50;
for (int i = 0; i < n; i++) {
for (int j = 0; j < k; j++) {
a[i][j] = b[i][j] * x;
}
}
The inner loop executes \(k=50\) times. Since \(50\) is a constant, the inner loop does a constant amount of work each time the outer loop runs. The outer loop executes \(n\) times, so the total running time is \[ \Theta(n\cdot 50)=\Theta(n). \] This is a nested loop, but it is not quadratic because the inner loop does not grow with \(n\).
Exercise 4
What is the worst-case complexity of the following algorithm? (This one is a little more tricky! Read the code carefully.)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n * n; j++) {
a[i][j] = b[i][j] * x;
}
}
We will count how many times the assignment statement, a[i][j] = b[i][j] * x
executes since it is the operation that occurs most often.
The inner loop executes \(n^2\) times for each execution of the outer loop. The
outer loop executes \(n\) times. Therefore the total number of executions of the
assignment statement is
\[
n\cdot n^2=n^3.
\]
Since the assignment takes constant time, the worst-case complexity is
\(\Theta(n^3)\).
This is an example of a nested loop that is worse than quadratic. The lesson is to read the loop bounds carefully rather than guessing from the number of loops.
Once we analyze an algorithm, the final answer often matches one of a small number of common growth rates. The table below lists several common complexity classes in increasing order of growth of the function \(f(n)\). Since \(f(n)\) represents running time, larger growth rates generally describe algorithms that scale worse as \(n\) increases.
| Complexity | Name | Common source | Example pattern |
|---|---|---|---|
| \(\Theta(1)\) | constant | A fixed amount of work independent of \(n\) | Accessing one array entry; swapping two values |
| \(\Theta(\log n)\) | logarithmic | Repeatedly discarding a constant fraction of the input | Binary search |
| \(\Theta(n)\) | linear | Examining each input item a constant number of times | Sequential search in the worst case; summing an array |
| \(\Theta(n \log n)\) | linearithmic | Many efficient divide-and-conquer algorithms | Merge sort; average-case quicksort |
| \(\Theta(n^2)\) | quadratic | Comparing or combining many pairs of items; examining every element of an \(n\times n\) matrix | Many simple sorting algorithms; two \(n\)-by-\(n\) nested loops |
| \(\Theta(n^k)\) | polynomial | A fixed number of nested loops or stages that each depend on \(n\) | Standard matrix multiplication is \(\Theta(n^3)\) |
| \(\Theta(k^n)\) | exponential | Trying all subsets, strings, assignments, or choices | Brute force over all \(2^n\) subsets |
These names are useful shortcuts, but they are not substitutes for analysis. A loop does not automatically imply linear time, and nested loops do not automatically imply quadratic time. The bound depends on how many times the relevant operations execute.
Let's see an example of an algorithm with each of the given complexities.
Example 14: Constant Time
The following code returns the fifth element of an array.
int theFifthElement(int a[], int n) {
return a[4];
}
Array indexing and returning a value both take constant time. The number of operations does not depend on \(n\), so the complexity is \(\Theta(1)\).
Example 15: Logarithmic Time
Binary search is a typical logarithmic-time algorithm. Each iteration compares the target value to the middle element of the current search range and then discards about half of the remaining elements.
Since the input size is repeatedly divided by \(2\), the number of iterations is proportional to the number of times \(n\) can be divided by \(2\) before reaching \(1\). Therefore, binary search has worst-case complexity \(\Theta(\log n)\). If you aren't buying it that quickly, do not fret. We will give a more detailed analysis of binary search shortly.
Example 16: Linear Time
The following code sums all \(n\) entries in an array.
int sumArray(int a[], int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum = sum + a[i];
}
return sum;
}
The loop executes exactly \(n\) times, and each iteration takes constant time. Therefore, the complexity is \(\Theta(n)\).
Example 17: \(n \log n\) Time
Many efficient sorting algorithms, such as merge sort, have running time \(\Theta(n \log n)\). Very roughly, merge sort has \(\log n\) levels of recursive splitting, and each level performs \(\Theta(n)\) total work to merge the pieces.
Multiplying the work per level by the number of levels gives \[ \Theta(n)\cdot \Theta(\log n)=\Theta(n\log n). \] We will analyze algorithms like this more carefully when we study recurrences.
Example 18: Quadratic Time
The following code computes all pairwise sums from an array.
int[][] pairSums(int a[], int n) {
int sums[][] = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
sums[i][j] = a[i] + a[j];
}
}
return sums;
}
The inner loop executes \(n\) times for each of the \(n\) iterations of the outer loop. Thus, the assignment executes \(n\cdot n=n^2\) times, so the complexity is \(\Theta(n^2)\).
Example 19: Polynomial Time
Standard matrix multiplication is a common cubic-time algorithm.
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
C[i][j] = 0;
for (int k = 0; k < n; k++) {
C[i][j] = C[i][j] + A[i][k] * B[k][j];
}
}
}
There are three nested loops, each with \(n\) iterations. The innermost statement executes \(n\cdot n\cdot n=n^3\) times, so the complexity is \(\Theta(n^3)\). Since \(n^3\) is \(n^k\) for the constant \(k=3\), this is polynomial time.
Later we will see an algorithm for matrix multiplication with a more interesting complexity. It is called Strassen's Algorithm and it has a complexity of about \(\Theta(n^{2.8})\) (to be precise, its complexity is \(\Theta(n^{\log_2 7})\)).
Example 20: Exponential Time
Suppose an algorithm prints every subset of an \(n\)-element set. Each element is either included or not included in a subset, so there are \(2^n\) possible subsets.
Since the algorithm must output \(2^n\) subsets, its running time is at least proportional to \(2^n\). A straightforward brute-force version therefore has complexity \(\Theta(2^n)\), which is exponential.
Generally speaking, we consider algorithms whose complexity is no worse than polynomial to be efficient. This does not mean that every polynomial-time algorithm is practical. For example, an algorithm with complexity \(\Theta(n^{100})\) would be polynomial, but it would be useless for all but very small inputs. Still, polynomial time is an important dividing line because, for a fixed exponent, the running time grows much more slowly than exponential functions such as \(2^n\).
Exponential-time algorithms are usually considered impractical, especially when \(n\) can be even moderately large. There are exceptions, however. Sometimes the problem itself requires exponential work. For example, an algorithm that lists all subsets of an \(n\)-element set must take at least \(\Theta(2^n)\) time because there are \(2^n\) subsets to output. In that case, the exponential running time is caused by the size of the required output, not by a poor algorithm design.
Algorithm analysis describes how the work done by an algorithm grows as the input size grows. We usually do this by choosing representative operations, counting how often they execute, and simplifying the result using asymptotic notation.
Remember that a faster-growing complexity function describes a slower-scaling algorithm. For example, \(n^2\) grows faster than \(n\), so a \(\Theta(n^2)\) algorithm generally scales worse than a \(\Theta(n)\) algorithm.
The examples on this page used relatively simple loops and familiar operations. On the Algorithm Analysis Examples page, we will apply the same ideas to algorithms where the loop bounds, helper methods, or data structures make the analysis less direct.
a[i] == val a reasonable
operation to count?
a[i] == val happens once for each element examined. Counting that
comparison therefore captures the main work of the algorithm.
list.get(i) might take
constant time for an array-based list but linear time for a linked list. If the
method is inside a loop, that hidden cost can change the total complexity.
int countPositive(int a[], int n) {
int count = 0;
for (int i = 0; i < n; i++) {
if (a[i] > 0) {
count++;
}
}
return count;
}
int total = 0;
for (int i = 0; i < n; i++) {
total = total + list.get(i);
}
list.get(i) takes \(\Theta(1)\) time,
and then assuming list.get(i) takes \(\Theta(i)\) time. Explain why the
answers differ even though the loop structure did not change.
x++ executes and give the complexity. Be careful:
the answer is not determined just by the number of loops.
for (int i = 0; i < n; i++) {
for (int j = 0; j < 20; j++) {
x++;
}
}
for (int i = 0; i < 20; i++) {
for (int j = 0; j < n; j++) {
x++;
}
}
for (int i = 0; i < n; i++) {
x++;
}
for (int j = 0; j < n * n; j++) {
x++;
}
for (int row = 0; row < r; row++) {
for (int col = 0; col < c; col++) {
x++;
}
}
int countOccurrences(int a[], int n, int val) {
int count = 0;
for (int i = 0; i < n; i++) {
if (a[i] == val) {
count++;
}
}
return count;
}
val.boolean containsNegative(int a[], int n) {
for (int i = 0; i < n; i++) {
if (a[i] < 0) {
return true;
}
}
return false;
}
bag.contains(x) takes \(\Theta(1)\) time in one implementation
and \(\Theta(n)\) time in another implementation, where \(n\) is the number of items
in bag. Analyze the following code under both assumptions.
int countMatches(int a[], int m, Bag bag) {
int count = 0;
for (int i = 0; i < m; i++) {
if (bag.contains(a[i])) {
count++;
}
}
return count;
}
bag.contains(x) takes \(\Theta(1)\) time.bag.contains(x) takes \(\Theta(n)\) time.x++ executes and
give the tight asymptotic complexity.
for (int i = 0; i < n; i++) {
x++;
}
for (int j = 0; j < n * n; j++) {
x++;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < 5; j++) {
x++;
}
}
for (int row = 0; row < r; row++) {
for (int col = 0; col < c; col++) {
x++;
}
}
for (int i = 0; i < n * n; i++) {
for (int j = 0; j < 3; j++) {
x++;
}
}