CSE408Semester 4 • 3 CreditsCore Subjects

Design And Analysis Of Algorithms

This course covers the basic techniques of analyzing algorithms using space and time complexity, asymptotic notations, and various algorithm design techniques such as brute force, divide and conquer, dynamic programming, and greedy algorithms. Students will also learn string matching algorithms, backtracking, branch and bound techniques, and basic concepts of complexity classes.

Syllabus PDF
6 Units • Notes • Model Answers • MCQs
Select Course Unit (6 Units)
Active: Unit 1
Unit 1:Unit 1: Foundations of Algorithm
30 min read•High-Yield Unit Guide

Unit 1: Foundations of Algorithm

Unit 1 Exam Focus & Core Takeaways

Review the core definitions, standard architectures, and syntax patterns below. Pay special attention to sections highlighted with bullet points for your mid-term and end-term subjective questions.

Unit 1: Foundations of Algorithm — Comprehensive Study Notes

1. Algorithms & Knuth's Formal Foundations

An Algorithm is an unambiguous, step-by-step computational procedure that takes a valid set of inputs and transforms them into a well-defined set of outputs within a finite amount of time. In the foundational terminology established by Donald E. Knuth in The Art of Computer Programming, every legitimate algorithm must rigorously satisfy five fundamental criteria:

CODE
+-------------------------------------------------------------------------+
|                  KNUTH'S FIVE FOUNDATIONAL CRITERIA                     |
|                                                                         |
|  1. FINITENESS    : Must terminate after a finite number of operations. |
|  2. DEFINITENESS  : Every instruction must be unambiguous and clear.    |
|  3. INPUT         : Zero or more valid quantities externally supplied.  |
|  4. OUTPUT        : One or more outputs mathematically related to input.|
|  5. EFFECTIVENESS : Operations must be basic enough to be done exactly  |
|                     in finite time with pencil and paper.               |
+-------------------------------------------------------------------------+

1.1 Program vs. Algorithm

A Program is the concrete implementation of an algorithm written in a specific programming language (such as C, Java, or Python) tied to compiler syntax and machine architectures. An Algorithm, by contrast, is an abstract, mathematical specification independent of hardware, memory limits, operating system, or syntax. An operating system kernel or server daemon that runs indefinitely without terminating is a computational process or program, but strictly speaking not an algorithm under Knuth's finiteness constraint.

1.2 Algorithmic Problem Solving Lifecycle

The standard methodology for developing efficient computational solutions involves systematic phases:

  1. Problem Formulation: Precise specification of domain constraints, valid input sets, and required output predicates.
  2. Computational Model Selection: Adopting an abstract hardware execution model—predominantly the Random Access Machine (RAM) model, where operations (arithmetic, logic, memory dereference) execute in uniform unit time O(1)O(1).
  3. Algorithmic Strategy Selection: Choosing an appropriate paradigm (Divide-and-Conquer, Greedy, Dynamic Programming).
  4. Data Structure Design: Coupling the algorithm with optimal representations (Heaps, Balanced BSTs, Hash Maps, Disjoint Sets).
  5. Formal Correctness Proof: Proving that the algorithm terminates and produces mathematically correct outputs for all valid inputs using Loop Invariants and Mathematical Induction.
  6. Complexity Analysis: Evaluating asymptotic time and space upper/lower bounds.
  7. Implementation & Empirical Profiling: Translating into robust code and bench-testing across synthetic and adversarial test suites.

2. Loop Invariants & Mathematical Proof of Correctness

To prove that an iterative algorithm computes the correct result, computer scientists utilize Loop Invariants. A loop invariant is a formal logical predicate that remains true prior to the loop, across every iteration, and upon loop termination.

CODE
+-------------------------------------------------------------------------+
|                      LOOP INVARIANT INDUCTION STEPS                     |
|                                                                         |
|  1. INITIALIZATION : Invariant is TRUE prior to the first iteration.    |
|  2. MAINTENANCE    : If TRUE before an iteration, it remains TRUE       |
|                      before the subsequent iteration (Inductive Step).  |
|  3. TERMINATION    : When the loop terminates, the invariant provides a |
|                      useful property showing that the algorithm is      |
|                      mathematically correct.                            |
+-------------------------------------------------------------------------+

2.1 Case Study: Insertion Sort Invariant Proof

Consider sorting an array A[1…n]A[1 \dots n]. In Insertion Sort, the outer loop advances an index jj from 22 to nn:

  • Loop Invariant: At the start of each iteration of the outer for loop, the subarray A[1…j−1]A[1 \dots j-1] consists of the elements originally in A[1…j−1]A[1 \dots j-1], but in sorted order.
  • Initialization: Prior to the first iteration (j=2j = 2), the subarray A[1…j−1]A[1 \dots j-1] is simply A[1]A[1], which contains a single element. A single-element array is trivially sorted. Invariant holds.
  • Maintenance: The body of the outer loop moves A[j−1],A[j−2],…A[j-1], A[j-2], \dots to the right until the proper position for A[j]A[j] is identified, inserting the key. Subarray A[1…j]A[1 \dots j] now contains the original elements in sorted order. Incrementing jj restores the invariant for the next iteration.
  • Termination: The loop terminates when j>nj > n, meaning j=n+1j = n + 1. Substituting j=n+1j = n + 1 into the invariant statement yields: the subarray A[1…n]A[1 \dots n] consists of the original elements in fully sorted order. Hence, the algorithm is proven correct.

3. Asymptotic Analysis & Formal Notations

Asymptotic analysis evaluates how the resource consumption (execution time or memory space) of an algorithm scales as the input size nn approaches infinity (n→∞n \to \infty). It abstracts away hardware-dependent clock frequencies, operating system scheduling, compiler optimizations, and constant factors.

CODE
             ASYMPTOTIC BOUNDS VISUALIZATION
   f(n)
    ^
    |                                   c2 * g(n)  [Upper Bound]
    |                                 /
    |                   f(n)        /
    |                     \       /
    |                       \   /
    |                        X
    |                      /   \
    |                    /       \      c1 * g(n)  [Lower Bound]
    |                  /           \  /
    |                 /             /
    |                /            /
    +---------------+------------+-------------------------> n
    0               n0           n

3.1 Formal Mathematical Definitions

1. Big-O Notation (OO): Asymptotic Upper Bound

O(g(n))={f(n):∃ positive constants c>0 and n0>0 such that 0≤f(n)≤c⋅g(n) for all n≥n0}O(g(n)) = \{ f(n) : \exists \text{ positive constants } c > 0 \text{ and } n_0 > 0 \text{ such that } 0 \le f(n) \le c \cdot g(n) \text{ for all } n \ge n_0 \}
Big-O characterizes the worst-case growth rate; f(n)f(n) grows no faster than g(n)g(n) for sufficiently large nn.

2. Big-Omega Notation (Ω\Omega): Asymptotic Lower Bound

Ω(g(n))={f(n):∃ positive constants c>0 and n0>0 such that 0≤c⋅g(n)≤f(n) for all n≥n0}\Omega(g(n)) = \{ f(n) : \exists \text{ positive constants } c > 0 \text{ and } n_0 > 0 \text{ such that } 0 \le c \cdot g(n) \le f(n) \text{ for all } n \ge n_0 \}
Big-Omega establishes the minimum resources required; f(n)f(n) grows at least as fast as g(n)g(n).

3. Big-Theta Notation (Θ\Theta): Asymptotically Tight Bound

Θ(g(n))={f(n):∃ positive constants c1>0,c2>0, and n0>0 such that 0≤c1⋅g(n)≤f(n)≤c2⋅g(n) for all n≥n0}\Theta(g(n)) = \{ f(n) : \exists \text{ positive constants } c_1 > 0, c_2 > 0, \text{ and } n_0 > 0 \text{ such that } 0 \le c_1 \cdot g(n) \le f(n) \le c_2 \cdot g(n) \text{ for all } n \ge n_0 \}
Theorem: f(n)=Θ(g(n))  ⟺  f(n)=O(g(n)) and f(n)=Ω(g(n))f(n) = \Theta(g(n)) \iff f(n) = O(g(n)) \text{ and } f(n) = \Omega(g(n)).

4. Little-o Notation (oo): Strict Asymptotic Upper Bound

o(g(n))={f(n):∀c>0,∃n0>0 such that 0≤f(n)<c⋅g(n) for all n≥n0}o(g(n)) = \{ f(n) : \forall c > 0, \exists n_0 > 0 \text{ such that } 0 \le f(n) < c \cdot g(n) \text{ for all } n \ge n_0 \}
Equivalently: lim⁡n→∞f(n)g(n)=0\lim_{n \to \infty} \frac{f(n)}{g(n)} = 0. f(n)f(n) becomes insignificant relative to g(n)g(n) as n→∞n \to \infty.

5. Little-omega Notation (ω\omega): Strict Asymptotic Lower Bound

ω(g(n))={f(n):∀c>0,∃n0>0 such that 0≤c⋅g(n)<f(n) for all n≥n0}\omega(g(n)) = \{ f(n) : \forall c > 0, \exists n_0 > 0 \text{ such that } 0 \le c \cdot g(n) < f(n) \text{ for all } n \ge n_0 \}
Equivalently: lim⁡n→∞f(n)g(n)=∞\lim_{n \to \infty} \frac{f(n)}{g(n)} = \infty.

3.2 Limit Quotient Rule for Comparing Orders of Growth

To compare the asymptotic orders of two functions f(n)f(n) and g(n)g(n), evaluate the limit of their ratio using L'Hôpital's Rule where appropriate:
L=lim⁡n→∞f(n)g(n)L = \lim_{n \to \infty} \frac{f(n)}{g(n)}

  • If L=0  ⟹  f(n)=o(g(n))L = 0 \implies f(n) = o(g(n)) and f(n)=O(g(n))f(n) = O(g(n)).
  • If L=cL = c (where 0<c<∞0 < c < \infty)   ⟹  f(n)=Θ(g(n))\implies f(n) = \Theta(g(n)).
  • If L=∞  ⟹  f(n)=ω(g(n))L = \infty \implies f(n) = \omega(g(n)) and f(n)=Ω(g(n))f(n) = \Omega(g(n)).
  • If the limit does not exist and oscillates (e.g., f(n)=n1+sin⁡nf(n) = n^{1 + \sin n}), the functions are asymptotically incomparable.

4. Hierarchy of Fundamental Efficiency Classes

In algorithm design, standard complexity functions form a strict ascending order of growth:
1≺log⁡log⁡n≺log⁡n≺n≺n≺nlog⁡n≺n2≺n3≺2n≺en≺n!≺nn1 \prec \log \log n \prec \log n \prec \sqrt{n} \prec n \prec n \log n \prec n^2 \prec n^3 \prec 2^n \prec e^n \prec n! \prec n^n

Efficiency ClassNotationPractical Implications & Canonical Algorithms
ConstantO(1)O(1)Direct array indexing, push/pop on stack, hash table lookup (average).
Double LogarithmicO(log⁡log⁡n)O(\log \log n)Interpolation search on uniformly distributed data, van Emde Boas trees.
LogarithmicO(log⁡n)O(\log n)Binary search, BST search, balanced tree insertions.
Sublinear / FractionalO(n)O(\sqrt{n})Square root decomposition, trial division primality testing.
LinearO(n)O(n)Linear scan, counting sort, Kadane's algorithm, tree traversals.
LinearithmicO(nlog⁡n)O(n \log n)Merge sort, Heap sort, Quick sort (average), Fast Fourier Transform (FFT).
QuadraticO(n2)O(n^2)Bubble sort, Selection sort, Insertion sort, all-pairs comparisons.
CubicO(n3)O(n^3)Standard matrix multiplication, Floyd-Warshall all-pairs shortest paths.
PolynomialO(nk)O(n^k)High-order polynomial optimization, interior point linear programming.
ExponentialO(2n)O(2^n)Exact 0/1 Knapsack brute force, Traveling Salesperson brute force.
FactorialO(n!)O(n!)Permutation generation, brute-force graph isomorphism.

5. Recurrence Relations & Divide-and-Conquer Analysis

Divide-and-conquer algorithms divide a problem of size nn into aa subproblems, each of size n/bn/b, solve them recursively, and combine their solutions in f(n)f(n) time:
T(n)=aT(n/b)+f(n)T(n) = a T(n/b) + f(n)

5.1 The Master Theorem (CLRS Formulation)

Let a≥1a \ge 1 and b>1b > 1 be constants, and let f(n)f(n) be an asymptotically positive function. Compare f(n)f(n) against the watershed function nlog⁡ban^{\log_b a}:

CODE
+----------------------------------------------------------------------------------------------------+
|                                    MASTER THEOREM (THREE CASES)                                    |
|                                                                                                    |
|  CASE 1: If f(n) = O(n^(log_b(a) - epsilon)) for some constant epsilon > 0,                        |
|          then T(n) = Theta(n^(log_b a)).  [Leaves dominate computational cost]                     |
|                                                                                                    |
|  CASE 2: If f(n) = Theta(n^(log_b a) * log^k(n)) for some k >= 0,                                  |
|          then T(n) = Theta(n^(log_b a) * log^(k+1)(n)).  [Work distributed evenly across levels]  |
|                                                                                                    |
|  CASE 3: If f(n) = Omega(n^(log_b(a) + epsilon)) for some constant epsilon > 0,                     |
|          AND the Regularity Condition holds: a * f(n/b) <= c * f(n) for some c < 1 and large n,   |
|          then T(n) = Theta(f(n)).  [Root / combination step dominates computational cost]          |
+----------------------------------------------------------------------------------------------------+

Detailed Examples:

  1. Merge Sort: T(n)=2T(n/2)+Θ(n)T(n) = 2T(n/2) + \Theta(n)
    • a=2,b=2  ⟹  nlog⁡22=n1=na = 2, b = 2 \implies n^{\log_2 2} = n^1 = n.
    • f(n)=Θ(n)  ⟹  f(n) = \Theta(n) \implies Case 2 with k=0k = 0.
    • Solution: T(n)=Θ(nlog⁡n)T(n) = \Theta(n \log n).
  2. Strassen's Matrix Multiplication: T(n)=7T(n/2)+Θ(n2)T(n) = 7T(n/2) + \Theta(n^2)
    • a=7,b=2  ⟹  nlog⁡27≈n2.807a = 7, b = 2 \implies n^{\log_2 7} \approx n^{2.807}.
    • f(n)=n2=O(n2.807−ϵ)f(n) = n^2 = O(n^{2.807 - \epsilon}) with ϵ≈0.807  ⟹  \epsilon \approx 0.807 \implies Case 1.
    • Solution: T(n)=Θ(nlog⁡27)≈Θ(n2.81)T(n) = \Theta(n^{\log_2 7}) \approx \Theta(n^{2.81}).
  3. Binary Search: T(n)=T(n/2)+Θ(1)T(n) = T(n/2) + \Theta(1)
    • a=1,b=2  ⟹  nlog⁡21=n0=1a = 1, b = 2 \implies n^{\log_2 1} = n^0 = 1.
    • f(n)=Θ(1)=Θ(n0)  ⟹  f(n) = \Theta(1) = \Theta(n^0) \implies Case 2 with k=0k = 0.
    • Solution: T(n)=Θ(log⁡n)T(n) = \Theta(\log n).

5.2 Pitfalls of the Master Theorem

The Master Theorem does not apply if:

  • T(n)=2T(n/2)+nlog⁡nT(n) = 2T(n/2) + n \log n: Here f(n)f(n) is larger than nlog⁡22=nn^{\log_2 2} = n, but not polynomially larger (it lacks the factor nϵn^\epsilon). Solved using recursion trees: Θ(nlog⁡2n)\Theta(n \log^2 n).
  • T(n)=2T(n/2)+nlog⁡nT(n) = 2T(n/2) + \frac{n}{\log n}: f(n)f(n) is smaller by a logarithmic factor, not polynomial n−ϵn^{-\epsilon}. Solved using substitution: Θ(nlog⁡log⁡n)\Theta(n \log \log n).
  • a<1a < 1 or b≤1b \le 1.
  • Non-constant subproblem sizes (e.g., T(n)=T(n/3)+T(2n/3)+nT(n) = T(n/3) + T(2n/3) + n), which require the Akra-Bazzi method or recursion trees.

6. Amortized Analysis Techniques

In many stateful data structures, an occasional operation is very expensive, but the majority of operations are extremely fast. Amortized Analysis guarantees the average performance of each operation over a worst-case sequence of operations without probabilistic assumptions.

CODE
+-------------------------------------------------------------------------+
|                  THREE METHODS OF AMORTIZED ANALYSIS                    |
|                                                                         |
|  1. AGGREGATE METHOD : Show that a sequence of n operations takes       |
|                        T(n) worst-case time in total.                   |
|                        Amortized cost per operation = T(n) / n.         |
|                                                                         |
|  2. ACCOUNTING METHOD: Assign artificial charges (amortized costs)      |
|                        to each operation. Overcharges are stored as     |
|                        credit to pay for future expensive operations.   |
|                        Credit must never drop below zero!               |
|                                                                         |
|  3. POTENTIAL METHOD : Define a potential function Phi(D) mapping data  |
|                        structure states to real numbers.                |
|                        Amortized cost: a_i = c_i + Phi(D_i) - Phi(D_i-1)|
+-------------------------------------------------------------------------+

6.1 Case Study: Dynamic Table Expansion

Consider a dynamic array that doubles in capacity whenever an insertion occurs in a full table.

  • Cost: If table is not full, cost is 11 (insert element). If full (capacity mm), allocating array of size 2m2m, copying mm elements, and inserting takes m+1m + 1 operations.
  • Accounting Method: Assign an amortized cost of \3$ to each insertion:
    • \1$ pays for inserting the element into the current slot.
    • \1$ pays for moving this element when the table doubles in the future.
    • \1paysformovingoneoftheolderelementsthatalreadyusedupitscredit.Whencapacitydoublesfrompays for moving one of the older elements that already used up its credit. When capacity doubles frommtoto2m,exactly, exactly minsertionshaveoccurredsincethelastdoubling,accumulatinginsertions have occurred since the last doubling, accumulating$2mincredit—preciselyenoughtopayforcopyingallin credit—precisely enough to pay for copying allmelements!Hence,amortizedcostperinsertionisstrictlyelements! Hence, amortized cost per insertion is strictlyO(1)$.

7. Fundamental Data Structures & Asymptotic Complexities

Data StructureAccess (Avg/Worst)Search (Avg/Worst)Insertion (Avg/Worst)Deletion (Avg/Worst)Space Complexity
ArrayΘ(1)/Θ(1)\Theta(1) / \Theta(1)Θ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(n)\Theta(n)
Singly Linked ListΘ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(1)/Θ(1)\Theta(1) / \Theta(1)*Θ(1)/Θ(1)\Theta(1) / \Theta(1)*Θ(n)\Theta(n)
Stack / QueueΘ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(1)/Θ(1)\Theta(1) / \Theta(1)Θ(1)/Θ(1)\Theta(1) / \Theta(1)Θ(n)\Theta(n)
Binary Search TreeΘ(log⁡n)/Θ(n)\Theta(\log n) / \Theta(n)Θ(log⁡n)/Θ(n)\Theta(\log n) / \Theta(n)Θ(log⁡n)/Θ(n)\Theta(\log n) / \Theta(n)Θ(log⁡n)/Θ(n)\Theta(\log n) / \Theta(n)Θ(n)\Theta(n)
AVL / Red-Black TreeΘ(log⁡n)/Θ(log⁡n)\Theta(\log n) / \Theta(\log n)Θ(log⁡n)/Θ(log⁡n)\Theta(\log n) / \Theta(\log n)Θ(log⁡n)/Θ(log⁡n)\Theta(\log n) / \Theta(\log n)Θ(log⁡n)/Θ(log⁡n)\Theta(\log n) / \Theta(\log n)Θ(n)\Theta(n)
Binary HeapΘ(1)\Theta(1) (min/max)Θ(n)/Θ(n)\Theta(n) / \Theta(n)Θ(log⁡n)/Θ(log⁡n)\Theta(\log n) / \Theta(\log n)Θ(log⁡n)/Θ(log⁡n)\Theta(\log n) / \Theta(\log n)Θ(n)\Theta(n)
Hash TableN/A\text{N/A}Θ(1)/Θ(n)\Theta(1) / \Theta(n)Θ(1)/Θ(n)\Theta(1) / \Theta(n)Θ(1)/Θ(n)\Theta(1) / \Theta(n)Θ(n)\Theta(n)
Disjoint Set (DSU)N/A\text{N/A}Θ(α(n))\Theta(\alpha(n))Θ(α(n))\Theta(\alpha(n))N/A\text{N/A}Θ(n)\Theta(n)

*Insertion and deletion at head or with known pointer.