Understanding time complexity is crucial for writing efficient algorithms, especially in competitive programming. This guide covers the fundamental concepts and provides practical examples in C++.
Time Complexity measures the time an algorithm takes to complete as a function of the input size, typically denoted as ( n ). It helps compare the efficiency of different algorithms. Big O Notation expresses time complexity, describing the upper bound of an algorithm's running time, focusing on the worst-case scenario.
Constant Time: O(1)
int getFirstElement(int arr[], int n) {
return arr[0]; // O(1)
}Logarithmic Time: O(log n)
int binarySearch(int arr[], int l, int r, int x) {
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x) return m;
if (arr[m] < x) l = m + 1;
else r = m - 1;
}
return -1; // O(log n)
}Linear Time: O(n)
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
} // O(n)Linearithmic Time: O(n log 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)Quadratic Time: O(n^2)
void printPairs(int arr[], int n) {
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
cout << "(" << arr[i] << ", " << arr[j] << ")";
}
}
} // O(n^2)Cubic Time: O(n^3)
void printTriplets(int arr[], int n) {
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
cout << "(" << arr[i] << ", " << arr[j] << ", " << arr[k] << ")";
}
}
}
} // O(n^3)Exponential Time: O(2^n)
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
} // O(2^n)Factorial Time: O(n!)
void permute(string a, int l, int r) {
if (l == r) cout << a << endl;
else {
for (int i = l; i <= r; i++) {
swap(a[l], a[i]);
permute(a, l + 1, r);
swap(a[l], a[i]); // backtrack
}
}
} // O(n!)Single Loop:
for (int i = 0; i < n; i++) {
// Constant time operations
}
// O(n)Nested Loops:
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// Constant time operations
}
}
// O(n^2)Dependent Nested Loops:
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
// Constant time operations
}
}
// O(n^2)Simple Recursion:
void recursiveFunction(int n) {
if (n <= 1) return;
// Constant time operations
recursiveFunction(n - 1);
}
// O(n)Multiple Recursive Calls:
void recursiveFunction(int n) {
if (n <= 1) return;
// Constant time operations
recursiveFunction(n - 1);
recursiveFunction(n - 1);
}
// O(2^n)Divide and Conquer (e.g., Merge Sort):
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)vector:
vector<int> vec;
vec.push_back(1); // O(1) amortized
vec[0]; // O(1)set:
set<int> s;
s.insert(1); // O(log n)
s.find(1); // O(log n)unordered_set:
unordered_set<int> us;
us.insert(1); // O(1) average case
us.find(1); // O(1) average casevoid dfs(vector<vector<int>>& graph, int v, vector<bool>& visited) {
visited[v] = true;
for (int u : graph[v]) {
if (!visited[u]) dfs(graph, u, visited);
}
}
// Time Complexity: O(V + E), where V is number of vertices and E is number of edgesint knapsack(int W, vector<int>& wt, vector<int>& val, int n) {
vector<vector<int>> K(n + 1, vector<int>(W + 1));
for (int i = 0; i <= n; i++) {
for (int w = 0; w <= W; w++) {
if (i == 0 || w == 0) K[i][w] = 0;
else if (wt[i - 1] <= w)
K[i][w] = max(val[i - 1] + K[i - 1][w - wt[i - 1]], K[i - 1][w]);
else
K[i][w] = K[i - 1][w];
}
}
return K[n][W];
}
// Time Complexity: O(nW)Linear Time Complexity:
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
}
// O(n)Quadratic Time Complexity:
void printPairs(int arr[], int n) {
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
cout << "(" << arr[i] << ", " << arr[j] << ")";
}
}
}
// O(n^2)Logarithmic Time Complexity:
int binarySearch(int arr[], int l, int r, int x) {
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x) return m;
if (arr[m] < x) l = m + 1;
else r = m - 1;
}
return -1;
}
// O(log n)Exponential Time Complexity:
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// O(2^n)While analyzing time complexity, it's also essential to consider space complexity, which measures the amount of memory an algorithm uses as a function of the input size.
Sometimes, improving time complexity may increase space complexity and vice versa. Understanding these trade-offs is essential for optimizing algorithms.
This guide should help you understand the fundamentals of time complexity and how to analyze and optimize your algorithms for competitive programming. Happy coding!