Algorithm Analysis in Computer Science
Algorithm Analysis in Computer Science
In the previous chapter, we approached computer science through the lens of problem
solving, noting that solving a problem with the help of computers requires far more than
simply writing instructions that work. True efficiency demands careful attention to how
those instructions are executed. A poorly chosen algorithm or data structure can render
a program impractical, no matter how logically sound it appears, whereas well-chosen
approaches enable solutions to remain effective and scalable as problem sizes grow. At the
heart of this challenge lies the field of algorithm analysis. An algorithm, in its simplest
form, is a finite sequence of well-defined steps that transforms input data into the desired
output, but the central questions that computer scientists must ask go deeper: does the
algorithm always produce the correct result, and if so, how much time and memory will it
require as the input size increases? These questions define the two essential dimensions
of analysis—correctness verification, which ensures that an algorithm terminates with
the intended output for all valid inputs, and complexity analysis, which measures the
computational resources needed as the problem grows. In this sense, algorithm analysis
functions like a microscope, enabling us to look beyond surface-level correctness and
evaluate how well a solution is prepared to meet the demands of real-world data. A simple
example illustrates why such analysis is indispensable: imagine two sorting algorithms,
both capable of producing the correct order for a list of one million numbers. If one
completes the task in seconds while the other requires several hours, only the first is
practically useful. This contrast shows that correctness alone is not enough—efficiency is
equally central. More broadly, algorithm analysis equips us with the means to predict
performance before implementation, to compare competing solutions objectively, to
identify and eliminate bottlenecks, and to understand the very limits of what problems
can be solved efficiently. These skills are not abstract; they are vital in domains such as
big data analytics, machine learning, scientific simulation, and real-time systems, where
inefficiency can turn a feasible task into an impossible one. Ultimately, algorithm analysis
provides a universal language for reasoning about efficiency, independent of programming
languages, hardware platforms, or low-level implementation details.
1. Explain what algorithm analysis is and why it plays a central role in computer
science.
5. Apply mathematical tools such as substitution, recursion trees, and the Master
Theorem to solve recurrence relations.
6. Compare and evaluate multiple algorithms objectively based on their time and space
requirements.
Proving correctness is not always trivial, but computer science has developed systematic
methods to approach it. One of the most widely used techniques is mathematical induction,
which is particularly well suited to the recursive and iterative structures of algorithms.
Induction begins with a base case, showing that the algorithm works for the smallest
possible input, then assumes correctness for inputs of size n, and finally demonstrates that
correctness must also hold for input size n + 1. This reasoning mirrors the incremental
way many algorithms build their solutions. For iterative algorithms, the idea is often
formalized through loop invariants, which are properties that hold true before and after
each loop iteration, ensuring the overall algorithm behaves as expected.
To illustrate these principles, let us examine two fundamental algorithms. The first is
insertion sort, an incremental sorting procedure whose pseudocode is shown in Algorithm 1.
The key idea behind insertion sort is the maintenance of a loop invariant: at the
beginning of each iteration with index i, the subarray A[1..i − 1] is already sorted. The
base case is immediate since A[1] is trivially sorted. Assuming the invariant holds before
iteration i, the inner loop shifts all larger elements to the right and inserts the current key
into its proper place, thereby preserving sorted order in A[1..i]. By induction, when the
outer loop completes, the entire array A[1..n] is sorted. Termination is straightforward:
the inner loop reduces j at each step, and the outer loop runs only n − 1 times. Thus,
Algorithm 1 is totally correct—it always halts and produces a sorted permutation of the
input.
A second example is binary search, an efficient method for locating an element within
a sorted array. Its pseudocode is presented in Algorithm 2.
2 high ← n
11 return NOT_FOUND
Here, the loop invariant states that, at the start of each iteration, if the target element
x is present, then it must lie within the current interval A[low..high]. Initially, this
interval covers the entire array. At each step, the midpoint element is compared with x: if
equal, the search succeeds immediately; if smaller, the interval is narrowed to the right
half; if larger, to the left half. In each case, the invariant is preserved because the new
interval still contains all possible occurrences of x. The loop terminates when low > high,
at which point the interval is empty and the algorithm correctly reports failure. Since
the interval shrinks strictly with each iteration, termination occurs after at most ⌈log2 n⌉
steps. Thus, Algorithm 2 is also totally correct.
These examples demonstrate the general pattern of correctness proofs: begin with
a clearly stated invariant or inductive claim, verify it in the base case, argue that it is
preserved through each step, and conclude correctness for all inputs. Correctness proofs,
reinforced with explicit pseudocode references, not only clarify what an algorithm does but
also explain why it does so reliably. By mastering this approach, one gains the ability to
prove that algorithms terminate, that they produce the intended results, and that they can
be trusted in both theoretical analysis and practical applications. Yet, correctness alone
does not settle the matter of which algorithm is best. When faced with a computational
problem, it is common to discover that more than one algorithmic solution exists. Each
approach may appear valid, yet their performance can differ drastically, making it difficult
to determine which is the most suitable. This naturally raises a second fundamental
question: by what criteria do we decide which algorithm is preferable? The efficiency of an
algorithm is typically evaluated along two fundamental dimensions: time and space. Time
efficiency refers to how quickly an algorithm completes its task, or in other words, the
number of computational steps required as the input size grows. This measure is called the
time complexity of the algorithm. Space efficiency, on the other hand, concerns the amount
of memory consumed during execution. This measure is called the space complexity of the
algorithm. Together, these two perspectives provide a framework for comparing algorithms
on both speed and memory usage. In practice, time complexity is often emphasized, since
execution time usually dominates performance considerations in large-scale applications.
Ideally we minimize both time and space, but perfection in both dimensions is rarely
achievable. A faster algorithm may require more memory; a memory-frugal one may be
slower. The right choice depends on the deployment context: embedded systems often
privilege space efficiency, while high-throughput cloud services typically prioritize time
efficiency and predictable latency.
Formally, we model performance with two axes: time complexity T (n), which describes
how running time grows with input size n, and space complexity S(n), which tracks how
working memory grows with n. In practice, polynomial-time algorithms (O(1), O(log n),
O(n), O(n log n), O(n2 )) are generally preferred, while exponential (O(2n )) algorithms are
infeasible beyond small n. Table 1 offers numerical intuition, while Figure 1 visualizes how
these rates diverge.
100
O(log n)
Growth (arbitrary scale)
80
O(n)
O(n2 )
60 O(2 ) (scaled)
n
40
20
0
5 10 15 20 25 30 35 40 45 50
Input Size n
While asymptotic analysis captures how running time scales, the observed execution
time of a concrete program depends on many additional factors. Input size and distribution
play a major role: quicksort, for example, is fast on random inputs but can degrade on
nearly sorted ones with poor pivot selection. The cost model and problem representation
also matter: a “comparison” of big integers is far costlier than that of machine integers, and
the efficiency of graph algorithms depends on whether adjacency lists or matrices are used.
Programming language and compiler quality influence timing as well. Compiled languages
like C/C++ often yield faster code than interpreted ones, and optimizations such as inlining
or vectorization can improve performance. Data structures and memory layout shape cache
behavior: contiguous arrays benefit from locality, while pointer-based structures incur
cache misses. Hardware characteristics further complicate matters, with CPU speed, cache
sizes, memory bandwidth, and storage type (SSD vs HDD) all affecting results. When data
exceed RAM, I/O becomes the bottleneck, requiring external-memory algorithms designed
to minimize block transfers. Parallelism and concurrency can accelerate computation, but
gains depend on load balancing, synchronization overheads, and the limits of Amdahl’s
law. Even the operating system, runtime environment, and background load can create
variability, as can details of the implementation such as branch prediction friendliness
or use of specialized instructions. Taken together, these observations highlight the dual
Apriori Analysis
Apriori analysis is a theoretical, machine-independent approach conducted before an
algorithm is implemented. The goal is to predict how resource consumption grows as the
input size n increases. The method relies on counting the number of basic operations and
expressing this frequency as a function of n. This abstraction ignores implementation
details such as processor speed, compiler optimizations, or memory hierarchy, focusing
instead on asymptotic notation—Big-O, Θ, and Ω—to classify algorithms by their growth
rate.
Program A:
x = x + 2; // frequency: 1 → O(1)
Program B:
for i = 1 to n do
x = x + 2; // frequency: n → O(n)
Program C:
for i = 1 to n do
for j = 1 to n do
x = x + 2; // frequency: n^2 → O(n^2)
Here, the number of operations grows from constant (1), to linear (n), to quadratic
(n2 ). Apriori analysis highlights these differences without ever running the program.
Another example is the linear search algorithm. In the worst case, every element of
a list must be checked, leading to O(n) operations. This conclusion is reached purely
through theoretical reasoning, not experimentation.
Posteriori Analysis
Posteriori analysis is an empirical, machine-dependent approach performed after the
algorithm has been implemented. The algorithm is coded, compiled, and executed on a
real platform, and concrete performance metrics such as execution time, memory usage,
and even energy consumption are measured. Because it involves actual execution, posteriori
analysis is influenced by numerous factors: CPU clock speed, cache and memory hierarchy,
branch prediction, operating system overhead, compiler optimizations, and coding style.
This type of analysis is indispensable for validating theoretical predictions and revealing
constant factors or bottlenecks that asymptotic models ignore. For example, two algorithms
may both run in O(n log n) time, yet one may be consistently faster due to better cache
locality or fewer memory allocations.
Apriori analysis tells us that both algorithms share the same average-case complexity
of O(n log n), suggesting equal efficiency in theory. Yet posteriori analysis often reveals
important differences: Quicksort typically outperforms Mergesort in practice due to in-place
partitioning, smaller constant factors, and better cache utilization. Conversely, Mergesort
provides guaranteed O(n log n) performance even in the worst case but requires additional
memory. Here, apriori analysis offers the asymptotic guarantee, while posteriori analysis
exposes the real-world trade-offs. The main distinctions between the two approaches are
In summary, apriori and posteriori analysis are not competing methods but comple-
mentary perspectives. Apriori analysis provides mathematical models for reasoning about
efficiency, while posteriori analysis grounds those models in empirical evidence. Used
together, they ensure that algorithms are not only theoretically elegant but also practically
efficient and reliable in real-world deployment.
Problem Statement
Redesign
Algorithm Design Alternative
Algorithms
Acceptable No
Complexity?
Yes
New Requirements
Implementation
Optimization
Practical Posteriori Analysis
Tuning Constants
Validation Empirical Testing
Memory Patterns
Meets Performance No
Targets?
Yes
Deployable Solution
Figure 2: Algorithm design cycle. Apriori analysis filters designs before implementation;
posteriori analysis validates on real hardware. If targets are not met, optimization iterates
back to implementation, with an optional redesign loop to the initial design. A positive
decision leads to deployment.
The cycle begins with a precise problem statement, which is then translated into one
or more candidate algorithmic designs. Before investing in implementation, the first
checkpoint is apriori analysis. At this stage, designers evaluate the asymptotic complexity
of candidate solutions in order to filter out those with poor scalability, such as O(n2 )
or O(2n ). Only designs with promising theoretical growth rates (e.g., O(n log n)) are
considered for further development. Next, the surviving designs move to implementation.
Here the algorithm is expressed concretely in code, using chosen data structures and
programming constructs. Once implemented, the design undergoes posteriori analysis,
where it is tested empirically on representative inputs and hardware. Performance metrics
such as execution time, memory footprint, and cache behavior are collected to verify
whether theoretical predictions align with real-world outcomes. If the posteriori analysis
and empirical requirements are satisfied, the algorithm exits the cycle as a deployable
solution. This structured approach ensures that development resources are focused on
viable designs and that every algorithm is validated at both the abstract and practical
levels.
5 Fundamental Definitions
Before we can study algorithms in detail, we need to establish a common vocabulary.
When computer scientists analyze algorithms, they are not only interested in whether an
algorithm works, but also in how efficiently it uses time and memory as the input size grows.
In this section, we introduce the key concepts and notations that form the foundation of
algorithm analysis. Understanding these terms will allow us to reason about performance
in a precise way, compare different solutions fairly, and predict how an algorithm will
behave on larger inputs.
Definition 0.3 (Data Size). The data size n refers to the number of elements that the
algorithm must process. Its meaning depends on the type of problem:
Definition 0.4 (Time Complexity). The time complexity T (n) of an algorithm measures
the number of elementary operations as a function of input size n.
Example: Binary search on a sorted array of size n has T (n) = O(log n).
Definition 0.5 (Space Complexity). The space complexity S(n) of an algorithm measures
how much memory is required as a function of n, including input storage, temporary
variables, and recursion stack.
Example: Merge Sort requires O(n) extra memory for temporary arrays.
Definition 0.7 (Best-Case Complexity). The best-case complexity is the minimum number
of operations on any input of size n.
Example: In linear search, the best case occurs when the target is the first element:
T (n) = 1.
Definition 0.9 (Asymptotic Notations). Asymptotic notations describe how functions grow
as n becomes very large:
Example: Bubble Sort has worst-case complexity O(n2 ), while Merge Sort runs in
Θ(n log n).
task of computing the sum of the first N integers. One straightforward method is to add
the numbers sequentially: start with a variable sum equal to zero, and then add 1, then 2,
then 3, and so on until N . This iterative approach is correct and easy to implement, but
its running time grows directly with N , since each new integer requires another addition.
In terms of complexity, this is a linear-time algorithm, denoted O(n). It also requires only
a small, fixed amount of memory (to store the loop counter and the running total), so its
space complexity is constant. However, there exists a far more efficient solution. Instead
N (N + 1)
Sum(N ) = .
2
This formula computes the sum with just a few arithmetic operations, regardless of how
large N is. Its time complexity is constant, O(1), and its memory usage is also constant.
Both methods are mathematically correct, but the difference in efficiency is dramatic:
while the iterative method becomes slower as N increases, the formula always runs in the
same fixed amount of time. For very large N , this difference can determine whether a
program runs instantly or takes an impractical amount of time. This closed-form solution
is famously attributed to Carl Friedrich Gauss. As a young schoolboy, Gauss was asked to
compute the sum of the numbers from 1 to 100. Rather than performing one hundred
additions, he recognized a pattern: pairing the first and last numbers (1 + 100), the
second and second-to-last (2 + 99), and so forth always produced the same sum, 101.
Since there are fifty such pairs, the total must be 50 × 101 = 5050. From this clever
observation, the general formula emerged. Gauss’s insight perfectly illustrates the essence
of complexity analysis: two correct solutions can differ vastly in efficiency, and recognizing
a more efficient method can save enormous effort. By comparing these two algorithms for
summing integers, we see why complexity matters so much. The iterative algorithm has
linear time complexity, O(n), while the closed-form solution achieves constant time, O(1).
This means that as N grows, the difference in performance between the two methods
becomes more and more significant. Complexity analysis therefore guides us in choosing
the right algorithm for the job and helps us understand the practical limits of computation.
In real-world applications where input sizes can be enormous, such insights are not just
theoretical—they are essential for writing programs that finish in a reasonable amount of
time.
This instruction performs one multiplication (5×x), one addition (3+·), and one assignment
(store result in y). Total cost: 3 operations.
Algorithm Sum
X, y, z: integer;
Begin
read(x)
read(y)
z ← x + y
write(z)
End.
Here we have: two reads, one addition, one assignment, and one write. Total cost: 5
operations.
If the condition costs Ccond and the branches cost Cthen and Celse , the total cost is:
Example:
if (x > y) then
z ← x - y
else
z ← y - x
for i ← 1 to n do
sum ← sum + i
for i ← 1 to n do
for j ← 1 to n do
sum ← sum + 1
Inner loop: (2 for the body +1 for the check) · n + 1 = 3n + 1. Outer loop repeats this n
times: n(3n + 1) + 1 = 3n2 + n + 1 operations.
Example 3 (Halving loop):
while n > 1 do
n ← n / 2
k
C(F ) = C(Ij ), where Ij are the instructions of F .
X
j=1
Summary Table
Table 3: Rules for calculating algorithm complexity with exact operation counts.
This table summarizes the main rules for calculating exact operation counts. Each
programming structure—whether a simple statement, a conditional, a loop, or a recursive
call—has a clear rule that can be applied directly. The examples illustrate how to translate
code into a cost expression. By practicing these rules, students can analyze any algorithm,
breaking it into parts and combining their costs systematically. Counting exact operations
is a valuable first step in algorithm analysis, because it shows precisely how each instruction
contributes to the total cost. However, these counts quickly become cumbersome and
depend on implementation details. To compare algorithms more generally, we often shift to
a higher-level abstraction: asymptotic complexity. Instead of writing 3n + 1 or 3n2 + n + 1,
we describe how costs grow as input size increases, using notations such as O(n) or O(n2 ).
The next section introduces this asymptotic approach, which provides a universal language
for reasoning about algorithm efficiency.
8 Asymptotic Complexity
Building precise mathematical models that capture every detail of a program’s execution is
extremely difficult, if not impossible. Different instructions may require different amounts
of time, and the control flow can vary widely depending on the input. Because of this,
determining the exact running time of an algorithm on all possible inputs is generally
infeasible. To overcome this limitation, computer scientists rely on asymptotic complexity
analysis. Rather than attempting to count every single operation, we model the running
time as a function of the input size parameter (commonly n) and study how this function
grows as n becomes large. This abstraction filters away machine- and language-dependent
constants while preserving the essential growth behavior that determines scalability. In
practice, asymptotic analysis enables meaningful comparisons of algorithms, regardless of
the hardware, compiler, or programming language. To make this idea concrete, consider
the classical problem of searching in a list. A linear search inspects elements one by one. If
the list size grows by a factor of 1000, the running time also grows by a factor of 1000—a
direct proportionality known as linear growth. By contrast, a binary search halves the
search space at each step, so increasing the list size by 1000× raises the running time by
only about log2 1000 ≈ 10 steps. This striking difference illustrates why orders of growth
are central in algorithm analysis: what may appear as a small difference in formulas quickly
becomes decisive at scale. The divergence between these growth rates, together with
others such as quadratic and exponential behavior, is illustrated in Figure 4. Because an
400
O(1)
O(log n)
O(n)
300
Growth of f (n)
O(n log n)
O(n2 )
O(2n )
200
100
0
2 4 6 8 10 12 14 16 18 20
Input size n
Figure 4: Comparison of common asymptotic growth rates. For small n, the curves
appear close, but as n grows, exponential and quadratic complexities quickly outpace
linear or logarithmic ones.
algorithm’s running time can vary with the input even for the same n, computer scientists
define three classical measures. The worst-case complexity corresponds to the maximum
number of steps an algorithm may take on any input of size n, providing a conservative
guarantee that performance will never be worse than this bound. For example, in linear
search, the worst case occurs when the target is absent, requiring n comparisons. The
best-case complexity, by contrast, corresponds to the minimum number of steps, describing
an ideal but rarely representative situation—for linear search, this is when the target is the
first element, requiring only one comparison. Finally, the average-case complexity gives
the expected number of steps over all inputs of size n, assuming a probability distribution;
in linear search, if the target is equally likely to be in any position, the expected number
of comparisons is (n + 1)/2. These three measures are summarized in Table 4, which
highlights the variability in performance across different scenarios.
While these measures highlight variability, analyzing algorithms at the level of exact
instruction counts is both impractical and uninformative. Different machines, compilers,
or coding styles can alter constants, and branching behavior makes precise predictions
infeasible. What truly matters is the order of growth: how running time increases as
input size becomes large. This perspective explains why algorithms with superficially
similar performance on small inputs can diverge drastically at scale. For example, while
a linear-time algorithm (O(n)) and a logarithmic-time algorithm (O(log n)) may appear
competitive for small n, Figure 4 shows that the logarithmic one will vastly outperform
the linear one as n grows. To connect theory to practice, Table 5 summarizes common
Because only the long-run trend is important, theoretical analyses describe running
times up to constant multiplicative factors. Thus, functions like 3n + 7 and 5n + 100 are
both considered “linear” since their growth rates are equivalent at scale. To formalize this
reasoning, computer scientists introduce the standard asymptotic notations: Big-O for
upper bounds, Ω for lower bounds, and Θ for tight bounds that serve as both upper and
lower. These notations constitute the common language of algorithm analysis, providing a
rigorous framework for comparing algorithms independently of implementation details and
for reasoning about scalability in a principled way. In the next section, we will formally
define these notations and examine their mathematical properties.
that f (n) = O(g(n)) if there exist constants C > 0 and n0 ∈ N such that |f (n)| ≤ C|g(n)|
for all n ≥ n0 . Intuitively, this means that beyond some point n0 , the function f (n)
never grows faster than a constant multiple of g(n). The symbol “=” in this expression
must not be misread as equality of functions; instead, it signifies membership: f belongs
to the class of functions asymptotically bounded by g. Thus, O(g(n)) denotes a family
of functions, not a single one. To make this concrete, consider a few examples. The
linear function f (n) = 3n + 2 satisfies f (n) ≤ 4n for all n ≥ 2, hence f (n) = O(n) with
C = 4 and n0 = 2. Similarly, f (n) = 100n is in O(n2 ) because, beyond n = 100, the
quadratic function dominates the linear one. In algorithmic practice, single assignments
or array accesses are constant-time operations, i.e., O(1); a simple loop over n elements
is O(n); and a nested double loop usually yields O(n2 ). From mathematics we also
know that for polynomials only the term of highest degree matters asymptotically: for
instance, f (n) = n2 + 500n + 1000 behaves like n2 for large n, so we write f (n) = O(n2 ).
This principle, sometimes called domination of the largest term, explains why complexity
analysis focuses on leading terms while ignoring constants and lower-order contributions.
Although many functions g can serve as an upper bound, we usually seek the tightest
bound to describe growth as accurately as possible. Saying that a linear function is O(n3 )
is technically correct but as uninformative as saying “this person’s age is less than a
thousand years.” The sharper description O(n) captures the true scale of growth and is
therefore preferred. This notion of tightness is critical when comparing algorithms, since
only the most accurate asymptotic characterization allows fair evaluation of their relative
efficiency. Big-O does not exist in isolation but is complemented by related notations.
Big-Ω provides an asymptotic lower bound, asserting that a function eventually grows
at least as fast as another. Big-Θ offers a tight bound, capturing both the upper and
lower constraints and thus characterizing the exact growth rate up to constant factors.
Finally, little-o notation, f (n) = o(g(n)), describes a strict upper bound, meaning f grows
strictly slower than g, i.e., limn→∞ f (n)/g(n) = 0. These notations together form the core
vocabulary of asymptotic analysis and are frequently used in proofs, complexity tables, and
theoretical discussions. The practical significance of asymptotic notation becomes evident
when comparing algorithms. A linear search through an unsorted list requires at most
O(n) comparisons, while binary search on a sorted list needs only O(log n) comparisons.
For small n, the difference is minor, but as shown in Table 6, logarithmic growth scales
vastly better than linear or quadratic growth. For very large datasets, such as national
identity databases containing millions of records, binary search outperforms linear search
by several orders of magnitude. Figure 5 further reinforces this point by visualizing the
divergence between different growth classes: logarithmic and linear curves rise slowly,
quadratic growth accelerates quickly, and exponential growth becomes infeasible almost
immediately.
Table 6: Growth of common complexity classes for increasing n. The contrast highlights
why logarithmic and linear algorithms scale better than quadratic or exponential ones.
100 log2 n
Relative growth (scaled)
n
80 n log2 n (scaled)
n2 (scaled)
60 2 n (compressed)
40
20
0
10 20 30 40 50 60 70 80 90 100
n
Figure 5: Contrasting asymptotic growth rates. Some curves are scaled to fit within the
same axes. Note how quickly quadratic and exponential functions dominate compared to
logarithmic and linear growth.
In summary, Big-O and its complementary notations provide a rigorous yet practical
framework for evaluating algorithm efficiency. By ignoring irrelevant constants and focusing
on dominant terms, they allow us to predict scalability, compare algorithms fairly, and
understand why certain approaches remain viable for massive inputs while others collapse
under growth. Together, Table 6 and Figure 5 illustrate the central lesson: asymptotic
analysis is not merely mathematical abstraction but an essential guide for algorithm design
and selection.
guidance about scalability but do not resolve close comparisons where constant factors
or small input sizes dominate; in those cases, empirical profiling or more detailed cost
models are indispensable. Moreover, Big-O represents an asymptotic upper bound and
is usually interpreted as a worst-case measure. Importantly, belonging to the same O
class does not imply equality: if f (n) = O(g(n)) and h(n) = O(g(n)), it does not follow
that f (n) = h(n). For instance, n2 + 2n = O(n2 ) and n3 + 20n + 50 = O(n3 ), but clearly
the two functions are not identical. The notation signals comparable growth trends, not
functional equivalence. The key algebraic properties of Big-O, which make it useful in
combining and simplifying complexity expressions, can be summarized as follows:
More generally, the largest term dominates a sum. For instance, O(n) + O(log n) =
O(n).
For example, O(n) · O(log n) = O(n log n). This explains why nested loops multiply
iteration counts.
3. Monotonicity. If g(n) ≤ h(n) for large n and f (n) = O(g(n)), then f (n) = O(h(n)).
Enlarging the bounding function cannot invalidate the relation.
Another important guideline concerns constants and tightness. Any fixed constant C is
O(1), since it does not grow with n. However, constants must not be confused with growth
rates: ignoring them is safe, but misusing the rules can lead to incorrect conclusions. For
example,
n
n(n + 1)(2n + 1)
i2 = = Θ(n3 ).
X
i=1 6
It would be wrong to argue that since each term i2 is O(n2 ), the entire sum must also be
O(n2 ). The accumulation over n terms increases the order of growth, and only by applying
proper summation identities do we obtain the correct cubic bound. A naive step such as
12 + 22 + · · · + n2 = O(max{12 , 22 , . . . , n2 }) = O(n2 )
for i = 1 to n do
A[i] = 0 // O(1) per iteration
end for
for j = 1 to n do
for k = 1 to n do
B[j][k] = j+k // O(1) per inner iteration
end for
end for
is misleading, because the true asymptotic is O(n3 ). This example illustrates why Big-O,
while abstracting details, still requires precise reasoning.
A short pseudocode fragment illustrates how the rules apply in practice:
The first loop executes n assignments, giving O(n). The nested double loop performs
n×n = n2 operations, contributing O(n2 ). By the sum rule, the total cost is O(n)+O(n2 ) =
O(n2 ). Within the nested structure, the product rule justifies multiplying the iteration
counts of the inner and outer loops. This systematic reasoning allows us to translate
raw iteration counts into meaningful asymptotic classes. Figure 6 illustrates the contrast
between sequential and nested loops: in the former, costs add and the largest term
dominates; in the latter, iteration counts multiply.
In summary, Big-O remains indispensable because it abstracts away machine-specific
details and focuses on scalability. But like any abstraction, it must be applied responsibly:
constants may be ignored but not confused with growth, summations must be evaluated
properly, and algebraic rules must be respected. With these guidelines in mind—and
supported by worked examples and visual intuition such as Figure 6—Big-O provides a
reliable framework for analyzing and comparing algorithms.
Arithmetic Sequence An arithmetic sequence is one in which two consecutive terms differ
by a constant value. For example, 1, 3, 5, 7, . . . is an arithmetic sequence with common
where a1 is the first term, d the common difference, and n the number of terms.
The sum of the first n terms is given by:
n(a1 + an )
Sn = , an = a1 + (n − 1)d.
2
Sum of Squares The sum of squares of the first n natural numbers is another frequently
used formula, especially when analyzing nested loops or quadratic-time algorithms:
n
n(n + 1)(2n + 1)
k2 =
X
.
k=1 6
a(rn − 1)
Sn = a + ar + ar2 + · · · + arn−1 = , (r ̸= 1).
r−1
By Stirling’s approximation,
√ n
n
n! ≈ 2πn ,
e
so,
log(n!) ≈ 21 log(2π) + 21 log(n) + n log(n) − n.
Floor and Ceiling Functions In algorithm design, indices or bounds often require integer
rounding. Two important functions help here:
• The floor function, ⌊x⌋, is the largest integer less than or equal to x.
• The ceiling function, ⌈x⌉, is the smallest integer greater than or equal to x.
Recurrence Relations Many recursive algorithms are analyzed using recurrence relations,
which define each value in terms of earlier ones. For example, factorial can be defined as:
1
n = 0 or 1,
n! =
n · (n − 1)!
otherwise.
Similarly, the running time of recursive algorithms often satisfies a recurrence such as:
b
n = 1 (base case),
T (n) =
aT (f (n)) + g(n)
otherwise.
In algorithm analysis, the two most important approaches are Repeated Substitution for
building intuition and the Master Theorem for handling divide-and-conquer algorithms.
We illustrate both below.
The repeated substitution (or iterative expansion) method replaces a recurrence with
smaller and smaller instances until reaching the base case. This reveals a pattern that can
then be expressed in closed form.
Substituting repeatedly:
This analysis can also be visualized using a recursion tree, shown in Figure 7. Each
level contributes cost n, and with log2 n levels, the total is n log n.
level 0: n
n
level 1: n
n n
2 2
level 2: n
n n n n
4 4 4 4 depth = log2 n
total = n log n
Figure 7: Recursion tree for T (n) = 2T (n/2) + n. Each level costs n, and there are log2 n
levels, giving T (n) = Θ(n log n).
n
T (n) = aT + f (n),
b
where a ≥ 1 is the branching factor, b > 1 the reduction factor, and f (n) the cost of
dividing and combining. Let nc = nlogb a . Three cases arise:
1. Recursion Dominates: If f (n) = O(n1−ε
c ), then T (n) = Θ(nc ).
2. Balanced Case: If f (n) = Θ(nc logk n), then T (n) = Θ(nc logk+1 n).
Comparison of Methods
For reference, Table 7 summarizes the key solving methods, their applicability, and
canonical examples.
Summary. Recurrence relations capture the cost of recursive algorithms, and mastering
their solutions is central to algorithm analysis. Substitution and recursion trees provide
intuition and exact solutions for basic recurrences, while the Master Theorem generalizes
to a broad class of divide-and-conquer algorithms. Together with change of variables,
induction, and linear methods, these tools equip us to analyze the complexity of nearly
any recursive process.
1. Simple statements
2. Sequences of statements
4. Loops
T (n) = O(1).
• Take the maximum of the branch costs (since only one path runs).
Example:
if (condition) {
O(n)
} else {
O(n^2)
}
Here: n
n(n + 1)
T (n) = i= = Θ(n2 ).
X
i=1 2
2. Solve the recurrence via repeated substitution, recursion trees, or the Master Theo-
rem.
Example 1: Fibonacci
F(n) {
if (n <= 1) return n;
return F(n-1) + F(n-2);
}
This gives
T (n) = T (n − 1) + T (n − 2) + O(1) = O(2n ).
These structural rules, combined with the summary table (Table 8), provide both intuition
and a quick reference for analyzing algorithms. They allow us to translate raw code into
asymptotic behavior, highlighting scalability without being distracted by constant factors
or machine-specific details.
10 Summary
Algorithm analysis is fundamental to computer science, providing the theoretical framework
to evaluate and compare algorithmic solutions. Through complexity analysis, we can:
Big O Notation: Provides asymptotic upper bounds on runtime growth with respect to
input size.
These methods form the foundation for designing efficient algorithms that scale grace-
fully with problem size. Whether creating new solutions or choosing among existing ones,
complexity analysis provides the rigorous framework for informed engineering decisions.
11 Exercises
1. Step counting (loops). Consider the pseudocode:
s <- 0
for i <- 1 to n do
for j <- 1 to i do
s <- s + 1
2. Asymptotic classification. For each function, give the tightest Big-O class and justify:
√
(i) 7n2 + 3n log n + 100 (ii) n log n + n/ log n (iii) 2n + n3 (iv) n log3 n (v) nlog2 7
3. Logarithm rules and equivalences. Prove or disprove each statement: (a) log(nk ) ∈
Θ(log n) for any fixed k > 0. (b) loga n ∈ Θ(logb n) for any a, b > 1. (c) log n! ∈
Θ(n log n).
4. Dominance via limits. Let f (n) = n log n and g(n) = n1.1 . Use limits to decide
whether f ∈ o(g), f ∈ ω(g), or f ∈ Θ(g). Explain the general method.
6. Recurrence II (Master Theorem). Give tight bounds (and identify the Master
case if applicable): (a) T (n) = 3T (n/3) + n log n (b) T (n) = 4T (n/2) + n (c)
T (n) = 8T (n/2) + n2 (d) T (n) = 2T (n/2) + n/ log n
7. Recursion tree method. Use a recursion tree to derive T (n) for T (n) = T (n/2) +
T (n/4) + n (assume n is a power of 4). Summarize level costs and total.
9. Binary search analysis. (a) Prove that binary search does at most ⌊log2 n⌋ + 1
comparisons in the worst case. (b) Show that the recursion depth is O(log n) for the
recursive version.
10. Transitivity and counterexamples. (a) Prove: if f ∈ O(g) and g ∈ O(h) then
f ∈ O(h). (b) Give a counterexample to: if f ∈ O(g) then g ∈ Ω(f ) with the same
hidden constant.
11. Little-o vs Big-O. For each claim, decide true/false and justify: (a) If f ∈ o(g)
then f ∈ O(g). (b) If f ∈ O(g) then f ∈ o(g). (c) If f ∈ Θ(g) then f ∈ O(g) and
f ∈ Ω(g).
12. Amortized analysis (dynamic array). Consider a dynamic array that doubles its
capacity when full; append(x) inserts at the end. (a) Show that the amortized cost
of append is O(1) over any sequence of m appends. (b) Explain one-shot expensive
operations and why amortized ̸= worst-case per operation.