Expressing Algorithms Clearly
Tracing Time and Space During a Dry Run
Read aloud is unavailable in this browser.
Beyond Correctness: Measuring Efficiency on Paper
A correct algorithm is necessary—but not sufficient. An algorithm that produces the right answer after a hundred years of computation is useless in practice. Beyond verifying correctness, we need to understand how much time and space an algorithm consumes.
In earlier modules, we studied asymptotic complexity—Big-O notation for describing how algorithms scale. Now we connect that theoretical knowledge to practical reality by tracing complexity during dry runs.
When you trace an algorithm for correctness, you're asking: "Does this produce the right output?" When you trace for complexity, you're asking: "How many operations does this take? How much memory does this use?"
The same trace table technique applies—we simply add columns to count operations and track memory allocation.
By the end of this page, you will be able to count primitive operations during a dry run, track memory allocation and deallocation, identify which parts of an algorithm dominate the complexity, and verify that your theoretical complexity analysis matches the actual behavior.
What Operations to Count
When analyzing time complexity, we count primitive operations—basic computational steps that take constant time regardless of input size. The exact definition varies, but the following are universally counted:
- •Assignment — Storing a value in a variable (
x ← 5) - •Comparison — Testing a condition (
x < y,x = target) - •Arithmetic — Addition, subtraction, multiplication, division, modulo
- •Array access — Reading or writing
array[i] - •Return — Returning a value from a function
- •Function call overhead — The act of calling a function (not counting the function body)
- •Pointer/Reference operations — Dereferencing, following a link
What NOT to count:
- Loop structure itself (counted via iterations × body operations)
- Declaration of variables (counted when assigned)
- Comments or pseudocode structure
Simplification for asymptotic analysis:
For Big-O analysis, we care about growth rate, not exact counts. We can:
- Count "significant operations" (comparisons for searching, swaps for sorting)
- Ignore constant-factor differences between operations
- Focus on the dominant term (the term that grows fastest with n)
We assume the Random Access Machine (RAM) model: each primitive operation takes one time unit, memory access is constant time regardless of address. This is a simplification—real computers have caches, pipelines, and varying memory latency—but it's the foundation of theoretical analysis.
Time Complexity Trace Tables
To trace time complexity, we augment our trace table with an operation count column. We'll count operations step by step, then derive the total.
Example: Counting operations in linear search
12345678910
FUNCTION LinearSearch(array, target) // Line FOR i ← 0 TO length(array) - 1 DO // L1: initialization, comparison IF array[i] = target THEN // L2: array access, comparison RETURN i // L3: return END IF END FOR // L4: increment i, loop back RETURN -1 // L5: returnEND FUNCTION Input: array = [4, 2, 7, 1, 9], target = 7 (found at index 2)| Step | i | array[i] | Matches? | Operations | Op Count |
|---|---|---|---|---|---|
| Init | 0 | Assignment: i ← 0 | 1 | ||
| i=0 | 0 | 4 | No | Compare i ≤ 4: 1, Access array[0]: 1, Compare 4 = 7: 1, Increment i: 1 | 4 |
| i=1 | 1 | 2 | No | Compare i ≤ 4: 1, Access array[1]: 1, Compare 2 = 7: 1, Increment i: 1 | 4 |
| i=2 | 2 | 7 | Yes | Compare i ≤ 4: 1, Access array[2]: 1, Compare 7 = 7: 1, Return: 1 | 4 |
| Total | 13 operations |
Analysis:
- Total operations: 1 (init) + 4 × 3 iterations = 13
- Per iteration: ~4 operations (loop check, array access, comparison, increment or return)
- If target was not found, we'd have 1 + 4 × 5 + 1 = 22 operations (all 5 iterations + final return)
Deriving Big-O from the trace:
In general, for an array of size n:
- Best case (found at index 0): ~4 operations = O(1)
- Worst case (not found or at end): ~1 + 4n + 1 = 4n + 2 operations = O(n)
- Average case (found in middle): ~4(n/2) = 2n operations = O(n)
The trace confirms linear search is O(n) in the worst and average case.
For asymptotic analysis, you don't need to count every single operation. Focus on the dominant operations—usually comparisons for searching, comparisons/swaps for sorting. Count iterations and multiply by operations-per-iteration.
Tracing Nested Loop Complexity
Nested loops often produce O(n²) or higher complexity. Tracing them reveals exactly how operation counts grow.
Example: Bubble sort (one pass)
123456789101112
FUNCTION BubbleSort(array) n ← length(array) FOR i ← 0 TO n - 2 DO // Outer loop: n-1 passes FOR j ← 0 TO n - 2 - i DO // Inner loop: decreasing range IF array[j] > array[j + 1] THEN // Compare adjacent elements swap(array[j], array[j + 1]) // Swap if out of order END IF END FOR END FOREND FUNCTION Input: n = 5 (counting comparisons only)| Outer (i) | Inner Loop Range | Inner Iterations | Comparisons |
|---|---|---|---|
| 0 | j: 0 to 3 | 4 | 4 |
| 1 | j: 0 to 2 | 3 | 3 |
| 2 | j: 0 to 1 | 2 | 2 |
| 3 | j: 0 to 0 | 1 | 1 |
| Total | 4 + 3 + 2 + 1 | 10 |
Deriving the complexity:
Total comparisons = (n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2
For n = 5: 5 × 4 / 2 = 10 ✓ (matches our trace)
For general n: n(n-1)/2 = (n² - n)/2 ≈ n²/2 = O(n²)
The trace proves Bubble Sort is O(n²) and shows us exactly how the quadratic term arises—the inner loop runs n-1, then n-2, then n-3... times, creating the triangular sum.
Common nested loop patterns: n × n = O(n²), n × n × n = O(n³), n + (n/2) + (n/4) + ... = O(n), 1 + 2 + ... + n = O(n²). Recognizing these patterns from traces builds intuition for quickly estimating complexity.
Tracing Recursive Complexity
Recursive algorithms require tracking the number of calls and work done per call. The total work is (number of calls) × (work per call).
Example: Tracing binary search complexity
1234567891011121314151617
FUNCTION BinarySearch(array, target, low, high) IF low > high THEN RETURN -1 END IF mid ← (low + high) / 2 IF array[mid] = target THEN RETURN mid ELSE IF array[mid] < target THEN RETURN BinarySearch(array, target, mid + 1, high) ELSE RETURN BinarySearch(array, target, low, mid - 1) END IFEND FUNCTION Input: array of size n = 16, target not in array (worst case)| Call # | low | high | Range Size | Operations |
|---|---|---|---|---|
| 1 | 0 | 15 | 16 | ~5 (compare, compute mid, array access, compare, recurse) |
| 2 | 8 or 0 | 15 or 7 | 8 | ~5 |
| 3 | 4 | ~5 | ||
| 4 | 2 | ~5 | ||
| 5 | 1 | ~5 | ||
| 6 | 0 (low > high) | ~2 (base case) | ||
| Total | 5 × 5 + 2 ≈ 27 |
Deriving complexity from the trace:
Observations:
- Range size halves each call: 16 → 8 → 4 → 2 → 1 → 0
- Number of calls until base case: log₂(16) + 1 = 5 calls + 1 base case = 6 calls
- Each call does O(1) work (constant operations)
Total work: O(log n) calls × O(1) work/call = O(log n)
For general n: The range halves until it reaches 0, taking log₂(n) steps. Each step does constant work. Binary search is O(log n).
12345678910111213141516171819202122
FUNCTION Fib(n) IF n ≤ 1 THEN RETURN n RETURN Fib(n-1) + Fib(n-2)END FUNCTION Call tree for Fib(5): Fib(5) / \ Fib(4) Fib(3) / \ / \ Fib(3) Fib(2) Fib(2) Fib(1) / \ / \ / \ Fib(2) Fib(1) ... ... ... Counting calls:- Fib(5): 1 call- Fib(4): 1 call → Fib(3): 1 call, Fib(2): 1 call- Fib(3): 2 calls total → Fib(2): 2 calls, Fib(1): 2 calls- Many overlapping subproblems! Total calls for Fib(n) ≈ 2^n (exponential!)This is why naive recursive Fibonacci is O(2ⁿ).The Fibonacci trace reveals exponential call count—each call spawns two more. This is why naive Fibonacci is impractical for n > 40. The trace makes this explosion visible, motivating memoization or iteration.
Space Complexity Tracing
Space complexity tracks memory usage. We count:
- Variables — Each primitive variable uses O(1) space
- Data structures — Arrays of size n use O(n) space, dictionaries depend on entries
- Recursion stack — Each recursive call adds a stack frame; maximum depth determines space
- Temporary allocations — Arrays created during execution
Example: Tracing space in iterative vs recursive factorial
| Aspect | Iterative | Recursive |
|---|---|---|
| Variables | n, result, i = O(1) | n, return value = O(1) per call |
| Stack frames | 1 frame (main) | n frames (Factorial(n) → Factorial(1)) |
| Maximum space | O(1) | O(n) |
| Space growth | Constant | Linear in n |
123456789101112131415161718192021
Factorial(4) space trace: Call Stack (growing down):┌─────────────────┐│ Factorial(4) │ ← Frame 1: n=4, waiting├─────────────────┤│ Factorial(3) │ ← Frame 2: n=3, waiting├─────────────────┤│ Factorial(2) │ ← Frame 3: n=2, waiting├─────────────────┤│ Factorial(1) │ ← Frame 4: n=1, base case reached└─────────────────┘ Maximum stack depth: 4 frames for Factorial(4)In general: n frames for Factorial(n) = O(n) space After base case, frames pop and multiply:Factorial(1) returns 1 → Frame 4 removedFactorial(2) returns 2×1=2 → Frame 3 removedFactorial(3) returns 3×2=6 → Frame 2 removedFactorial(4) returns 4×6=24 → Frame 1 removedExample: Tracing auxiliary space in merge sort
1234567891011121314151617
Merge Sort on array of size n: Space components:1. Input array: O(n) - not counted as "extra" space2. Recursive stack depth: O(log n) - halving each level3. Temporary arrays during merge: O(n) at each level [8 elements] ← Level 0: one array of n / \ [4 elem] [4 elem] ← Level 1: two arrays of n/2 (total n) / \ / \ [2] [2] [2] [2] ← Level 2: four arrays of n/4 (total n) At each level, total temporary space = O(n)But arrays can be reused/deallocated, so maximum at once = O(n) Total auxiliary space: O(n) for merge buffers + O(log n) for stack = O(n)An 'in-place' algorithm uses O(1) auxiliary space (ignoring input). Algorithms like QuickSort use O(log n) stack space but O(1) extra array space. MergeSort needs O(n) auxiliary space for merging. Tracing reveals these distinctions clearly.
Combined Time-Space Trace Example
Let's trace both time and space for a complete algorithm: finding the two-sum using a hash map.
Two-Sum: Find indices of two numbers that sum to target
1234567891011121314151617
FUNCTION TwoSum(array, target) seen ← empty dictionary // Maps value → index FOR i ← 0 TO length(array) - 1 DO complement ← target - array[i] IF contains_key(seen, complement) THEN RETURN [seen[complement], i] END IF seen[array[i]] ← i END FOR RETURN nullEND FUNCTION Input: array = [2, 7, 11, 15], target = 9| i | array[i] | complement | In seen? | seen (after) | Ops | Space |
|---|---|---|---|---|---|---|
| Init | {} | 1 | O(1) | |||
| 0 | 2 | 7 | No | {2: 0} | ~5 | O(1) |
| 1 | 7 | 2 | Yes! Found | ~4 | O(1) | |
| Total | ~10 | O(n) dict |
Time analysis:
- Best case (found at i=1): 10 operations = O(1) practical, but depends on input
- Worst case (found at end or not found): O(n) iterations × O(1) per iteration = O(n)
- Hash map operations (lookup, insert): O(1) average case
Space analysis:
- seen dictionary: can grow up to n entries = O(n)
- Variables (i, complement): O(1)
- Total auxiliary space: O(n)
Comparison with brute force:
| Aspect | Brute Force (nested loops) | Hash Map Solution |
|---|---|---|
| Time | O(n²) | O(n) |
| Space | O(1) | O(n) |
| Tradeoff | Slower, but no extra memory | Faster, but uses memory |
This is a classic example of trading space for time. By using O(n) extra space (hash map), we reduce time from O(n²) to O(n). Tracing both metrics together reveals these tradeoffs quantitatively.
Identifying Complexity Bottlenecks
When tracing complex algorithms, some parts contribute more to the total complexity than others. Identifying these bottlenecks helps focus optimization efforts.
Bottleneck identification principles:
- •Nested loops dominate — A single O(n) loop inside another O(n) loop creates O(n²). The nested portion dominates any O(n) work outside.
- •Expensive operations inside loops — An O(log n) operation in an O(n) loop creates O(n log n) total. Watch for sorting, binary search, or tree operations in loops.
- •Recursive branching — If a recursive function makes k calls with input size n-1, that's O(kⁿ). The branching dominates.
- •Data structure operations — List insertion might be O(1) amortized but O(n) worst case. In a loop, worst case matters.
- •Hidden complexity — Built-in functions aren't free. Sorting is O(n log n), string concatenation might copy the whole string (O(n)).
123456789101112131415161718192021222324252627282930313233
FUNCTION ProcessData(items) // Step 1: O(n) - linear scan FOR EACH item IN items DO validate(item) // O(1) END FOR // Step 2: O(n²) - BOTTLENECK ← FOR i ← 0 TO length(items) - 1 DO FOR j ← i + 1 TO length(items) - 1 DO compare(items[i], items[j]) END FOR END FOR // Step 3: O(n log n) sortedItems ← sort(items) // Step 4: O(n) - linear scan FOR EACH item IN sortedItems DO output(item) END FOREND FUNCTION Complexity breakdown:- Step 1: O(n)- Step 2: O(n²) ← Dominates!- Step 3: O(n log n)- Step 4: O(n) Total: O(n) + O(n²) + O(n log n) + O(n) = O(n²) The nested loop in Step 2 is the bottleneck. Optimizing Steps 1, 3, 4 won't help.Only improving or eliminating Step 2 improves overall complexity.If 90% of runtime is in the bottleneck, making other parts infinitely fast only gives 10% improvement. Identify and attack the bottleneck first. Tracing with operation counts reveals where the bottleneck is.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.