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 T(n)T(n). This function reports the number of primitive operations an algorithm performs when given an input of size nn. 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. T(n)T(n) 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, T(n)T(n) 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 T(n)T(n). This includes choosing nn, 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-Ω\Omega, Big-Θ\Theta). It also sets up the systematic loop-counting techniques that follow. These techniques follow directly from having a well-defined T(n)T(n) in hand.

Key takeaways
  • Running time is measured as a function T(n)T(n) of input size nn, not as a single number of seconds.
  • nn must be defined precisely for each problem (array length, number of vertices, bit-length of a number, etc.).
  • T(n)T(n) 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.
  • T(n)T(n) 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.
The core insight

Running time becomes a rigorous, comparable quantity only once it is expressed as a function T(n)T(n) of a precisely defined input size nn, 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 T(n)T(n), where nn is a natural number summarizing "how much input" the algorithm was given. Choosing nn is itself a modeling decision, not an automatic fact. For an array-processing algorithm, nn is usually the number of elements. For a graph algorithm, the natural choice is often a pair, the number of vertices VV and edges EE, 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 NN as the size of an integer input instead of log⁡N\log N) silently changes every subsequent complexity claim, so making nn explicit and defensible is the first task of any analysis. Once nn is fixed, T(n)T(n) is constructed compositionally: each line or statement of an algorithm has some cost cic_i to execute once, and some number of times ti(n)t_i(n) it is actually executed as a function of nn (and possibly of the specific input, not just its size). The total running time is the sum

T(n)=∑ici⋅ti(n).T(n) = \sum_i c_i \cdot t_i(n).

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 T(n)T(n) 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 nn 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 O(1)O(1) 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 ii, write down cic_i (constant cost per execution — often left symbolic since its exact value depends on the machine/compiler) and ti(n)t_i(n) (number of times the line executes, which may depend on nn and, for inputs of the same size, on the specific input).

Step 4 — Sum. Add ci⋅ti(n)c_i \cdot t_i(n) over all lines to obtain T(n)=∑iciti(n)T(n) = \sum_i c_i t_i(n).

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 T(n)T(n) 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 c1,c2,c3,c4c_1, c_2, c_3, c_4 denote the constant time for a loop-control step, a comparison, a return statement inside the loop, and the final return, respectively:

LineCost per executionNumber of executions
1 (loop control)c1c_1up to n+1n+1 (the extra +1 for the final failing test)
2 (comparison)c2c_2up to nn
3 (return on match)c3c_300 or 11
4 (return -1)c4c_400 or 11

If xx 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

T(n)=c1(n+1)+c2n+c4=(c1+c2) n+(c1+c4).T(n) = c_1(n+1) + c_2 n + c_4 = (c_1 + c_2)\,n + (c_1 + c_4).

If xx is at position 1, lines 1 and 2 execute once each and line 3 executes once — the best-case running time

T(n)=c1+c2+c3,T(n) = c_1 + c_2 + c_3,

a constant, independent of nn. This single example already shows the key subtlety: T(n)T(n) is not always a single well-defined function per size nn; it can depend on which input of size nn 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) ```

Figure
Building T(n): from pseudocode lines to a summed cost function
LINEAR-SEARCH(A, n, x) 1 for i = 1 to n 2 if A[i] == x 3 return i 4 return -1 cost c₁ × count (n+1) cost c₂ × count n cost c₃ × count (0 or 1) cost c₄ × count (0 or 1) T(n) = c₁(n+1) + c₂·n + c₃·[found] + c₄·[not found] Worst case (never found): T(n) = (c₁+c₂)n + (c₁+c₄) — grows linearly in n Best case (found at i=1): T(n) = c₁+c₂+c₃ — a constant, independent of 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 T(n)=∑iciti(n)T(n) = \sum_i c_i t_i(n) correctly and exactly reports the total time the algorithm spends on an input of size nn, 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 cic_i — so

total time=∑instructions executedcost=∑lines ici×(number of times line i executes)=∑ici ti(n).\text{total time} = \sum_{\text{instructions executed}} \text{cost} = \sum_{\text{lines } i} c_i \times (\text{number of times line } i \text{ executes}) = \sum_i c_i\, t_i(n).

This is not an approximation; it is exact bookkeeping, as long as ti(n)t_i(n) is computed correctly for the given input. The only subtlety is that tit_i may depend on more than nn — 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 T(n)T(n) alone is sometimes ambiguous, and the standard resolution is to define three distinct functions:

  • Worst-case T(n)=max⁡T(n) = \max over all inputs of size nn of the actual running time — a guarantee that no input of that size does worse.
  • Best-case T(n)=min⁡T(n) = \min over all inputs of size nn — often less useful as a guarantee, but useful for lower-bound intuition.
  • Average-case T(n)=E[running time]T(n) = \mathbb{E}[\text{running time}] under some assumed distribution over inputs of size nn — powerful but only as meaningful as the distributional assumption.

Complexity of computing T(n)T(n) itself. This is a meta-question worth being honest about: constructing T(n)T(n) for a piece of code with loop nesting depth dd 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 ∑i=1ni=n(n+1)/2\sum_{i=1}^n i = n(n+1)/2, 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 ti(n)t_i(n) for data-dependent control flow, which is where case analysis (best/worst/average) becomes unavoidable.

Tradeoffs to be honest about. Exact T(n)T(n), expressed with explicit constants cic_i, 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 T(n)T(n). Skipping straight to "it's obviously O(n2)O(n^2)" 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 nn. ``` 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 tjt_j denote the number of times the `while`-test in line 4 is evaluated during the jj-th outer iteration (this is the input-dependent quantity). Costs c1,…,c7c_1,\dots,c_7 are the constant per-execution costs of each line.

LineCostExecutions
1c1c_1nn (the loop runs for j=2,…,nj=2,\dots,n, plus the final failing test)
2c2c_2n−1n-1
3c3c_3n−1n-1
4c4c_4∑j=2ntj\sum_{j=2}^{n} t_j
5c5c_5∑j=2n(tj−1)\sum_{j=2}^{n} (t_j - 1)
6c6c_6∑j=2n(tj−1)\sum_{j=2}^{n} (t_j - 1)
7c7c_7n−1n-1

Summing gives the general form

T(n)=c1n+(c2+c3+c7)(n−1)+c4∑j=2ntj+(c5+c6)∑j=2n(tj−1).T(n) = c_1 n + (c_2+c_3+c_7)(n-1) + c_4 \sum_{j=2}^n t_j + (c_5+c_6)\sum_{j=2}^n (t_j - 1).

Now trace two concrete inputs of the same size n=5n = 5 to see tjt_j diverge:

Input A — already sorted: A=[2,5,7,9,12]A = [2, 5, 7, 9, 12]. For each jj, `A[i] > key` is false immediately (the element to the left is never larger), so tj=1t_j = 1 for every j=2,…,5j = 2,\dots,5.

∑j=25tj=4,∑j=25(tj−1)=0.\sum_{j=2}^5 t_j = 4, \qquad \sum_{j=2}^5 (t_j - 1) = 0.
T(5)=c1(5)+(c2+c3+c7)(4)+c4(4)+(c5+c6)(0),T(5) = c_1(5) + (c_2+c_3+c_7)(4) + c_4(4) + (c_5+c_6)(0),

a best-case running time — linear in nn for this family of inputs.

Input B — reverse sorted: A=[12,9,7,5,2]A = [12, 9, 7, 5, 2]. Now every new key must shift past all previously placed elements: tj=jt_j = j for j=2,…,5j=2,\dots,5 (the while-loop runs until `i` hits 0, i.e., j−1j-1 comparisons succeed plus one failing test, giving tj=jt_j = j).

∑j=25tj=2+3+4+5=14,∑j=25(tj−1)=1+2+3+4=10.\sum_{j=2}^5 t_j = 2+3+4+5 = 14, \qquad \sum_{j=2}^5 (t_j-1) = 1+2+3+4 = 10.
T(5)=c1(5)+(c2+c3+c7)(4)+c4(14)+(c5+c6)(10),T(5) = c_1(5) + (c_2+c_3+c_7)(4) + c_4(14) + (c_5+c_6)(10),

a worst-case running time — quadratic in nn in general, since for reverse-sorted input of general size nn,

∑j=2nj=n(n+1)2−1,\sum_{j=2}^n j = \frac{n(n+1)}{2} - 1,

a term proportional to n2n^2. Same algorithm, same n=5n=5, wildly different T(n)T(n) — precisely because tjt_j 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 T(n)T(n).

Common pitfalls & traps

PitfallWhy it happensConsequence
Using the numeric value of an input as nn instead of its size in bitsConfusing "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 executionA `for`/`while` loop tests its condition one more time than the body executesOff-by-one error in ti(n)t_i(n), e.g. writing nn instead of n+1n+1
Treating T(n)T(n) as if it were single-valued per nnIgnoring that execution counts can be input-dependent, not just size-dependentSilently reports "the" running time when only best/worst/average case is meaningful
Counting only the outer loop, ignoring nested-loop dependenceSloppy line-by-line accounting; treating nested loops as if independent of the outer indexMiscounts total operations (e.g., missing that insertion sort's inner loop bound depends on jj)
Assuming all primitive operations cost the same constantRAM 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
AT
Alan Turing
1912–1954 · Formalized the notion of mechanical computation via the Turing machine, providing the substrate on which "steps taken as a function of input" could later be made rigorous

DE
Donald E. Knuth
1938–present · Systematized the analysis of algorithms in The Art of Computer Programming, popularizing precise operation-counting and the running-time function T(n)T(n) as the standard object of study

History
1936
— Turing formalizes mechanical computation, giving a rigorous notion of "step" for later cost-counting.
1949
— early computer designers begin informally estimating instruction counts for numerical routines.
1965
— Jack Edmonds argues informally for "efficient" algorithms as those with polynomially bounded step counts in input size.
1968
— Knuth's The Art of Computer Programming Volume 1 formalizes counting primitive operations as T(n)T(n).
1971
— Cook's theorem on NP-completeness cements input-size-parameterized running time as the field's central quantity.

Where this matters

Every performance-sensitive system implicitly reasons in terms of T(n)T(n). 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 nn, then constructing T(n)T(n) 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 T(n)T(n) of input size instead separates the algorithm's behavior — how the number of primitive operations grows — from these incidental machine effects. The constants cic_i absorb machine-dependence, while the shape of T(n)T(n) as nn 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. T(n)T(n) 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, T(n)T(n) is more naturally expressed as a recurrence relation (e.g., T(n)=2T(n/2)+cnT(n) = 2T(n/2) + cn 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 T(n)T(n) in the first place.

How does choosing n incorrectly break an entire analysis?

Every subsequent step — the operation counts ti(n)t_i(n), the sum T(n)T(n), and any later asymptotic classification — is defined relative to nn. If nn 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 nn is defined.

What's the difference between T(n) for a specific input and T(n) as worst-case complexity?

T(n)T(n), in the fullest sense, is a value that can depend on the specific input, not merely on its size nn — two arrays of the same length can force an algorithm through different execution paths. "Worst-case complexity" is a derived, single-valued function of nn obtained by taking the maximum of T(n)T(n) over all inputs of that size. It is a guarantee: no input of size nn will exceed it. Best-case and average-case are the analogous minimum and expectation. All three are built from the same underlying per-input T(n)T(n); 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 cic_i 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 T(n)T(n) has been honestly constructed as ∑iciti(n)\sum_i c_i t_i(n), Asymptotic Notation provides a principled way to discard the cic_i's and lower-order terms, keeping only the dominant growth rate as n→∞n \to \infty. 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.

Before this

Where this leads