Complexity analysis in computer science is the study of how the performance of an algorithm or a data structure grows with the size of the input. It helps us understand how efficient an algorithm is in terms of time (how fast it runs) and space (how much memory it uses).
There are two main types of complexity:
Time complexity tells us how many operations an algorithm needs to perform relative to the size of the input. It’s often expressed using Big O notation, which describes the worst-case scenario of the algorithm.
Big O notation helps to classify algorithms based on their time or space requirements. Here are some common Big O notations:
O(1) – Constant Time: The algorithm takes the same amount of time regardless of the input size.
int arr[5] = {1, 2, 3, 4, 5};
int element = arr[2]; // O(1)
O(log n) – Logarithmic Time: The algorithm's time increases logarithmically as the input size increases. This usually happens in algorithms that divide the input into smaller parts.
int binarySearch(int arr[], int size, int target) {
int low = 0, high = size - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) low = mid + 1;
else high = mid - 1;
}
return -1;
}
// O(log n)
O(n) – Linear Time: The time grows linearly with the input size. If the input doubles, the execution time doubles.
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
}
// O(n)
O(n log n) – Linearithmic Time: Often occurs in algorithms that involve both dividing the input (like O(log n)) and then processing each part (like O(n)).
void merge(int arr[], int l, int m, int r) {
// Merging logic
}
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
// O(n log n)
O(n²) – Quadratic Time: The time increases quadratically with the input size. If the input size doubles, the time quadruples. This usually happens in algorithms that have nested loops.
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
}
// O(n²)
O(2ⁿ) – Exponential Time: The time doubles with each addition to the input size. This is typical in algorithms that solve problems by recursively exploring all possible solutions.
void towerOfHanoi(int n, char from_rod, char to_rod, char aux_rod) {
if (n == 1) {
cout << "Move disk 1 from rod " << from_rod << " to rod " << to_rod << endl;
return;
}
towerOfHanoi(n - 1, from_rod, aux_rod, to_rod);
cout << "Move disk " << n << " from rod " << from_rod << " to rod " << to_rod << endl;
towerOfHanoi(n - 1, aux_rod, to_rod, from_rod);
}
// O(2ⁿ)
O(n!) – Factorial Time: The time increases dramatically as the input size increases. This is typical in brute-force solutions that try every possible configuration.
Space complexity measures how much memory an algorithm uses, relative to the size of the input. Just like time complexity, space complexity is expressed in Big O notation.
For example, if you're using an extra array to store results, your space complexity might increase. Here are some typical space complexities:
O(1) – Constant space: The algorithm uses the same amount of memory, regardless of the input size.
O(n) – Linear space: The memory usage grows linearly with the input size.
n.When analyzing an algorithm, there are different scenarios:
Best Case: The minimum amount of time the algorithm will take (optimistic scenario).
Worst Case: The maximum amount of time the algorithm will take (pessimistic scenario).
Average Case: The expected time the algorithm will take, considering all possible inputs. This can be harder to calculate but gives a realistic picture of performance.
Let's consider linear search in an unsorted array.
Time Complexity:
Space Complexity: O(1) – No extra space is used apart from a few variables for indexing and comparisons.
Efficiency: It helps in selecting the best algorithm for the task. If you have a large dataset, you want an algorithm that runs efficiently with low time complexity.
Scalability: Complexity analysis tells you how the algorithm will behave as the input size grows. This is especially important when dealing with big data.
Performance: Understanding the complexity helps you optimize both time and space usage, which is crucial for resource-constrained environments like embedded systems or mobile devices.
Complexity analysis helps us measure the efficiency of algorithms by focusing on how they perform with increasing input size. By using Big O notation, we can evaluate the time and space complexity, allowing us to make better choices when designing and implementing algorithms.
Open this section to load past papers