Data Structure and Algorithm Design
Chapter 4
Xingang (Ian) Fang
Sections
Algorithm Analysis
Algorithm Analysis
Xingang (Ian) Fang
Outline
Motivation
Type of Analysis
Empirical Analysis
Theoretical Analysis
Computational Complexity
Asymptotic Notation
Cases
Examples
Overview
Motivation: To compare algorithms and select the best algorithm for a given problem.
Types of Analysis
Empirical Analysis (FYI)
Implement, run, and measure the performance of the algorithms.
Hardware and software dependent.
A simple approach: <chrono> library in C++.
#include <iostream>
#include <chrono>
int main() {
// Start the clock
auto start = std::chrono::high_resolution_clock::now();
// Call the function to be measured
algorithm(parameter);
// Stop the clock
auto end = std::chrono::high_resolution_clock::now();
// Calculate the duration
std::chrono::duration<double> duration = end - start;
// Print the duration in seconds
std::cout << "Function took " << duration.count() << " seconds." << std::endl;
return 0;
}
Computational Complexity
Definition: The computational complexity of an algorithm is an estimation of the amount of resources required to run it. This model ignores the hardware and software details and focuses on the resources required by the algorithm as the problem size increases.
Resources: Time, Space, power consumption, network traffic, etc.
Time Complexity
The number of constant time operations performed by the algorithm.
A function of the input size n. \(T(n)\) or \(T(N)\)
Approximation: All constant time operations are considered one unit of time.
Space Complexity
The amount of storage space (usually memory) required by the algorithm.
Auxiliary space: The extra space required by the algorithm, excluding the input size.
A function of the input size n. \(S(n)\) or \(S(N)\)
We will focus on \(T(n)\) afterwards and the same concepts apply to \(S(n)\).
Asymptotic Notation
Complexity can be a complicated and non-intuitive function and we need a simple way to describe it.
A.k.a Bachmann–Landau notation. A mathematical notation system to describe the limiting behavior of a function.
Provides a simple way to describe the complexity of an algorithm.
Big O notation \(T(n) = O(f(n))\) describes the upper bound of the function \(T(n)\).
Big Omega notation \(T(n) = \Omega(f(n))\) describes the lower bound of the function \(T(n)\).
Big Theta notation \(T(n) = \Theta(f(n))\) describes the tight bound of the function \(T(n)\).
Little o notation \(T(n) = o(f(n))\) describes the strict upper bound of the function \(T(n)\). (FYI)
How bounds work and which one to choose
Infinite upper and lower bounds exist and we only care the ‘tightest’ ones.
If exist, the exact bound is the tightest upper and lower bound at the same time. Thus, this is the most helpful bound.
If big theta does not exist, the tightest upper bound is the most helpful bound.
E.g. Mark Zuckerberg’s income is an upper bound to your income but it is not helpful to estimate your income because it is not a “tight” upper bound.
Considering Cases
In complexity analysis, various cases are considered to assess the efficiency and performance of algorithms. These cases help us understand how algorithms behave under different scenarios and input distributions.
Best Case: The minimum amount of resources required by the algorithm.
Worst Case: The maximum amount of resources required by the algorithm.
Average Case: The average amount of resources required by the algorithm.
Warning
Bounds and cases are not directly related. Do not treat upper bound as worst case and lower bound as best case!
Each case provides a complexity as a function of problem size and we can use upper/lower/exact bound to estimate the growth rate of this function.
Checkpoint
How many searching and sorting algorithms do you know so far?
What is the complexity of each algorithm?
Is a Merge sort (\(\Theta(n \log n)\)) always faster than an Insertion sort (\(\Theta(n^2)\))?
Answers
You should have learned
Linear search (\(\Theta(n)\))
Binary search (\(\Theta(\log n)\))
Selection sort (\(\Theta(n^2)\))
Insertion sort (\(\Theta(n^2)\))
Maybe Bubble sort (\(\Theta(n^2)\))
As a programmer, we should no longer use the term “faster” to compare algorithms. We should use the term “more or less complex/efficient” instead. Less complex means more efficient. It also means slower growth rate.
Merge sort is more efficient than Insertion sort in terms of time complexity. However, it is not always faster than Insertion sort. The efficiency of an algorithm depends on the problem size, the input distribution, and the hardware characteristics.
Find \(T(n)\) 1
\(T(n)\) = number of constant time operations
Each statement, and expression evaluation is considered to take constant time.
Function calls depend
std::swap, cout <<, getlineare constant timestd::sort, std::search, std::shuffleare not constant time
worst case: \(T(n) = 1 + n + 1 + n + n + 1 = 3n + 3 = \Theta(n)\)
Best case: \(T(n) 1 + 1 + 1 + 1 = 4 = \Theta(1)\)
int search(int * arr, int size, int val) {
for (int i = 0; i < size; ++i)
if (arr[i] == val)
return i;
return -1;
}
\(T(n)\) of for loops
For a loop with \(n\) iterations
Init \(T(n) = 1\)
condition \(T(n) = n + 1\)
update \(T(n) = n\)
body \(T(n) = n\)
for (init; condition; update) {
body statements;
}
Find \(T(n)\) Selection Sort
\(T(n)\) = number of comparisons + number of swaps
number of comparisons = \(1 + 2 + 3 + ... + n - 1 = n(n - 1) / 2\)
number of swaps = \(n - 1\)
best case: no swap needed, sorting a sorted array
\(T(n) = 1 + 2 + 3 + ... + n - 1 = n(n - 1) / 2 = \Theta(n^2)\)
worst case: swap needed every time, sorting a reverse sorted array
\(T(n) = n(n - 1) / 2 + n - 1 = \Theta(n^2)\)
// Sort an array to ascending order
// arr: array to be sorted, must not be nullptr
// size: must be greater than 0
void selectionSort(int * arr, int size) {
for (int i = 0; i < size - 1; ++i) {
int min = i;
for (int j = i + 1; j < size; ++j)
if (arr[j] < arr[min])
min = j;
if (min != i)
std::swap(arr[i], arr[min]);
}
}
Find \(\Theta(f(n))\) given \(T(n)\)
Keep the term with highest order. Drop the coefficient and lower order terms.
Example:
\(T(n) = 20 \times n^2 + 10 \times n + 12 = \Theta(n^2)\)
\(T(n) = 5 \times 2^n + 3 \times n^3 = \Theta(2^n)\)
Exercise: Find a simplest \(\Theta(f(n))\) for the following functions
\(T(n) = 2n^2 + 3n \log n + 10\)
\(T(n) = 2n^2 + 3n + 1 + 5 \times 2^n\)
\(T(n) = 5 \log n + 5n\)
\(T(n) = n! + 2^n + 1000n\)
Term |
Growth Rate |
|---|---|
Constant Time |
\(1\) |
Logarithmic |
\(\log n\) |
Linear |
\(n\) |
Linearithmic |
\(n \log n\) |
Quadratic |
\(n^2\) |
Cubic |
\(n^3\) |
Exponential |
\(2^n\) |
Factorial |
\(n!\) |
Find \(\Theta(f(n))\) directly
Much easier than finding \(T(n)\) first.
additive (sequential, branch)
\(\Theta(f(n)) + \Theta(g(n)) = \Theta(f(n) + g(n))\)
E.g. Initialize an array and then sort it using selection sort: \(\Theta(n) + \Theta(n^2) = \Theta(n^2)\)
multiplicative (nested loop, recursion)
\(\Theta(f(n)) \times \Theta(g(n)) = \Theta(f(n) \times g(n))\)
Where \(f(n)\) and \(g(n)\) are outer loop complexity and per iteration complexity respectively.
Examples:
Selection sort \(\Theta(n) \times \Theta(n) = \Theta(n^2)\)
Binary search \(\Theta(\log n) \times \Theta(1) = \Theta(\log n)\)
Merge sort \(\Theta(\log n) \times \Theta(n) = \Theta(n \log n)\)
Algorithms of Certain Complexity
Name |
Complexity (Prefer Big Theta) |
Representative Algorithms |
|---|---|---|
Constant |
\(\Theta(1)\) |
Array indexing, Hash table lookups |
Logarithmic |
\(\Theta(\log n)\) |
Binary Search, Balanced Binary Search Trees (e.g., AVL, Red-Black) |
Linear |
\(\Theta(n)\) |
Linear Search, Most Simple Array/Linked List Operations |
Linearithmic |
\(\Theta(n \log n)\) |
Merge Sort, Heap Sort, Quick sort |
Quadratic |
\(\Theta(n^2)\) |
Bubble Sort, Selection Sort, Insertion Sort |
Exponential |
\(\Theta(2^n)\) |
Recursive algorithms without memoization (e.g., naive Fibonacci) |
Factorial |
\(\Theta(n!)\) |
Traveling Salesman Problem (naive solution) |
Exercise
Given the following function:
bool isReverse(const char* arr1, const char* arr2, int length) { for (int i = 0; i < length; ++i) if (arr1[i] != arr2[length - 1 - i]) return false; return true; }What is the best and worst case time complexity function?
What is the problem size?
What is the worst case bound? (which bound to use?)
Answers
Best case is when the first comparison fails. The loop will only run once.
Worst case is when all comparisons passes or the last comparison fails. The loop will run \(n\) times.
The problem size is the length of the arrays.
The worst case bound is \(\Theta(n)\).
Amortized Analysis
Motivation: To analyze the average time complexity of a sequence of operations performed on a data structure.
When best, worst, and average cases are not enough to describe the performance of an algorithm or a data structure.
Types of amortized analysis:
Aggregate analysis
Accounting method
Potential method
Example of dynamic data structures that require amortized analysis:
Dynamic arrays (resizing)
Hash tables (rehashing)
Self-balancing binary search trees (balancing)
Splay trees (splaying)