Measuring Running Time as a Function of Input Size
Imagine two programmers each hand you a sorting routine and both claim theirs is "fast." Without a shared way to measure speed, this claim is meaningless. Fast on what input, on what machine, compared to what? The first real step toward a science of algorithms is to stop asking "how many seconds does this take." Instead, we start asking "how does the work grow as the input grows." That question forces us to define a function, conventionally written . This function reports the number of primitive operations an algorithm performs when given an input of size . This idea matters because real systems handle inputs whose size is never fixed. Input size is often unpredictable — a database query might touch ten rows or ten billion. A search engine indexes pages by the trillion. A compiler parses files ranging from a few lines to millions. An algorithm that is instantaneous on small inputs can be catastrophically slow on large ones. The only way to see this coming is to express running time as a function of input size, rather than as a single number. is that function. Building it correctly is a foundational skill that everything else in algorithm analysis rests on. This means deciding what counts as "the input," what counts as "size," and what counts as an "operation." The big picture is this: running time is not a property of an algorithm in isolation. It is a relationship between an algorithm and a family of inputs, parameterized by size. Once we accept that framing, becomes a precise mathematical object. It is built by counting how many times each primitive step in the RAM Model of Computation executes, then summing those counts. This is exactly the same activity as counting steps in a physics experiment or terms in a series. Nothing about it is approximate at this stage. The approximation, via asymptotic notation, comes later. This article delivers a careful, mechanical method for building . This includes choosing , identifying primitive operations, and writing counting expressions for each line of pseudocode. It also involves summing them into a closed-form or bounded expression. It sets up, without yet introducing, the asymptotic simplifications (Big-O, Big-, Big-). It also sets up the systematic loop-counting techniques that follow. These techniques follow directly from having a well-defined in hand.
- Running time is measured as a function of input size , not as a single number of seconds.
- must be defined precisely for each problem (array length, number of vertices, bit-length of a number, etc.).
- is built by counting primitive operations from the RAM Model of Computation and summing their costs.
- Different inputs of the same size can yield different operation counts — this is where best/worst/average case enters.
- is typically written as a sum of terms, each the product of a per-execution cost and an execution count.
- This exact counting is the foundation for later simplification via Asymptotic Notation and systematic loop analysis.
Running time becomes a rigorous, comparable quantity only once it is expressed as a function of a precisely defined input size , built by summing counts of primitive operations.
The idea
An algorithm's running time is the total cost of the primitive operations it executes on a given input, where cost is measured in the RAM Model of Computation as a constant amount of time per operation (arithmetic, comparison, assignment, array indexing, and so on). But a single input gives a single number, and a single number tells us almost nothing about how the algorithm will behave on inputs we have not yet seen. The essential move in algorithm analysis is to treat running time not as a number but as a function of input size, denoted , where is a natural number summarizing "how much input" the algorithm was given. Choosing is itself a modeling decision, not an automatic fact. For an array-processing algorithm, is usually the number of elements. For a graph algorithm, the natural choice is often a pair, the number of vertices and edges , or sometimes a single combined parameter. For an algorithm on integers — testing primality, for instance — the size should be the number of bits needed to represent the integer, not the integer's numeric value itself, because the RAM model charges cost per word or per bit, not per unit of magnitude. Getting this choice wrong (for example, treating the value as the size of an integer input instead of ) silently changes every subsequent complexity claim, so making explicit and defensible is the first task of any analysis. Once is fixed, is constructed compositionally: each line or statement of an algorithm has some cost to execute once, and some number of times it is actually executed as a function of (and possibly of the specific input, not just its size). The total running time is the sum
This single equation is the entire content of "measuring running time": identify the statements, cost each one, count how many times each runs, multiply, and add. Everything that comes later — Asymptotic Notation, the classification of loops, the notions of best-case/worst-case/average-case complexity — is built on top of this sum, either by bounding it, simplifying it, or taking it over different input distributions.
How it works
Building for a piece of pseudocode is a mechanical, line-by-line process. The procedure below formalizes it.
Step 1 — Fix the input-size parameter. Decide what denotes for this problem: array length, number of nodes, number of bits, etc. State it explicitly.
Step 2 — Identify primitive operations. Under the RAM model, primitive operations include: arithmetic operations (+, −, ×, ÷), comparisons, assignments, array/field access, and control transfers (a loop test, a function call setup). Each is assumed to cost actual time — some fixed constant, though the constant may differ operation to operation.
Step 3 — Annotate each line with a cost and an execution count. For each statement , write down (constant cost per execution — often left symbolic since its exact value depends on the machine/compiler) and (number of times the line executes, which may depend on and, for inputs of the same size, on the specific input).
Step 4 — Sum. Add over all lines to obtain .
Step 5 — Simplify / bound. Collapse the sum into a closed form (using arithmetic series formulas for loops, for instance) and, if the count depends on more than the size (i.e., on the specific input), define worst-case, best-case, or average-case by taking a max, min, or expectation over all inputs of that size. Here is the process carried out on a concrete algorithm — linear search for a target value in an array. ``` LINEAR-SEARCH(A, n, x) 1 for i = 1 to n 2 if A[i] == x 3 return i 4 return -1 ``` Line-by-line costs, letting denote the constant time for a loop-control step, a comparison, a return statement inside the loop, and the final return, respectively:
| Line | Cost per execution | Number of executions |
|---|---|---|
| 1 (loop control) | up to (the extra +1 for the final failing test) | |
| 2 (comparison) | up to | |
| 3 (return on match) | or | |
| 4 (return -1) | or |
If is not in the array (or is at the last position), lines 1 and 2 each execute the maximum number of times, and line 4 executes once, giving the worst-case running time
If is at position 1, lines 1 and 2 execute once each and line 3 executes once — the best-case running time
a constant, independent of . This single example already shows the key subtlety: is not always a single well-defined function per size ; it can depend on which input of size is given, which is exactly why "worst case," "best case," and "average case" become necessary refinements once we start summing operation counts. ``` // General recipe for building T(n) from pseudocode FUNCTION BUILD-T(algorithm, n): total_cost := 0 for each line L in algorithm (in structural, not execution, order): c_L := constant unit-cost of executing L once // from RAM model t_L(n) := symbolic expression for how many times L executes, as a function of n (and possibly the specific input) total_cost := total_cost + c_L * t_L(n) return total_cost as T(n) ```
Complexity & correctness
"Correctness" here is not about the algorithm's output — it is about the analysis itself. The claim we must justify is: the sum correctly and exactly reports the total time the algorithm spends on an input of size , under the RAM model. The argument is a direct consequence of how the RAM model is defined. In that model, a program's execution is a finite sequence of primitive steps, each taking a fixed amount of time (allowed to differ between operation types, but not between instances of the same line). Total time is therefore, by definition, the sum over all executed instructions of their individual costs. Grouping this sum by line of source code rather than by individual instruction instance is valid because instances of the same line all cost the same — so
This is not an approximation; it is exact bookkeeping, as long as is computed correctly for the given input. The only subtlety is that may depend on more than — two inputs of the same size can force different execution counts (as in linear search, where a "hit" at position 1 and a "miss" produce wildly different line-3 and line-4 counts). This is precisely why alone is sometimes ambiguous, and the standard resolution is to define three distinct functions:
- Worst-case over all inputs of size of the actual running time — a guarantee that no input of that size does worse.
- Best-case over all inputs of size — often less useful as a guarantee, but useful for lower-bound intuition.
- Average-case under some assumed distribution over inputs of size — powerful but only as meaningful as the distributional assumption.
Complexity of computing itself. This is a meta-question worth being honest about: constructing for a piece of code with loop nesting depth and no data-dependent branching typically requires evaluating nested summations, which for straightforward polynomial loop bounds resolve in closed form using standard series identities (arithmetic series , geometric series, etc.) — an operation that is itself "fast" (it is symbolic algebra, not something we run at execution time). The genuine difficulty is not computing the sum once counts are known; it is correctly determining for data-dependent control flow, which is where case analysis (best/worst/average) becomes unavoidable.
Tradeoffs to be honest about. Exact , expressed with explicit constants , is precise but fragile: the constants depend on the compiler, the instruction set, cache behavior, and other machine details not modeled at this level of abstraction. This fragility is exactly the motivation for moving to Asymptotic Notation, which discards the constants and lower-order terms and keeps only the growth rate — but that step is only trustworthy because it is built on top of an honestly and completely constructed . Skipping straight to "it's obviously " without ever writing the sum is how subtle errors (miscounting a loop bound, forgetting an operation, conflating value and bit-length as the size parameter) slip through unnoticed.
Worked example
Consider insertion sort, a slightly richer example where the execution count of the inner loop genuinely depends on the specific input, not just on . ``` INSERTION-SORT(A, n) 1 for j = 2 to n 2 key = A[j] 3 i = j - 1 4 while i > 0 and A[i] > key 5 A[i+1] = A[i] 6 i = i - 1 7 A[i+1] = key ``` Let denote the number of times the `while`-test in line 4 is evaluated during the -th outer iteration (this is the input-dependent quantity). Costs are the constant per-execution costs of each line.
| Line | Cost | Executions |
|---|---|---|
| 1 | (the loop runs for , plus the final failing test) | |
| 2 | ||
| 3 | ||
| 4 | ||
| 5 | ||
| 6 | ||
| 7 |
Summing gives the general form
Now trace two concrete inputs of the same size to see diverge:
Input A — already sorted: . For each , `A[i] > key` is false immediately (the element to the left is never larger), so for every .
a best-case running time — linear in for this family of inputs.
Input B — reverse sorted: . Now every new key must shift past all previously placed elements: for (the while-loop runs until `i` hits 0, i.e., comparisons succeed plus one failing test, giving ).
a worst-case running time — quadratic in in general, since for reverse-sorted input of general size ,
a term proportional to . Same algorithm, same , wildly different — precisely because depends on the specific arrangement of the input, not merely on its size. This is the concrete phenomenon that forces best-/worst-/average-case running time to be defined as separate functions rather than a single .
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Using the numeric value of an input as instead of its size in bits | Confusing "the number" with "the encoding of the number" | Algorithms on integers (e.g., primality testing) look exponential when they are actually polynomial in the true input size |
| Forgetting the "extra" loop-test execution | A `for`/`while` loop tests its condition one more time than the body executes | Off-by-one error in , e.g. writing instead of |
| Treating as if it were single-valued per | Ignoring that execution counts can be input-dependent, not just size-dependent | Silently reports "the" running time when only best/worst/average case is meaningful |
| Counting only the outer loop, ignoring nested-loop dependence | Sloppy line-by-line accounting; treating nested loops as if independent of the outer index | Miscounts total operations (e.g., missing that insertion sort's inner loop bound depends on ) |
| Assuming all primitive operations cost the same constant | RAM model gives O(1) cost per operation, but the constants genuinely differ (e.g., multiply vs. increment) | Overprecise/misleading comparisons between algorithms based on raw constant counts |
Pioneers & history
The idea of measuring computational cost as a function that grows with problem size emerges from the convergence of several lines of thought: formal models of computation, the early practice of algorithm design, and the mathematics of asymptotic growth borrowed from analysis and number theory.
Origins · 1 minAlan Turing and how this was discovered
Where this matters
Every performance-sensitive system implicitly reasons in terms of . Database query planners estimate cost as a function of table size before choosing a join strategy. Compilers reason about the size of an abstract syntax tree when deciding how aggressively to optimize a pass. Network routers must bound per-packet processing time as a function of routing-table size to guarantee throughput under load. Cryptographic protocols are designed and broken based on whether an attack's running time is polynomial or exponential in the bit-length of the key — exactly the input-size subtlety highlighted above. Machine learning pipelines profile training time as a function of dataset size and feature dimension before scaling to production data. In every one of these settings, the discipline of first fixing , then constructing by counting operations, is what turns "this seems slow" into a claim that can be checked, compared, and trusted — and it is the necessary foundation before growth rates are compared abstractly via Asymptotic Notation or loop structures are analyzed systematically as in Analyzing Loops by Counting Operations.
Ask the tutor: Can you walk through building T(n) for a doubly-nested loop where the inner bound depends on the outer index?
Why isn't "running time in seconds" a good enough measure on its own?
Running time in seconds mixes together the algorithm's inherent workload with machine-specific facts: clock speed, compiler optimizations, cache behavior, and even background system load. Two runs of the identical algorithm on different hardware, or even the same hardware under different conditions, can report different wall-clock times without the algorithm having changed at all. Measuring running time as a function of input size instead separates the algorithm's behavior — how the number of primitive operations grows — from these incidental machine effects. The constants absorb machine-dependence, while the shape of as grows captures something intrinsic to the algorithm, which is exactly what later gets distilled further via Asymptotic Notation.
Is T(n) always a polynomial or simple closed-form expression?
Not necessarily. is defined as a sum of costs times execution counts, and for straightforward loop-based algorithms this sum often does resolve into a closed form using standard series identities. But for algorithms with recursive structure, is more naturally expressed as a recurrence relation (e.g., for merge sort) rather than a closed form directly. Solving such recurrences (by substitution, recursion trees, or the Master Theorem) is a distinct — though related — skill built on top of correctly setting up in the first place.
How does choosing n incorrectly break an entire analysis?
Every subsequent step — the operation counts , the sum , and any later asymptotic classification — is defined relative to . If does not faithfully represent "how much input the algorithm actually receives," the resulting function describes growth with respect to the wrong quantity. The classic failure mode is treating the value of a numeric input as its size instead of its bit-length; this single choice can make an algorithm appear to have exponential running time in the "size" you chose when it is actually polynomial in the true (bit-length) size, or vice versa. There is no way to recover from this downstream — the fix must happen at the point is defined.
What's the difference between T(n) for a specific input and T(n) as worst-case complexity?
, in the fullest sense, is a value that can depend on the specific input, not merely on its size — two arrays of the same length can force an algorithm through different execution paths. "Worst-case complexity" is a derived, single-valued function of obtained by taking the maximum of over all inputs of that size. It is a guarantee: no input of size will exceed it. Best-case and average-case are the analogous minimum and expectation. All three are built from the same underlying per-input ; they differ only in how they aggregate over the inputs of a fixed size.
Does building T(n) by hand scale to large, complex programs?
For small procedures with simple control flow, hand-counting each line is entirely feasible and is exactly the discipline this article develops. For larger programs, the same principle applies but the bookkeeping is organized around structural rules: sequential statements add their costs, loops multiply a body's cost by an iteration count (possibly itself needing analysis, as with nested loops), and conditionals take the cost of whichever branch actually executes (or the worst of the branches, for worst-case analysis). This structural, compositional view — formalized further in Analyzing Loops by Counting Operations — is what allows the technique to scale beyond toy examples without abandoning the underlying "sum of cost times count" definition.
Why do we eventually discard the constants c_i if they're part of an honest T(n)?
The constants are genuinely part of the true running time, but they depend on details — machine architecture, compiler, instruction encoding — that are outside the scope of what an algorithm's design controls. Once has been honestly constructed as , Asymptotic Notation provides a principled way to discard the 's and lower-order terms, keeping only the dominant growth rate as . This is not a shortcut around doing the counting — it is a simplification applied after the counting has been done correctly, which is why this article's exact-sum construction is a prerequisite rather than an alternative to asymptotic analysis.