Chapter 03

Analyzing Algorithms

This chapter develops the machinery for analysing algorithms rigorously: proving correctness with loop invariants, the assumptions behind our cost model, and computing best-, worst-, and average-case running times. We apply these tools to insertion sort, linear search, binary search (introducing recurrence relations along the way), and selection sort.

3.1Insertion Sort

Insertion sort works the way many people sort playing cards: keep the left part of the array sorted, take the next element (the key), and insert it into its correct position by shifting larger elements one place to the right.

Insertion sort as a hand of cards sorted hand + next card
2♣2♣
4♠4♠
5♥5♥
6♦6♦
3♥3♥
Fig 3.1 · Sorting a hand of playing cards: the cards already in hand stay sorted, and each new card is slid into its correct place — exactly what insertion sort does.
INSERTION-SORT(A, n)
1. for i = 2 to n
2.     key = A[i]
3.     // Insert A[i] into the sorted subarray A[1 : i−1]
4.     j = i − 1
5.     while j > 0 and A[j] > key
6.         A[j + 1] = A[j]
7.         j = j − 1
8.     A[j + 1] = key
Complete pass trace A = [5, 2, 4, 6, 1, 3]
start[5] 2 4 6 1 3
insert 2[2, 5] 4 6 1 3
insert 4[2, 4, 5] 6 1 3
insert 6[2, 4, 5, 6] 1 3
insert 1[1, 2, 4, 5, 6] 3
insert 3[1, 2, 3, 4, 5, 6]
Fig 3.2 · Trace on A = [5, 2, 4, 6, 1, 3] (from the lecture): at each step the key (highlighted) is inserted into the sorted prefix.
Watch it: insertion sort, step by step
A = [5, 2, 4, 6, 1, 3]. Use Step to move one action at a time, or Play to run it; the narration below the bars explains every comparison and shift.
sorted prefix key being placed shifting right not yet reached
Press Play or Step to start.
Invariant idea At the start of each iteration of the for loop, the subarray A[1 : i−1] holds the elements originally in A[1 : i−1], but in sorted order.

3.2Correctness: The Loop Invariant Technique

To prove the correctness of algorithms, we typically use the loop invariant technique. Using a loop invariant, we show three things:

  • Initialization — the invariant holds true before the loop starts.
  • Maintenance — it remains true after every iteration of the loop.
  • Termination — when the loop finishes, the invariant helps prove that the algorithm is correct.

Applying it to insertion sort

StepArgument
1. Initialization
(base case)
Before the first iteration (i = 2), A[1] is trivially sorted — a single element. So the invariant holds.
2. Maintenance
(inductive step)
Assume A[1 : i−1] is sorted before iteration i. The algorithm inserts A[i] (the key) into its correct position in A[1 : i] by shifting larger elements right. After insertion, A[1 : i] is sorted — the invariant is preserved.
3. Termination The loop ends at i = n + 1. At this point the invariant tells us that A[1 : n] — the full array — is sorted.

3.3Key Assumptions in Algorithm Analysis

AssumptionMeaning
1. Sequential execution Instructions are executed one after another; no concurrent operations allowed.
2. Equal cost for similar instructions Data access or updating a variable of a given data structure takes constant time.
3. No unrealistic assumptions For example, we cannot assume sorting an array takes constant time.
4. Ignoring precision & word size Floating-point precision is ignored; values may be big but not infinite; the word size is enough to hold input values and intermediate results.
5. Memory access assumption The memory hierarchy (caches, RAM vs disk) is ignored.

3.4What is Input Size?

The right notion of "input size" depends on the problem:

  • Sorting — determined by the number of elements in the array or list.
  • Graph problems — defined by the number of vertices (V) or edges (E).
  • Multiplication — depends on the number of bits required to represent the two numbers.

3.5Calculating Running Time

Definition · Running time The running time of an algorithm is the sum of the running times of all the individual statements it executes.

Let a statement take ck steps (CPU cycles) to execute. If it is executed m times, its total contribution is:

Formula T(n) = ck · m
Note ck is a hypothetical constant, not an actual clock time. To generalise the time-complexity formula for insertion sort, we analyse the algorithm line by line, multiplying each line's cost by the number of times it runs, and summing.
Statement cost × execution count line-by-line model
LineStatementCostTimes executed
1for i = 2 to nc₁n
2key = A[i]c₂n − 1
3insert into sorted prefix0n − 1
4j = i − 1c₄n − 1
5while j > 0 and A[j] > keyc₅Σ tᵢ
6A[j + 1] = A[j]c₆Σ (tᵢ − 1)
7j = j − 1c₇Σ (tᵢ − 1)
8A[j + 1] = keyc₈n − 1
Fig 3.3 · Line-by-line analysis of insertion sort: each line has a cost cₖ and a count of how many times it executes. tₙ is the number of while-loop tests for a given i.
General running-time expression sum of every contribution

T(n) = c₁n + c₂(n − 1) + c₄(n − 1)

+ c₅Σi=2n tᵢ

+ c₆Σi=2n (tᵢ − 1)

+ c₇Σi=2n (tᵢ − 1) + c₈(n − 1)

Fig 3.4 · Summing every line’s contribution gives the general running-time expression T(n). The Σtₙ terms are what change between best and worst case.

3.6Insertion Sort: Best, Worst, and Average Case

Best case — array already sorted

The while-loop condition A[j] > key fails immediately in every iteration, so no shifting happens: every tₙ = 1.

show answer
linear in n

tᵢ = 1 for every i

Σ(tᵢ − 1) = 0

Tbest(n) = an + b = Θ(n)

Fig 3.5 · With tₙ = 1, the sum terms vanish and T(n) collapses to the form an + b — a linear function of n.
Worst case — array sorted in reverse

Every key must be compared with, and shifted past, all elements to its left, so every tₙ = i: the while loop runs the maximum possible number of times.

show answer
quadratic in n

tᵢ = i

Σi=2n i = n(n + 1)/2 − 1

Σi=2n (i − 1) = 1 + 2 + ⋯ + (n − 1) = n(n − 1)/2

Fig 3.6 · Substituting tₙ = i turns the sums into the arithmetic series 1 + 2 + … + (n−1) = n(n−1)/2.

Tworst(n) = c₁n + c₂(n − 1) + c₄(n − 1)

+ c₅[n(n + 1)/2 − 1]

+ (c₆ + c₇)[n(n − 1)/2] + c₈(n − 1)

Tworst(n) = an² + bn + c = Θ(n²)

Fig 3.7 · Collecting terms gives T(n) in the form an² + bn + c — a quadratic function of n.
Average case — random input

How long does it take to find where in A[1 : i−1] to insert A[i] for a random instance?

show answer
quadratic in n

On average, half the elements in A[1 : i−1] are greater than A[i], so tₙ ≈ i/2 — about half the worst-case shifts. Halving the sum only halves the constant, so the resulting average-case running time is still a quadratic function of the input size, just like the worst case.

Note We usually consider one algorithm more efficient than another if its worst-case running time has a lower order of growth.

3.7Asymptotic Behaviour of Running-Time Expressions

Example — which term wins for large n?
T(n) = n²/100 + 100n + 7
show answer
n² dominates

The n²/100 term dominates when n is much larger than 10,000, and lower-order terms become negligible for large n. This is why analysis focuses on the fastest-growing term — the idea behind asymptotic notation, coming in the next chapter.

3.8Linear Search

Definition · Linear search Linear search is a straightforward algorithm to find a target element (key) in a list. It checks each element sequentially until the target is found or the list ends.

Algorithm steps

  1. Start from the first element of the array.
  2. Compare the key with the current element.
  3. If the key matches the current element, return its index.
  4. If it does not match: if the list has not ended, move to the next element and go to step 2; else return −1.
LINEAR-SEARCH(A, x)
1. for i = 1 to n
2.     if A[i] == x
3.         return i
4. return NIL
Try it: linear search, one comparison at a time
Use the same lecture array. Choose a best-case, worst-case, or absent key and watch the scan move strictly from left to right.
Ready to inspect the first element.
comparisons: 0

3.9Linear Search: Case Analysis

Take arr = [3, 8, 7, 1, 9]:

CaseExampleComparisonsTime
Best case key = 3 (first element) 1 constant
Worst case key = 9 (last element or absent) 5 (= n) linear n
Average case key can be present anywhere (n + 1)/2 = all possible cases / total cases linear n

3.10Linear Search: Properties and Correctness

Key characteristics

  • Works on both sorted and unsorted arrays.
  • Time complexity varies based on the position of the target element.

Advantages

  • Simple and easy to implement.
  • Works on both sorted and unsorted arrays.
  • Requires no preprocessing of the array.

Disadvantages

  • Inefficient for large datasets.
  • Performance degrades linearly with the size of the array.

Correctness via loop invariant

Invariant: at the start of iteration i, x is not in A[1 : i−1].

StepArgument
Initialization Before the first iteration (i = 1), the subarray A[1 : 0] is empty, so the invariant holds.
Maintenance Assume the invariant holds at iteration i, so x ∉ A[1 : i−1]. If A[i] = x we return i (correct); else we move to i + 1 and the invariant is maintained.
Termination When the loop ends, all of A[1 : n] has been checked; the invariant ensures x is not in the array, so returning NIL is correct.

3.11Exercises

Questiontry it
  • Modify linear search to count occurrences of a target element.
  • Use linear search to find the largest / smallest element in an array.
  • What is the best-case, worst-case, and average-case execution time for the above problems?
show answer
all cases: n

Counting occurrences: the loop can never return early — every element must be checked even after a match — so best = worst = average = n comparisons.
Largest / smallest element: one pass keeping a running max (or min) always makes n − 1 comparisons, regardless of the input order — again best = worst = average ≈ n.
The lesson: early exit is what separated linear search’s best case (1) from its worst (n); remove the early exit and all three cases collapse together.

3.12Binary Search — Search Smart, Not Hard!

If the array is sorted, we can do much better than checking every element. Binary search keeps two pointers, low and high, computes the middle index mid = (low + high) / 2, and compares A[mid] with the key:

  • If A[mid] == key → return mid.
  • If A[mid] > key → discard the right half: high = mid − 1.
  • If A[mid] < key → discard the left half: low = mid + 1.

Tracing the lecture example — searching key = 33 in a 15-element sorted array: mid = (0+14)/2 = 7 gives A[7] = 53 > 33, so high = 6; mid = (0+6)/2 = 3 gives A[3] = 25 < 33, so low = 4; mid = (4+6)/2 = 5 gives A[5] = 43 > 33, so high = 4; finally mid = (4+4)/2 = 4 and A[4] = 33 — found.

Try it: binary search for key = 33
Amber = current mid being compared; faded = discarded half; teal = found.
comparisons: 0

3.13Binary Search: Algorithm

BinarySearch(array, key):
    1. Set low = 0
    2. Set high = length(array) − 1
    3. While low <= high:
        a. Set mid = low + (high − low) // 2
        b. If array[mid] == key:
               return mid
        c. Else if array[mid] < key:
               Set low = mid + 1
        d. Else:
               Set high = mid − 1
    4. return −1   // Target not found

3.14Binary Search: Execution Time Analysis

Each step halves the search space: n → n/2 → n/4 → … → 1. The number of halvings until one element remains is log n (compare the halving loop from Chapter 2: for (int i = n; 1 < i; i = i/2)).

As a recurrence relation

At each step, the algorithm divides the input into two halves, performs a single comparison, and decides whether to search the left or right half:

Recurrence T(n) = T(n/2) + c

T(n): time for input size n · T(n/2): time for the reduced input after one division · c: constant time for the comparison and midpoint calculation.

Solving by the substitution method

T(n) = T(n/2) + c
     = T(n/4) + c + c
     = T(n/8) + c + c + c
     ⋮
     = T(n/2ᵏ) + k·c  =  T(1) + k·c

where n/2ᵏ = 1, so k = log n

Cases

CaseWhenTime
BestThe target is found on the first comparison.O(1)
WorstThe search interval shrinks to size 1 (target absent or found at the last step).O(log n)
AverageThe element is found after searching half the depth of the search tree.O(log n)
Conclusion Binary search is an efficient divide-and-conquer algorithm with log n time, making it suitable for large datasets. Precondition: the array must be sorted.

3.15Selection Sort

Selection sort repeatedly finds the minimum element of the unsorted part and places it at the front:

SELECTION-SORT(A, n)
1. for i ← 0 to n − 2 do
2.     min_index ← i
3.     for j ← i + 1 to n − 1 do
4.         if A[j] < A[min_index] then
5.             min_index ← j
6.     end for
7.     if min_index ≠ i then
8.         swap A[i] and A[min_index]
9. end for
Watch it: selection sort, step by step
A = [5, 2, 4, 6, 1, 3]. Each pass scans the unsorted part, remembers its minimum, and swaps that value into the next position.
sorted prefix value being compared current minimum
Ready to find the minimum of the unsorted array.

Execution time as a recurrence

At each step the algorithm reduces the input size by one: it performs n−1 comparisons and puts the current minimum element in its correct position.

Recurrence T(n) = T(n−1) + c·(n−1)

T(n): time for input size n · T(n−1): time for the reduced input after one step · c: constant time per comparison.

Solving this recurrence (try expanding it like the binary search one!) gives c·[(n−1) + (n−2) + … + 1] = c·n(n−1)/2 — a quadratic function of n, regardless of the input order.