Complexity Analysis
This section provides a general overview of algorithmic analysis in programming terms. We present the motivating question, its answer, as well as illustrations of the answer applied. The materials that follow assume a basic understanding of functions, limits, sequences, and series (i.e., comfort with ideas from a basic calculus and discrete mathematics course). For a thorough mathematical justification of algorithmic analysis,
The most pertinent concern for analyzing algorithms is efficiency. One measure of efficiency is the amount of time an algorithm takes to execute. That is, how long it takes a given algorithm to solve a particular problem. To do so, however, we need some agreed-upon method for quantifying "speed." We can't rely on timing from start to finish if we want to compare algorithms across different machines, since every machine is different. Solving some differential equation on a small mobile phone will be slower than running the same operation on a super computer. Although it may be useful to know how fast the search runs on just one device model, it's not the principal concern in the analysis of algorithms. We want generality.
We often also want to know the amount of space an algorithm takes to execute. For example, one algorithm might be faster than algorithm but requires a significant amount of memory. Determining space complexity can serve as tie breakers in choosing one algorithm over another. Perhaps algorithms and take the same amount of time, but consumes less memory.
So how do we determine the amount of time or space an algorithm takes? One way is to count the number of basic steps performed as a function of where is the input size. These basic steps are listed below:
syntax | |
---|---|
declaration | |
assignment | |
comparisons | |
addition | |
subtraction | |
multiplication | |
real division | |
integer division | |
remainder | |
bitwise-not | |
bitwise-and | |
bitwise-or | |
bitwise-xor | |
bitwise-left-shift | |
bitwise-right-shift | |
goto | |
return |
example. Consider the summation algorithm below, which computes
- for (, , )
- return
Counting the number of operations:
operation | count | remark |
---|---|---|
Performed exactly once. | ||
Performed exactly once. | ||
Comparison: performed after each iterate. It's true times and false once. | ||
Addition: performed only if the guard returns true. | ||
Assignment operation: performed only if the guared returns true. | ||
Increment: Performed only if the guard returns true. | ||
Performed exactly once. |
Summing the number of executions for each operation, denoted we get:
The final expression gives us the algorithm's runtime function as a closed form expression: This is still just an estimate. If we wanted to be even more accurate, we would count the number of JUMP
instructions executed by loop. And if we wanted to be even more accurate, we would count the number of calls to RAM the CPU made to retrieve data not found in its registers. And if we wanted to be even more accurate, we would count the number of steps the CPU took to check its processor registers, the number of steps needed to run through the cache hierarchy, and so on. The point is, counting steps will always be an estimation at best. This doesn't mean that step counting is a useless exercise. In fact, we'll often use step counting to build a base case or to build some intuition. Our final description, however, will be a complexity function.
Complexity Functions
To understand complexity analysis notations, we must understand how functions grow. In the preceding examples, we stated no requirements about the functions we used to model efficiency. At this point, we've seen enough to handle formality.
complexity function. Let be a function from to with Then is a complexity function if, and only if, the following axioms are true: (1) There exists a number such that, if then (2) There exists a number such that, if then
Complexity analysis — for our purposes — is limited to the class of functions above. We do so for several reasons. First, resources like time and memory are easiest to interpret in terms of positive numbers. Second, we could make things even easier by just limiting ourselves to the natural numbers, but then we'd exclude swathe of useful functions — and even the sinusoids So, we restrict complexity analysis to real-valued functions.
Requirement (1) in the definition ensures that we won't run into functions that never get defined on the reals when given positive arguments. For example, Requirement (2) is similar in spirit; we don't want to work with functions that never become positive. It's perfectly fine for the function to return negative values (e.g., ), but it's not ok for the function to always return negative values (e.g., ).
Big O
big o. Given the complexity functions and if there exists a constant and a threshhold such that, for all both and are defined with then there exists a class of functions called big O of denoted with
This definition has a few moving parts. But before we examine those parts in detail, let's get a big picture view first. The letter comes from the German word ordnung, which means order of approximation. This gives us a hint as to what 's purpose is.
Next, it's helpful to recall what a function is: It's a set of tuples. Given that we're working with complexity functions, we're looking at sets of pairs — objects of the form where and are real numbers. Say we had two complexity functions, and Both of these functions have the same domain and the same codomain, and again, both are sets of pairs. Suppose contains a pair and contains a pair We're using instead of and instead of to make things clearer. Because and result from the same argument and both and are real numbers, we can compare them. And if we did, the trichotomy law tells that there are three possible outcomes, only one of which is true:
Case 1 | Case 2 | Case 3 |
---|---|---|
Now, what big O says is this: If we ever see that case 1 is true and stays true for all the s after or if we ever see that case 2 is true and stays true for all the s after then we can use as an approximation of Why? Because contains all of 's values eventually.
So why exactly do we write Because we want to emphasize that is an approximation. The notation represents a collection of functions — called a class of functions — whose s are, at some point, always less than or equal to 's s. Writing is akin to hedging our bets — we're not saying that is we're saying that is just one of the many functions covered by That second half is why describe this as hedging — there may actually be another function that's a much better approximation. That is, and To illustrate, consider the following function graphs:
example. Let and Then because if then
example. Function definitions do not always determine growth rates. For example, given and if and then Or, more explicitly, thus, we have when and
Having seen some examples, it's helpful to put the complexity functions in perspective. We do so with the table below. The times correspond to the approximate amount of time needed to completely execute an algorithm of order
instant | instant | instant | ||
instant | instant | instant | ||
instant | instant | instant | ||