Time complexity
The time complexity of a program tells you how fast its number of steps grows as the input gets bigger. Make the input 10 times longer, and one program makes the same number of steps, another 10 times more, and a third 100 times more. Knowing it, you can tell whether an algorithm is fast enough for the input sizes you expect, before you write any code.
Explanation
A teacher has 6 students and wants to know how many of them were born in March. She asks each student one by one, so she needs 6 checks. In a class of 12, she would need 12 checks. The number of checks grows in proportion to the number of students , so this way of counting has linear time complexity, written .
Fig. 1. One box per student, with the month the student was born in.
To find the pairs of students born in the same month, compare each student with every student after them: student 1 with students 2 to 6, student 2 with students 3 to 6, and so on. For 6 students that is 5 + 4 + 3 + 2 + 1 = 15 checks, and 4 of them find a pair. For 12 students it is 66 checks. Twice the students take about four times the checks, because students make n(n - 1)/2 checks, which grows like n^2. This is quadratic complexity, O(n^2): a loop over the students with another loop over the students inside it.
Fig. 2. Checks against the class size: one per student for March, one per pair for the same month.
Counting how the steps grow gives one way to compare any two programs, whatever they compute. Once the input is large enough, the one whose count grows slower is faster: an program beats an O(n^2) one.
A step is one simple operation, such as a comparison, an addition or reading a number. A program rarely makes exactly steps. The March count reads each month and compares it with 3, and it makes a few steps before and after the loop, so it takes something like 2n + 5 steps. Such constants don't change how the count grows. At n = 10^6, 2n + 5 is still about twice , while n^2 is a million times . So only the part that grows fastest is kept, without its constant factor: 2n + 5 is written , and n(n - 1)/2 = n^2/2 - n/2 is written O(n^2) (read "big O of n squared"). The number of steps as a function of the input size, written this way, is the time complexity.
The difference matters for large inputs. For 10^6 students the pair program makes about 5 \cdot 10^{11} checks. A common rule of thumb for turning steps into seconds is about 10^8 simple steps per second. It is a rough estimate, not an exact figure. At that speed the pair program takes about 5,000 seconds, well over an hour.
Once the complexity of the first approach is known, you can look for a faster one. Keep an array of 12 counters, one per month: counter 1 holds how many students were born in January, counter 12 how many in December. Go through the students once and add 1 to the counter of each student's month, then go through the 12 counters once. A month with students gives c(c - 1)/2 pairs, the same formula as before on a smaller group. That is n + 12 steps, which is .
To estimate a solution, go through four stages:
Find : the largest input size the statement allows.
Count the runs: how many times the busiest line runs on the worst input of that size, as a formula in . The busiest line is the one inside the most loops; each run of it counts as one step.
Keep the largest term: drop constant factors and smaller terms to get .
Compare with the budget: the budget is the time limit in seconds times 10^8. Put the largest into the form and compare. If the result is within a factor of about 3 of the budget, go back to the exact count with its constant.
For the pair program with up to 10^6 students and a time limit of 1 second: find : 10^6. Count the runs: n(n - 1)/2. Keep the largest term: O(n^2). Compare with the budget: 10^{12} against 10^8, far too slow. For the counting program, gives 10^6 steps, a hundredth of the budget.
Estimate for the slowest input of that size, not a lucky one. A search for a student born in March can stop at the first student, or go through all students when nobody was born in March. Tests include inputs like that, so the search is .
Common complexities
Fig. 3 shows how fast the number of steps grows for the complexities you meet most often. The constants are dropped, so only the shape of each curve matters.
Fig. 3. Steps against the input size . Before reaches 25, the curves for n \log n, n^2 and 2^n are already past the top.
Complexity | Name | Twice the input | Typical code |
|---|---|---|---|
constant | the same steps | a formula, such as | |
O(\log n) | logarithmic | one more step | a loop that doubles |
O(\sqrt n) | square root | about 1.4 times the steps |
|
linear | twice the steps | one loop over the input | |
O(n \log n) | n \log n | a bit more than twice | |
O(n^2) | quadratic | four times the steps | a loop inside a loop, such as the pair loop |
O(n^3) | cubic | eight times the steps | three loops, one inside the other |
O(2^n) | exponential | the steps squared | all subsets of items |
factorial | far more than squared | all orders of items, often with backtracking |
\log_2 n is how many times you can double 1 before passing : for (int i = 1; i < n; i *= 2) runs 20 times for n = 10^6, because 2^{20} = 1{,}048{,}576 is the first power of 2 above 10^6.
A complexity can depend on two input sizes. Take an array of numbers and an array of numbers. A loop over the first array, then a loop over the second, is O(n + m): for n = 1000 and m = 2000 that is 3,000 steps. A loop over the second array inside a loop over the first, comparing every pair, is O(n \cdot m): 2,000,000 steps.
From the limits to the complexity
For a time limit of 1 second, with the rule of thumb of about 10^8 steps per second:
Largest | Complexity that fits | Steps at that |
|---|---|---|
10 | 10! = 3{,}628{,}800 | |
20 | O(2^n) | 2^{20} = 1{,}048{,}576 |
400 | O(n^3) | 6.4 \cdot 10^7 |
5,000 | O(n^2) | 2.5 \cdot 10^7 |
10^6 | O(n \log n) | about 2 \cdot 10^7 |
10^8 | 10^8, the whole budget: check the constant (stage 4) | |
10^{12} | O(\sqrt n) | 10^6 |
10^{18} | O(\log n) | about 60 |
Find the largest in the statement and take the first row whose is at least that large. Aim for that complexity or a faster one.
A shop has up to 200,000 receipts. For each receipt, count how many receipts have a smaller total. The obvious program compares every receipt with every other one. Find : 200,000. Count the runs: each of the receipts is checked against n - 1 others, so n(n - 1) checks.
When the constant matters
The constant matters in two cases: two solutions have the same , or the estimate is within a factor of about 3 of the budget (stage 4). Then the kind of step matters: a division is slower than an addition. An inner loop over 26 letters for n = 10^7 is , but it is 2.6 \cdot 10^8 steps, more than the budget of 1 second. Reading 10^6 numbers with cin also needs the line ios::sync_with_stdio(false); cin.tie(nullptr);.
Common mistakes
Missing a loop hidden in one line, such as
s = s + c;, which copies the whole string each time, orv.erase(v.begin()), which moves all remaining elements one place forward. Writes += c;, and instead of erasing from the front keep an index that moves forward or use a queue.Forgetting the cost of a built-in function.
sortis O(n \log n), not one step, so sorting inside a loop that runs times is O(n^2 \log n); sort once, before the loop.Forgetting the number of test cases, which multiplies the steps of one test by . Count times the steps for the largest , unless the statement limits the sum of over all tests.
Measuring the input by its length instead of its value, as if one input number made a program , while checking every from 1 to is . Stopping at \sqrt n makes it O(\sqrt n).
Counting only the lucky case. Time limits are checked on the slowest input of each size, so a program that is fast on most inputs can still exceed the limit and lose points. Still, when you can't find a faster solution, a slow one is worth submitting: in contests that score each test, it earns the points of the small tests.