Big-O Notation
Quick reference
| O(1) — constant | O(1) |
| O(log n) — logarithmic | O(log n) |
| O(n) — linear | O(n) |
| O(n log n) — linearithmic | O(n log n) |
| O(n²) — quadratic | O(n²) |
| O(2ⁿ) — exponential | O(2ⁿ) |
| O(n!) — factorial | O(n!) |
Two programs can solve the same problem. Both programs can be correct. But the two programs can be very different in practice. One program gives a result immediately. The other program stops your computer when the input becomes large. Big-O is the language for this difference. Big-O answers one question. When the input size N becomes larger, how does the quantity of work become larger? Big-O does not measure the time in milliseconds on your machine. That time changes with the CPU speed, the language, and the other programs that run. Big-O describes the shape of the growth. This shape is true on all machines, in all languages, and at all times.
This is why we remove the machine data and use N. A loop that touches each element one time does approximately N units of work. This is true if N is 10 or 10 million. We call this O(n). A loop inside another loop does approximately N multiplied by N units of work. We call this O(n²). We remove the exact constants. For example, we do not keep the difference between 2N operations and 5N operations. The constants do not change the shape of the growth. The shape shows if your program can process a large input.
Three cases are important. The best case is the most agreeable input. The best case is usually not useful for your plans. The worst case is the input that makes your algorithm do the most work. You must design for the worst case. The average case is the usual result for realistic inputs. The average case is useful when the worst case is rare and expensive to prevent. A hash map that manages collisions is an example. When people say that an algorithm is O(n) with no more data, they usually mean the worst case. You must also assume the worst case.
How to find the class
// The method: read the code. Count how the "work" increases with N. Give the class a name. // Ask one question for each loop or structure: "Does this range depend on N? How?" // - a fixed number of steps, for all sizes of N -> O(1) // - one pass over N items -> O(n) // - a loop inside a loop, each over N items -> O(n^2) // - a loop that divides the remaining work by two each step -> O(log n) // Multiply the nested or sequential costs. Then keep only the term that increases most quickly.
constant time — O(1)
function firstElement(arr):
return arr[0] // One lookup. The cost is the same for all sizes of arr.single loop — O(n)
function sum(arr):
total = 0
for x in arr: // This loop runs N times.
total += x // This line does constant work for each iteration.
return totalnested loop — O(n²)
function hasDuplicatePair(arr):
for i in range(len(arr)): // This loop runs N times.
for j in range(len(arr)): // This loop runs N times for each value of i.
if i != j and arr[i] == arr[j]:
return true
return falsehalving loop — O(log n)
function binarySearch(sortedArr, target):
lo, hi = 0, len(sortedArr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if sortedArr[mid] == target:
return mid
elif sortedArr[mid] < target:
lo = mid + 1
else:
hi = mid - 1 // Each iteration removes half of the remaining range.
return -1This list shows the classes in sequence, from the least expensive growth to the most expensive growth. The last column is the most important column to memorize. The last column tells you if an approach can process a realistic input size. This is useful during an interview or a code review.
| Operation | Time | Space | Why |
|---|---|---|---|
| O(1) — constant | O(1) | — | The cost is the same at n = 10 or n = 1,000,000. Examples are a hash lookup, an array index, and a fixed arithmetic formula. |
| O(log n) — logarithmic | O(log n) | — | At n = 1,000,000, log₂n is approximately 20. Division of the problem by two at each step finds the answer very quickly. Binary search is the usual example. |
| O(n) — linear | O(n) | — | The algorithm does one pass over the input. At n = 1,000,000, this is 1,000,000 operations. This is fast. It is usually the best result when you must look at each element. |
| O(n log n) — linearithmic | O(n log n) | — | At n = 1,000,000, this is approximately 20,000,000 operations. This is still fast. Good sorting algorithms use this class. Examples are merge sort and heap sort. |
| O(n²) — quadratic | O(n²) | — | At n = 1,000,000, this is one trillion operations. Do not use this class at this size. It is acceptable at n = 100. It is dangerous when the input size is not small. Every-pair comparisons are in this class. |
| O(2ⁿ) — exponential | O(2ⁿ) | — | At n = 30, this is already more than one billion. At n = 1,000,000, the number is too large to calculate. This class occurs in brute-force approaches that try each subset. |
| O(n!) — factorial | O(n!) | — | At n = 20, the number is larger than you can enumerate. This class occurs when you try each order or permutation. It is only possible for a very small N. |
Constants are not always unimportant
Big-O removes constants because the constants become unimportant when N becomes large. But sometimes N has a limit. For example, N is a maximum of 20 for a fixed-size board or a small configuration list. Then an O(n²) algorithm with small constants can be faster than an O(n log n) algorithm with large overhead for each step. Big-O describes the trend at a large scale. Big-O does not tell you which code is faster on your actual input.
A loop that becomes smaller is still O(n²)
A loop such as "for i in range(n): for j in range(i, n)" looks smaller at each pass. The inner loop runs n times, then n-1 times, then n-2 times. But the total is n + (n-1) + (n-2) + ... + 1, which is equal to n(n+1)/2. This is still a quadratic expression. When you remove the constants and the lower-order terms, n(n+1)/2 is O(n²), not O(n). Big-O measures the total work, not the quantity for each iteration.
Worst case compared to average case
A hash map lookup is O(1) on average. But sometimes each key goes to the same bucket. This can occur with a bad hash function or an adversarial input. Then the lookup becomes O(n) in the worst case, because you must go through a full bucket chain. If you say "O(1)" with no qualifier, you hide this risk. Know which case you claim.
A hidden O(n) inside a loop
The code "for i in range(n): result += arr[:i]" looks like one O(n) loop. But a slice or a concatenation of a string or a list is O(k) in the size that you build. Thus the real cost is 1 + 2 + ... + n, which is approximately O(n²), not O(n). Always ask if the line inside your loop is really constant time before you count the loop.
Sign in to mark problems done — progress syncs across devices.